/* Inter = the widget's default face. Newsreader serves the serif template
   (Editorial); Space Grotesk the geometric one (Inline) — both come from the
   "Booking Widget Directions" design file, which sets them on those designs.
   IBM Plex Mono comes from the later "Guided Booking Layouts" file and carries
   every NUMBER in the widget — times, dates, prices, step counts — plus the
   uppercase micro-labels; tabular figures are what stop a column of times
   from looking ragged. Loaded here rather than per-template so a template
   swap never causes a font pop-in. */
@import url('https://fonts.googleapis.com/css2?family=Inter:ital,wght@0,400;0,500;0,600;0,700;0,800;1,400;1,600;1,700&family=IBM+Plex+Mono:wght@400;500;600&family=Newsreader:ital,opsz,wght@0,6..72,400;0,6..72,500;0,6..72,600;1,6..72,400;1,6..72,500&family=Space+Grotesk:wght@400;500;600;700&display=swap');
/**
 * Schedulerity Pro - Public Booking Widget Styles
 *
 * Uses CSS custom properties for easy theming.
 * Scoped under #so-booking-widget / .so-bw-container to avoid conflicts.
 *
 * Supports light (default) and dark themes via [data-theme="dark"].
 */

/* ==========================================================================
   1. CSS Custom Properties (Theming)
   ========================================================================== */

:root {
    --so-bw-primary: #3b82f6;
    --so-bw-primary-hover: #2563eb;
    --so-bw-primary-light: #eff6ff;
    --so-bw-primary-rgb: 59, 130, 246;
    --so-bw-text: #1f2937;
    --so-bw-text-secondary: #6b7280;
    --so-bw-text-tertiary: #9ca3af;
    --so-bw-bg: #ffffff;
    --so-bw-bg-secondary: #f9fafb;
    --so-bw-bg-tertiary: #f3f4f6;
    /* Card surface — kept on its own token (not bound to
       --so-bw-bg-secondary) so themed templates with a tinted
       --so-bw-bg (Fitness Red's cream pink, Boutique Rose's rose,
       Cafe Coffee's yellow, Art Violet's violet, etc.) still get
       a clean white card surface lifted above the tinted body.
       Without this the cards inherited the gray bg-secondary and
       read as a disconnected gray patch on every themed bubble /
       gradient / studio template. The dark-theme override flips it
       to a dark surface to match the dark canvas. */
    --so-bw-card-bg: #ffffff;
    --so-bw-card-bg-hover: #fafafa;
    --so-bw-border: #e5e7eb;
    --so-bw-border-focus: #3b82f6;
    --so-bw-success: #10b981;
    --so-bw-success-light: #d1fae5;
    --so-bw-error: #ef4444;
    --so-bw-error-light: #fee2e2;
    --so-bw-warning: #f59e0b;
    --so-bw-warning-light: #fef3c7;
    --so-bw-radius-sm: 0.375rem;
    --so-bw-radius: 0.5rem;
    --so-bw-radius-lg: 0.75rem;
    --so-bw-radius-xl: 1rem;
    --so-bw-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
    --so-bw-shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
    --so-bw-shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
    --so-bw-font: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    --so-bw-transition: 150ms ease;
    --so-bw-transition-slow: 300ms ease;

    /* ── Design-system tokens — Buttons (tracer bullet) ──────────────
       Templates override THESE tokens, never the raw .so-bw-btn
       padding/font-size/radius declarations directly. Mobile @media
       overrides the same tokens once and every template inherits.
       Goal: structural consistency across all 30 templates with
       identity-only customisation per layout.

       If/when this proof-of-concept extends to the full design
       system (header / card / step / typography), the new tokens
       go in the same block and follow the same rule: no template
       sets a raw value, only the token.
    */
    --so-bw-btn-pad-y: 10px;
    --so-bw-btn-pad-x: 24px;
    --so-bw-btn-font-size: 15px;
    --so-bw-btn-font-weight: 600;
    --so-bw-btn-radius: var(--so-bw-radius);
    --so-bw-btn-letter-spacing: normal;
    --so-bw-btn-text-transform: none;

    /* ── The GUTTER: the widget's one horizontal inset ────────────────
       Header, step content and nav are three stacked regions of the same
       card, so in every layout study of the design file they share one
       left edge. They did not here: the header ran at 44px (32px after
       the design-system block), the step at 24px and the nav at a
       hardcoded 24px — an 8px stair-step at the top of the card on 17 of
       the 19 architectures.

       The 44px was a local fix that broke a global: it was bumped from
       36px so "Step N of M" would stop LOOKING clipped against bubble's
       24px corner radius. That cured the symptom at the right edge and
       misaligned the left edge of every template.

       One token now drives all three. The rule, same as the button and
       header tokens below: an architecture overrides --so-bw-gutter, it
       never sets a raw padding. Mobile/tablet tiers override the token
       once and every architecture inherits. */
    --so-bw-gutter: 24px;

    /* Header */
    --so-bw-header-pad-y-top: 24px;
    --so-bw-header-pad-x: var(--so-bw-gutter);
    --so-bw-header-pad-y-bottom: 18px;
    --so-bw-header-label-size: 18px;
    --so-bw-header-text-transform: none;
    --so-bw-header-letter-spacing: normal;
    --so-bw-header-border: var(--so-bw-border);

    /* Step content area */
    --so-bw-step-pad: 24px;
    --so-bw-step-gap: 0;
    --so-bw-step-margin: 0;
    --so-bw-step-radius: 0;
    --so-bw-step-bg: transparent;

    /* Card */
    --so-bw-card-pad: 16px;
    --so-bw-card-gap: 16px;
    --so-bw-card-radius: var(--so-bw-radius-lg);
    --so-bw-card-border-width: 1px;
    --so-bw-card-title-size: 15px;
    --so-bw-card-meta-size: 13px;
    /* The narrowest a card may get before the grid drops a column. It is the
       only number behind the column count — see the note at .so-bw-card-grid.
       180px fits five across a ~1050px content pane and still holds a
       two-line service name; the card is a centred/left stack now, so its
       title has the full track rather than what was left beside a mark, which
       is what makes a narrower track readable than it used to be. Templates
       whose grid sits in a narrow pane raise this for themselves. */
    --so-bw-card-min: 180px;

    /* Avatar tile (sized for the .so-bw-card-icon pin).
       SVG and text sizes are tuned to read at comparable visual
       weight to a photo that fills the entire circle: ~58% glyph
       diameter for SVG, ~40% cap-height for initials. Closes the
       perceived size gap between photo / Lucide icon / initials
       avatars on the staff grid. */
    --so-bw-icon-size: 48px;
    --so-bw-icon-svg-size: 28px;
    --so-bw-icon-text-size: 19px;

    /* Form fields (.so-bw-input / .so-bw-textarea / .so-bw-select) */
    --so-bw-input-pad-y: 10px;
    --so-bw-input-pad-x: 14px;
    --so-bw-input-font-size: 15px;
    --so-bw-input-radius: var(--so-bw-radius);
    --so-bw-input-label-size: 13px;

    /* Time slot grid (.so-bw-time-slot) — modernised: smaller pill
       shape, tighter gap, slightly smaller text so a typical 8-slot
       day fits in two compact rows instead of pushing the page. */
    --so-bw-slot-pad-y: 9px;
    --so-bw-slot-pad-x: 10px;
    --so-bw-slot-font-size: 13px;
    --so-bw-slot-radius: 999px;
    --so-bw-slot-gap: 6px;

    /* Calendar (.so-bw-calendar-day, .so-bw-calendar-nav) —
       compacted: circular day cells (50%), tiny nav buttons,
       smaller font. The whole calendar is also constrained to a
       max-width below so it sits as a focused mini-month rather
       than spanning the full step width. */
    --so-bw-calendar-day-font-size: 13px;
    --so-bw-calendar-day-radius: 50%;
    --so-bw-calendar-nav-size: 28px;
    --so-bw-calendar-nav-radius: 50%;

    /* Summary / review pane */
    --so-bw-summary-pad-y: 12px;
    --so-bw-summary-pad-x: 20px;
    --so-bw-summary-radius: var(--so-bw-radius-lg);

    /* Timezone picker (compact inline dropdown in the Date/Time step).
       Kept on its own token so it can stay visually smaller than a
       full form field while still respecting the input radius and
       border tokens. Mobile sets this to 16px to prevent iOS
       auto-zoom on focus — same guard as --so-bw-input-font-size. */
    --so-bw-tz-select-font-size: 13px;
    --so-bw-tz-select-pad-y: 5px;
    --so-bw-tz-select-pad-x: 10px;
}

/* Dark theme overrides */
[data-theme="dark"] {
    --so-bw-primary-light: rgba(59, 130, 246, 0.15);
    --so-bw-text: #f9fafb;
    --so-bw-text-secondary: #9ca3af;
    --so-bw-text-tertiary: #6b7280;
    --so-bw-bg: #1f2937;
    --so-bw-bg-secondary: #111827;
    --so-bw-bg-tertiary: #374151;
    --so-bw-card-bg: #1e293b;
    --so-bw-card-bg-hover: #273449;
    --so-bw-border: #374151;
    --so-bw-success-light: rgba(16, 185, 129, 0.15);
    --so-bw-error-light: rgba(239, 68, 68, 0.15);
    --so-bw-warning-light: rgba(245, 158, 11, 0.15);
    --so-bw-shadow: 0 1px 3px rgba(0, 0, 0, 0.3), 0 1px 2px rgba(0, 0, 0, 0.2);
    --so-bw-shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -1px rgba(0, 0, 0, 0.2);
    --so-bw-shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.15);
}

/* ==========================================================================
   2. Widget Container
   ========================================================================== */

#so-booking-widget {
    font-family: var(--so-bw-font);
    font-size: 15px;
    color: var(--so-bw-text);
    line-height: 1.5;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    /* Separate the appointment booking widget from whatever follows it
       on the page (e.g. the [scheduleos_events] container stacked below
       on the booking page) so the two don't sit flush together. */
    margin-bottom: 40px;
}

/* box-sizing applies to EVERYTHING inside the widget, unconditionally. */
#so-booking-widget *,
#so-booking-widget *::before,
#so-booking-widget *::after {
    box-sizing: border-box;
}

/* ── The margin/padding reset, scoped away from our own components ───────
   This reset exists to stop a host theme's `p {}` / `div {}` / `ul {}` from
   leaking spacing into the widget. That part still works below.

   What it must NOT do is outrank the widget's own component rules. Being
   ID-qualified it scored (1,0,0), which beats every plain `.so-bw-*` rule
   at (0,1,0) — so 119 non-zero margin/padding declarations across ~90
   components were being silently discarded. `.so-bw-badge { padding: 2px
   8px }` resolved to 0, collapsing the price/duration pill on every card
   onto its own text; `.so-bw-card-title/-meta/-badges` lost their margins,
   so card interiors had no vertical rhythm at all.

   That was invisible for years because each retired base layout restated
   the paddings inside `#so-booking-widget[data-template="…"]` blocks at
   (1,1,1) and quietly won. The Guided Booking Layouts consolidation
   retired those layout keys and the spacing went with them — see the
   partial (1,1,0) rescue near the end of this file, which patched eight
   components and left the rest at zero.

   Excluding so-bw-classed elements fixes the whole class of bug at the
   source: each component's own rule applies exactly as authored, and
   architecture/signature overrides still win on top as before.

   Safe because every so-bw-classed element that carries a user-agent
   default margin/padding declares its own. Verified across all 21 such
   elements in the rendered markup (h2/h3/p/li/ul); both <ul>s
   (.so-bw-policies-items, .so-bw-tz-combo-list) declare padding too, so
   the UA `padding-inline-start: 40px` is covered. If you add a heading,
   list or paragraph that takes a so-bw class, give it an explicit margin
   (and padding, for a list) rather than relying on this reset. */
#so-booking-widget *:not([class*="so-bw-"]),
#so-booking-widget *:not([class*="so-bw-"])::before,
#so-booking-widget *:not([class*="so-bw-"])::after {
    margin: 0;
    padding: 0;
}

/* Alpine.js x-cloak support */
[x-cloak] {
    display: none !important;
}

/* ── Phone: let the grid tracks actually shrink ──────────────────────────
   Most templates lay .so-bw-container out as a grid and collapse it to a
   single `1fr` column on mobile. `1fr` is minmax(AUTO, 1fr), and a grid
   item's `min-width` is also `auto` — both floor the track at the item's
   MIN-CONTENT. The summary rail's min-content is ~400-480px, so on a phone
   the single column refused to go below that: Accordion pushed the whole
   widget sideways (page scrollWidth 416 in a 288px viewport), while Tabbed
   and Split clipped the rail instead, because their container is
   overflow-x: hidden. Either way the mobile layout was wrong.

   Releasing the item's automatic minimum makes the track's auto minimum 0
   too, so `1fr` behaves like minmax(0, 1fr) everywhere without having to
   restate the track in each of the fifteen templates' media blocks. Scoped
   to the container's own children, and only below the layout breakpoint. */
/* was @media (max-width:860px) — now gated on the CONTAINER width
   via data-cw (see booking-widget.js _wireContainerWidth). :where()
   keeps specificity identical to the media query it replaces. */

    #so-booking-widget:where([data-cw~="860"]) .so-bw-container > *{ min-width: 0; }


.so-bw-container {
    max-width: 680px;
    margin: 0 auto;
    background: var(--so-bw-bg);
    border: 1px solid var(--so-bw-border);
    border-radius: var(--so-bw-radius-xl);
    box-shadow: var(--so-bw-shadow-md);
    overflow: hidden;
    position: relative;
}

/* ==========================================================================
   3. Header & Progress
   ========================================================================== */

.so-bw-header {
    /* Padding bound to design-system header tokens. Template
       overrides set --so-bw-header-pad-* on .so-bw-container --
       never change this rule directly. Mobile @media at the end
       of section 12 collapses the tokens once for every template.
       Border-bottom is also tokenised so colored-header templates
       (classic / bubble / gradient / dark) can drop the hairline
       (transparent) and let the header colour flow flush into the
       step content; light-header templates keep the divider so the
       header reads as a separated band against the white step. */
    padding: var(--so-bw-header-pad-y-top) var(--so-bw-header-pad-x) var(--so-bw-header-pad-y-bottom);
    border-bottom: 1px solid var(--so-bw-header-border);
    background: var(--so-bw-bg-secondary);
}

/* ── Theme layout knob: header alignment ─────────────────────────────────
   The ONE structural knob this widget can honour. `data-so-header` is emitted
   only when a widget theme is chosen, so an unthemed install matches nothing
   here and is unchanged.

   .so-bw-step-indicator is a `justify-content: space-between` row holding the
   step label and the "Step 1 of 6" counter. Left as-is under a centred header
   it produces a centred title above a title-left/counter-right row, which reads
   as a centre that failed. Stacking it is the only arrangement that looks
   deliberate. */
#so-booking-widget[data-so-header="center"] .so-bw-header {
    text-align: center;
}
#so-booking-widget[data-so-header="center"] .so-bw-step-indicator {
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 6px;
}

/* Brand header (Studio → Content → Widget Title / Subtitle). Optional block
   above the step indicator; only rendered when the admin sets a title.

   Colour comes from --so-bw-text / --so-bw-text-secondary rather than a
   literal, so it inverts on the dark templates instead of going invisible on
   a dark header band. Both tokens are already re-declared per template. */
.so-bw-brand {
    margin-bottom: 14px;
    /* The mark beside the words, not above them: the header is a horizontal
       band on every architecture, so a stacked mark would spend height the
       band does not have. `flex-start` rather than `center` because the block
       may be one line or two, and a mark that slides down when a subtitle
       appears reads as a different design on two otherwise identical sites. */
    display: flex;
    align-items: flex-start;
    gap: 12px;
}
.so-bw-brand__text {
    min-width: 0;
}
/* A tinted rounded square holding the accent glyph — the same shape language
   as the card's mark, at the header's scale. The tint is mixed from the brand
   colour rather than picked, so it follows a runtime accent change; the glyph
   takes the readable ink tone, because at 20px a button-fill accent on a pale
   tint of itself is the one place the ramp gets too close. */
.so-bw-brand__mark {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: none;
    width: 40px;
    height: 40px;
    border-radius: var(--so-bw-r-11);
    background: color-mix(in srgb, var(--so-bw-primary) 12%, var(--so-bw-card-bg, #fff));
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}
.so-bw-brand__mark svg {
    width: 20px;
    height: 20px;
}

.so-bw-brand__title {
    font-size: 18px;
    font-weight: 700;
    line-height: 1.25;
    letter-spacing: -0.01em;
    color: var(--so-bw-text);
}

.so-bw-brand__subtitle {
    margin-top: 3px;
    font-size: 13px;
    line-height: 1.45;
    color: var(--so-bw-text-secondary);
}

/* Narrow screens: the header already carries a step label, a step bar and a
   progress bar. Shrink the brand block rather than let it push the actual
   booking controls below the fold on a phone — the densest templates
   (Compact, Tabbed Rail) are tightest here. */
/* was @media (max-width: 480px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    #so-booking-widget:where([data-cw~="480"]) .so-bw-brand{
        margin-bottom: 10px;
    }

    #so-booking-widget:where([data-cw~="480"]) .so-bw-brand__title{
        font-size: 16px;
    }

    #so-booking-widget:where([data-cw~="480"]) .so-bw-brand__subtitle{
        font-size: 12px;
    }


.so-bw-step-indicator {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 4px 12px;
    margin-bottom: 12px;
    /* Both children carry translated text of unpredictable length ("Choose
       your preferred member of staff" / "Schritt 3 von 8"). Wrapping the ROW
       drops the counter onto its own line when the label needs the width,
       instead of squeezing one of them into an overflow. */
    flex-wrap: wrap;
}

.so-bw-step-label {
    font-size: var(--so-bw-header-label-size);
    font-weight: 700;
    line-height: 1.3;
    letter-spacing: var(--so-bw-header-letter-spacing);
    text-transform: var(--so-bw-header-text-transform);
    color: var(--so-bw-text);
    /* min-width:0 releases the flex item's automatic minimum so a long
       label wraps inside its cell rather than widening the header. */
    min-width: 0;
    overflow-wrap: anywhere;
}

.so-bw-step-count {
    font-size: 13px;
    font-weight: 500;
    line-height: 1.3;
    color: var(--so-bw-text-secondary);
    /* Was `white-space: nowrap` with `flex-shrink: 0` — on a 320px phone
       with a translated counter that reserved width the label needed and
       pushed the row past the header's padding. The row wraps now, so the
       counter can keep its own line and still break if it has to. */
    flex-shrink: 0;
    min-width: 0;
    overflow-wrap: anywhere;
}

.so-bw-progress {
    width: 100%;
    height: 6px;
    background: var(--so-bw-bg-tertiary);
    border-radius: var(--so-bw-r-3);
    overflow: hidden;
}

.so-bw-progress-bar {
    height: 100%;
    background: var(--so-bw-primary);
    border-radius: var(--so-bw-r-3);
    transition: width var(--so-bw-transition-slow);
    min-width: 6px;
}

/* ==========================================================================
   4. Error Banner
   ========================================================================== */

.so-bw-error {
    padding: 0 24px;
}

.so-bw-error-content {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 12px 16px;
    margin-top: 16px;
    background: var(--so-bw-error-light);
    border: 1px solid var(--so-bw-error);
    border-radius: var(--so-bw-radius);
    color: var(--so-bw-error);
    font-size: 14px;
    font-weight: 500;
}

.so-bw-error-dismiss {
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 24px;
    height: 24px;
    background: none;
    border: none;
    color: var(--so-bw-error);
    font-size: 18px;
    font-weight: 700;
    cursor: pointer;
    border-radius: 50%;
    transition: background var(--so-bw-transition);
}

.so-bw-error-dismiss:hover {
    background: rgba(239, 68, 68, 0.15);
}

/* ==========================================================================
   5. Loading Overlay
   ========================================================================== */

.so-bw-loading {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 12px;
    background: rgba(255, 255, 255, 0.85);
    z-index: 50;
    font-size: 14px;
    color: var(--so-bw-text-secondary);
    font-weight: 500;
}

[data-theme="dark"] .so-bw-loading {
    background: rgba(31, 41, 55, 0.85);
}

.so-bw-loading-inline {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 16px 0;
    font-size: 14px;
    color: var(--so-bw-text-secondary);
}

.so-bw-spinner {
    width: 32px;
    height: 32px;
    border: 3px solid var(--so-bw-border);
    border-top-color: var(--so-bw-primary);
    border-radius: 50%;
    animation: so-bw-spin 600ms linear infinite;
}

.so-bw-spinner--small {
    width: 18px;
    height: 18px;
    border-width: 2px;
}

.so-bw-spinner--white {
    border-color: rgba(255, 255, 255, 0.3);
    border-top-color: #ffffff;
}

@keyframes so-bw-spin {
    to {
        transform: rotate(360deg);
    }
}

/* ==========================================================================
   6. Hold Timer Banner
   ========================================================================== */

.so-bw-hold-timer {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 10px 24px;
    background: var(--so-bw-warning-light);
    border-bottom: 1px solid var(--so-bw-warning);
    color: var(--so-bw-warning);
    font-size: 13px;
    font-weight: 600;
}

.so-bw-hold-timer svg {
    flex-shrink: 0;
}

/* Under a minute the banner escalates from "informational amber" to the
   template's danger colour, so an imminent expiry is visibly different from
   the calm state a customer has been ignoring for four minutes. Tokens only,
   so every template escalates in its own palette. */
.so-bw-hold-timer--urgent {
    background: var(--so-bw-danger-light, var(--so-bw-warning-light));
    border-bottom-color: var(--so-bw-danger, var(--so-bw-warning));
    color: var(--so-bw-danger, var(--so-bw-warning));
}

/* "Keep this time" — inline in the banner, not a .so-bw-btn: it must read as
   secondary to Continue/Confirm in the nav, which is still the action the
   customer is meant to take. */
.so-bw-hold-extend {
    margin-inline-start: auto;
    padding: 4px 10px;
    background: transparent;
    border: 1px solid currentColor;
    border-radius: var(--so-bw-ds-radius-chip, 8px);
    color: inherit;
    font: inherit;
    font-size: 12px;
    font-weight: 700;
    line-height: 1.2;
    cursor: pointer;
    transition: background var(--so-bw-transition), opacity var(--so-bw-transition);
}
.so-bw-hold-extend:hover {
    background: color-mix(in srgb, currentColor 12%, transparent);
}
.so-bw-hold-extend:focus-visible {
    outline: 2px solid currentColor;
    outline-offset: 2px;
}
.so-bw-hold-extend:disabled {
    opacity: 0.6;
    cursor: default;
}

/* ==========================================================================
   7. Step Content Area
   ========================================================================== */

.so-bw-step {
    /* --so-bw-step-pad carries the VERTICAL padding; the horizontal inset is
       the shared gutter, so the step content can never drift off the header's
       and nav's left edge — at any breakpoint. */
    padding: var(--so-bw-step-pad) var(--so-bw-gutter);
    margin: var(--so-bw-step-margin);
    border-radius: var(--so-bw-step-radius);
    background: var(--so-bw-step-bg);
    min-height: 200px;
}

.so-bw-step-desc {
    font-size: 14px;
    color: var(--so-bw-text-secondary);
    margin-bottom: 20px;
}

/* ── Form Builder multi-page indicator (custom-fields step) ──────────────
   Pages WITHIN step 6, so this is deliberately lighter than the widget's own
   step header — it must not compete with it or the customer sees two progress
   bars and can't tell which one the Continue button drives.

   Two navigation styles come from the form's Steps tab:
     buttons — the pages are a read-only indicator; Continue/Back move you
     tabs    — the pages are clickable (forward jumps still validate)
   `is-locked` is what makes the button-style pips read as non-interactive. */
.so-bw-fsteps {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin-bottom: 16px;
}

.so-bw-fstep {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    padding: 6px 11px 6px 7px;
    border: 1px solid var(--so-bw-border, #e5e7eb);
    border-radius: 999px;
    background: transparent;
    color: var(--so-bw-text-tertiary, #9ca3af);
    font: inherit;
    font-size: 12.5px;
    font-weight: 600;
    line-height: 1;
    cursor: pointer;
    transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}

.so-bw-fstep.is-locked {
    cursor: default;
}

.so-bw-fstep__num {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 19px;
    height: 19px;
    border-radius: 50%;
    background: var(--so-bw-border, #e5e7eb);
    color: var(--so-bw-text-secondary, #6b7280);
    font-size: 11px;
    font-weight: 700;
    flex-shrink: 0;
}

/* A completed page keeps the accent so the customer can see how far they've
   come, but stays quieter than the one they're on. */
.so-bw-fstep.is-done {
    border-color: var(--so-bw-primary, #6366f1);
    color: var(--so-bw-primary-ink, #4f46e5);
}
.so-bw-fstep.is-done .so-bw-fstep__num {
    background: var(--so-bw-primary, #6366f1);
    color: #fff;
}

.so-bw-fstep.is-active {
    border-color: var(--so-bw-primary, #6366f1);
    background: var(--so-bw-primary, #6366f1);
    color: #fff;
}
.so-bw-fstep.is-active .so-bw-fstep__num {
    background: rgba(255, 255, 255, 0.25);
    color: #fff;
}

/* Only the tabs style is interactive, so only it gets a hover affordance. */
.so-bw-fstep:not(.is-locked):not(.is-active):hover {
    border-color: var(--so-bw-primary, #6366f1);
    color: var(--so-bw-primary-ink, #4f46e5);
}

/* Narrow screens: the page NAME is the first thing to go — the numbered pip
   still communicates position, and three or four full names wrap into a block
   taller than the fields they label. */
/* was @media (max-width: 480px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    #so-booking-widget:where([data-cw~="480"]) .so-bw-fstep__name{
        display: none;
    }
    #so-booking-widget:where([data-cw~="480"]) .so-bw-fstep{
        padding: 6px;
    }


.so-bw-empty {
    text-align: center;
    padding: 32px 16px;
    color: var(--so-bw-text-tertiary);
    font-size: 14px;
}

.so-bw-empty p {
    margin: 0;
}

/* Staff-unavailability banner shown on the date/time step. */
.so-bw-unavailable {
    display: flex;
    align-items: flex-start;
    gap: 12px;
    margin: 16px 0;
    padding: 14px 16px;
    border: 1px solid #fecaca;
    background: #fef2f2;
    border-radius: var(--so-bw-r-10);
    color: #7f1d1d;
}

.so-bw-unavailable svg {
    color: #dc2626;
    flex-shrink: 0;
    margin-top: 2px;
}

.so-bw-unavailable strong {
    display: block;
    font-size: 14px;
    color: #991b1b;
    margin-bottom: 4px;
}

.so-bw-unavailable p {
    margin: 0;
    font-size: 13px;
    line-height: 1.45;
}

.so-bw-unavailable-hint {
    margin-top: 4px !important;
    color: #b91c1c !important;
    font-size: 12px !important;
}

/* ==========================================================================
   8. Cards (Location, Service, Staff, Add-ons)
   ========================================================================== */

.so-bw-card-grid {
    display: grid;
    /* Column count follows the grid's OWN width, not the viewport.
       The two-pane (rail) templates put this grid in a ~500px right
       pane even on a 1440px screen, so a viewport-driven `repeat(2, 1fr)`
       produced ~210px cards whose title cell collapsed to ~70px and
       broke titles a word (or a syllable) per line -- "Aromath / erapy
       / Massage". The minimum keeps a card wide enough for a real title
       and drops to a single column whenever the container itself is narrow.

       ── The minimum is a TOKEN, and that is what makes the count adapt ────
       `auto-fill` fits as many whole tracks as the grid's own width allows, so
       the column count is never written down anywhere: a 1050px content pane
       takes five, a 610px pane three, a 380px phone one, and an embed resized
       in the Studio re-flows without a breakpoint. The only number is how
       narrow a card is allowed to get, and it is a variable so a template
       whose grid lives in a narrow pane can raise it for itself rather than
       restating the whole track expression. */
    grid-template-columns: repeat(auto-fill, minmax(min(100%, var(--so-bw-card-min)), 1fr));
    gap: 12px;
}

.so-bw-card {
    display: flex;
    /* ── A CENTRED STACK, not a row ───────────────────────────────────────
       The card was a ROW — round mark on the left, text column beside it —
       and `align-items: center` was there to stop a card with no bio leaving
       its mark "hanging" at the top beside a neighbour with two lines.

       It reads top-to-bottom now: mark, name, description, then the two fact
       pills on the card's bottom edge. A grid of cards is scanned by name, and
       a centred name under its own mark is one column of text to run an eye
       down instead of a name whose left edge is set by how wide the mark is.
       `align-items: center` survives the change and does the same job on the
       other axis — the stack is centred in the card's width.

       FOUR ARCHITECTURES KEEP THE ROW, and each has a reason that is about
       its own composition rather than about the card:
         counter, roster   place the card's parts with `grid-area`; a flex
                           direction here is not what lays them out.
         weekboard, wide   restructure the service card themselves — Wide
                           Canvas's whole proposition is a comparison ROW,
                           where the marks line up in a column down the page.
       Tabbed Rail also keeps its own: a cover band above a left-aligned
       block, which is a picture card, not this.
       All five state it in their own sections; nothing is excluded here.

       The DIRECTION is the base default; the CENTRING is not, and the
       difference was measured. Setting `text-align: center` here too put
       centred text on thirteen live architectures that override the direction
       back to a row — they had no reason to have an opinion about text-align,
       because the card was never centred before, so they silently inherited
       it and read as centred text beside a left-hand mark. `text-align` is
       therefore stated on the architectures whose card actually IS a stack,
       in one allowlist further down; a layout that overrides the direction
       cannot inherit a centring that was never declared. */
    align-items: center;
    flex-direction: column;
    gap: var(--so-bw-card-gap);
    padding: var(--so-bw-card-pad);
    border: var(--so-bw-card-border-width) solid var(--so-bw-border);
    border-radius: var(--so-bw-card-radius);
    cursor: pointer;
    transition: border-color var(--so-bw-transition),
                background var(--so-bw-transition),
                box-shadow var(--so-bw-transition);
    /* Bound to --so-bw-card-bg (clean white in light theme, dark
       surface in dark theme). The previous --so-bw-bg-secondary
       binding read as a flat gray patch on themed templates that
       set a tinted --so-bw-bg (Fitness Red, Boutique Rose, Cafe
       Coffee, Art Violet, etc.) — gray cards on cream/rose/yellow
       canvases looked disconnected. White cards lift cleanly off
       any tinted body and the dark override keeps dark templates
       on a dark surface. Border dropped from 2px to 1px so the
       card chrome doesn't dominate the now-cleaner surface. */
    background: var(--so-bw-card-bg);
    position: relative;
}

.so-bw-card:hover {
    border-color: var(--so-bw-primary);
    box-shadow: var(--so-bw-shadow);
}

/* Service cards (Step 2) get a vertical layout so the inline
   "+ Add Extras" toggle and the expandable add-ons panel can
   stack below the top row (icon + body + check). The top row
   keeps the original horizontal flex layout via .so-bw-card-top
   below; .so-bw-card itself drops its own padding so the inner
   sections control their own spacing. Other card flavours
   (staff, location, add-on) still inherit the default flex-row
   .so-bw-card layout — this only kicks in for service cards.
   Selectors include #so-booking-widget + !important so the rule
   beats per-template overrides (e.g. the minimal/zen-stone block
   that forces .so-bw-card { flex-direction: row; padding: 12px;
   !important } and would otherwise break the column layout). The
   [data-template] attribute selector bumps specificity to (1,2,0) so
   it beats per-template .so-bw-card { flex-direction: row !important }
   rules that sit at the same (1,2,0) level but come EARLIER in source
   order. Without this boost the minimal / gradient / classic / bubble
   preview-mirror blocks all set flex-direction: row !important on
   .so-bw-card and the service card's vertical layout (card-top above,
   extras-toggle strip below) collapsed into a single horizontal row on
   those templates — the "+ Add extras" badge drifted next to the icon
   instead of living in its own full-width bottom strip. */
#so-booking-widget[data-template] .so-bw-card--service {
    flex-direction: column !important;
    align-items: stretch !important;
    padding: 0 !important;
    gap: 0 !important;
}
/* Also cover the no-data-template case (plain widget, no Studio save) */
#so-booking-widget .so-bw-card--service {
    flex-direction: column !important;
    align-items: stretch !important;
    padding: 0 !important;
    gap: 0 !important;
}
#so-booking-widget .so-bw-card-top {
    display: flex;
    /* A CENTRED STACK: mark, name, description, then the two facts. See the
       note at .so-bw-card below for why the card reads top-to-bottom now and
       which architectures keep the row. */
    flex-direction: column;
    align-items: center;
    gap: var(--so-bw-card-gap);
    padding: var(--so-bw-card-pad);
    cursor: pointer;
    min-width: 0;
    width: 100%;
    /* `overflow: hidden` used to sit here as a blunt guard against the icon
       tile and the title fighting for width. It cost more than it bought: the
       row is height:auto, so it never clipped vertically, but it DID cut the
       right edge off a long title, swallow the focus ring on keyboard
       navigation, and hide any badge that wrapped past the row. The tile is
       bounded by its own min/max tokens and the body carries min-width:0, so
       the width is settled without clipping anything. */
    overflow: visible;
}
#so-booking-widget .so-bw-card--service.so-bw-card--has-extras-open {
    border-color: var(--so-bw-primary);
}

/* --------------------------------------------------------------------------
   Service image gallery (Appearance → Image, multi-image)

   Sits as the FIRST child of .so-bw-card--service, above .so-bw-card-top, and
   fills the card edge to edge -- the service card already zeroes its own
   padding (see the rule above) and lets each inner section own its spacing.

   Every rule is scoped under #so-booking-widget so it wins against the
   per-template blocks further down this file, which are aggressive about
   .so-bw-card descendants (several set `background`/`color`/`flex-direction`
   with !important). None of them mention these classes, so this one shared
   block is what all nineteen templates render -- the media strip inherits each
   template's own --so-bw-card-radius / --so-bw-primary tokens instead of
   needing nineteen copies.

   A fixed aspect ratio (not an intrinsic height) is what keeps a row of cards
   aligned regardless of the uploaded images' dimensions, and stops the card
   from reflowing as the customer steps through the carousel.
   -------------------------------------------------------------------------- */
#so-booking-widget .so-bw-card-media {
    position: relative;
    width: 100%;
    aspect-ratio: 16 / 10;
    flex: 0 0 auto;
    overflow: hidden;
    /* Placeholder tint behind a still-loading or transparent image. */
    background: var(--so-bw-bg-tertiary);
    /* Top corners only: the strip caps the card, the rest of the card keeps
       its own bottom radius. Matches --so-bw-card-radius exactly rather than
       insetting by the border width -- a sub-pixel overshoot is invisible and
       calc() against an unset token would drop the whole declaration. */
    border-radius: var(--so-bw-card-radius) var(--so-bw-card-radius) 0 0;
}

#so-booking-widget .so-bw-card-media-img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
    /* Reset the .so-bw-card-img legacy sizing and any theme img rule that
       would otherwise pin a 48px avatar box on this one. */
    max-width: 100%;
    border-radius: inherit;
    margin: 0;
}

/* Prev / next. Always visible rather than hover-only: the carousel has to be
   discoverable on touch, where there is no hover state at all. */
#so-booking-widget .so-bw-card-media-nav {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    z-index: 2;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    padding: 0;
    border: none;
    border-radius: 50%;
    background: rgba(17, 24, 39, 0.5);
    color: #fff;
    cursor: pointer;
    opacity: 0.9;
    transition: background var(--so-bw-transition), opacity var(--so-bw-transition);
    /* Neutralize per-template .so-bw-card button rules (font inheritance,
       min-widths) so the control stays a circle everywhere. */
    min-width: 0;
    line-height: 1;
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.25);
}
#so-booking-widget .so-bw-card-media-nav:hover,
#so-booking-widget .so-bw-card-media-nav:focus-visible {
    background: rgba(17, 24, 39, 0.78);
    opacity: 1;
}
/* Hit area, not appearance. The visible circle stays 28px, but these controls
   sit INSIDE a card whose own click selects the service and advances the step,
   so a near-miss did not just fail — it booked something. A centred ::before
   widens the tappable region to the 44px touch target without moving or
   resizing anything on screen. */
#so-booking-widget .so-bw-card-media-nav::before {
    content: '';
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: 44px;
    height: 44px;
    border-radius: 50%;
}
#so-booking-widget .so-bw-card-media-nav--prev { left: 8px; }
#so-booking-widget .so-bw-card-media-nav--next { right: 8px; }
#so-booking-widget .so-bw-card-media-nav > svg {
    width: 16px;
    height: 16px;
    flex-shrink: 0;
}

/* Position dots — also the count indicator, so the customer can see there is
   more than one photo without hunting for the arrows. */
#so-booking-widget .so-bw-card-media-dots {
    position: absolute;
    z-index: 2;
    left: 50%;
    bottom: 8px;
    transform: translateX(-50%);
    display: flex;
    align-items: center;
    /* 10px (was 5px) so each dot's hit area below can be 16px wide and still
       tile edge-to-edge instead of overlapping its neighbour — a near-miss then
       lands on SOME dot rather than falling through to the card. */
    gap: 10px;
    /* A scrim keeps the dots legible over a light image without darkening
       the photo itself. */
    padding: 4px 7px;
    border-radius: 999px;
    background: rgba(17, 24, 39, 0.32);
}
#so-booking-widget .so-bw-card-media-dot {
    position: relative;
    width: 6px;
    height: 6px;
    min-width: 0;
    padding: 0;
    border: none;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.6);
    cursor: pointer;
    transition: background var(--so-bw-transition), transform var(--so-bw-transition);
}
/* A 6px dot is far under the 24px minimum target, and these sit inside the
   click-to-book card, so a missed tap used to select the service and advance
   the step — the customer wanted photo 2 and got the date picker. The ::before
   widens the target to 16x24 (the full height of the strip, and the exact
   6px-dot + 10px-gap pitch, so targets tile without overlapping) while the
   visible dot stays 6px. Full 24x24 would need larger dots or wider spacing —
   a visual-design call, noted rather than taken here. */
#so-booking-widget .so-bw-card-media-dot::before {
    content: '';
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: 16px;
    height: 24px;
}
#so-booking-widget .so-bw-card-media-dot--on {
    background: var(--so-bw-primary);
    transform: scale(1.35);
}
#so-booking-widget .so-bw-card-media-dot:hover {
    background: #fff;
}

/* Dark surfaces: the placeholder tint behind the image needs to read as part
   of the dark card, not a light patch while the photo loads. */
#so-booking-widget[data-theme="dark"] .so-bw-card-media {
    background: rgba(255, 255, 255, 0.06);
}

/* Narrow viewports: a single-column card grid makes each card much wider, so
   a 16:10 strip becomes very tall. Squarer ratio + smaller controls keeps the
   service list scannable on a phone. */
/* was @media (max-width: 480px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    #so-booking-widget:where([data-cw~="480"]) .so-bw-card-media{
        aspect-ratio: 3 / 2;
    }
    #so-booking-widget:where([data-cw~="480"]) .so-bw-card-media-nav{
        width: 26px;
        height: 26px;
    }
    #so-booking-widget:where([data-cw~="480"]) .so-bw-card-media-nav--prev{ left: 6px; }
    #so-booking-widget:where([data-cw~="480"]) .so-bw-card-media-nav--next{ right: 6px; }
    #so-booking-widget:where([data-cw~="480"]) .so-bw-card-media-dots{ bottom: 6px; }


/* Respect reduced-motion: the dot scale is decoration, not information (the
   brand colour already marks the active dot). */
@media (prefers-reduced-motion: reduce) {
    #so-booking-widget .so-bw-card-media-dot,
    #so-booking-widget .so-bw-card-media-nav {
        transition: none;
    }
    #so-booking-widget .so-bw-card-media-dot--on {
        transform: none;
    }
}

/* "+ Add Extras" toggle pill — hugs the bottom edge of the top
   row, full-width strip below. Subtle separator above to read
   as part of the same card. Brand-tinted hover. Selector uses
   #so-booking-widget so the rule wins over any per-template
   button styles (some templates have aggressive .so-bw-card
   button !important rules). */
#so-booking-widget .so-bw-card-extras-toggle {
    display: flex !important;
    align-items: center;
    justify-content: center;
    gap: 6px;
    width: 100%;
    padding: 8px 14px;
    background: transparent;
    border: none;
    border-top: 1px dashed var(--so-bw-border);
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
    font-family: inherit;
    font-size: 12px;
    font-weight: 600;
    cursor: pointer;
    transition: background 0.15s ease, color 0.15s ease;
    letter-spacing: 0.01em;
    flex-direction: row !important;
    box-sizing: border-box;
}
/* Hide the toggle on cards whose service has no add-ons. The
   --no-extras modifier is bound on the .so-bw-card class via
   Alpine's :class binding (which reliably re-renders inside
   x-for after serviceAddonsMap fills) — much more dependable
   than putting x-show / x-if on the button itself, which we
   tried previously and Alpine's reactivity didn't always
   re-evaluate. !important so per-template `display: flex
   !important` on .so-bw-card-extras-toggle doesn't win. */
#so-booking-widget .so-bw-card--no-extras .so-bw-card-extras-toggle {
    display: none !important;
}
.so-bw-card-extras-toggle:hover {
    background: var(--so-bw-primary-light, color-mix(in srgb, var(--so-bw-primary) 12%, transparent));
}
.so-bw-card-extras-toggle svg {
    flex-shrink: 0;
    transition: transform 0.2s ease;
}
.so-bw-card--has-extras-open .so-bw-card-extras-toggle svg {
    transform: rotate(45deg);
}
.so-bw-card-extras-count {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 18px;
    height: 18px;
    padding: 0 5px;
    background: var(--so-bw-primary);
    color: #fff;
    border-radius: 999px;
    font-size: 11px;
    font-weight: 700;
    margin-left: 4px;
}

/* Expanded add-on panel — list of selectable rows beneath the
   service card. Each row is a label so the entire row is a
   tap target for the checkbox. Selected state echoes the
   primary tint to make the running selection obvious. */
.so-bw-card-extras {
    display: flex;
    flex-direction: column;
    gap: 6px;
    padding: 10px 14px 14px;
    background: var(--so-bw-bg-secondary);
    border-top: 1px solid var(--so-bw-border);
}
.so-bw-card-extra-row {
    display: flex;
    align-items: center;
    /* An add-on row is name + duration + price. When the name is long enough
       that the three cannot share a line, the meta tail wraps under it rather
       than compressing the name to a few characters. */
    flex-wrap: wrap;
    gap: 10px;
    padding: 9px 12px;
    background: var(--so-bw-card-bg);
    border: 1px solid var(--so-bw-border);
    border-radius: var(--so-bw-radius);
    cursor: pointer;
    transition: border-color 0.15s ease, background 0.15s ease;
}
.so-bw-card-extra-row:hover {
    border-color: var(--so-bw-primary);
}
.so-bw-card-extra-row--selected {
    border-color: var(--so-bw-primary);
    background: var(--so-bw-primary-light, color-mix(in srgb, var(--so-bw-primary) 10%, transparent));
}
.so-bw-card-extra-check {
    width: 16px;
    height: 16px;
    accent-color: var(--so-bw-primary);
    flex-shrink: 0;
    margin: 0;
    cursor: pointer;
}
.so-bw-card-extra-name {
    flex: 1 1 auto;
    min-width: 0;
    font-size: 13px;
    font-weight: 600;
    color: var(--so-bw-text);
    /* Add-on names are admin-authored and routinely long ("Deep Tissue
       Massage — Extended 30 Minute Add-On"). The single-line ellipsis that
       used to live here hid the part that distinguishes one add-on from
       another, which is the only reason the row exists. Wrap instead;
       overflow-wrap: anywhere covers a name with no space in it at all. */
    white-space: normal;
    overflow-wrap: anywhere;
    word-break: break-word;
    line-height: 1.35;
}
.so-bw-card-extra-meta {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    flex-shrink: 0;
}
.so-bw-card-extra-duration {
    font-size: 11px;
    color: var(--so-bw-text-secondary);
    font-weight: 500;
}
.so-bw-card-extra-price {
    font-size: 13px;
    font-weight: 700;
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}

/* Mobile: tighten the add-on panel. Wrapping is no longer a phone-only
   concession — the base rule wraps at every width — so only the spacing
   and type scale change here. */
/* was @media (max-width: 480px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    #so-booking-widget:where([data-cw~="480"]) .so-bw-card-extras{
        padding: 8px 12px 12px;
    }
    #so-booking-widget:where([data-cw~="480"]) .so-bw-card-extra-row{
        padding: 8px 10px;
    }
    #so-booking-widget:where([data-cw~="480"]) .so-bw-card-extra-name{
        font-size: 12.5px;
    }


.so-bw-card--selected {
    border-color: var(--so-bw-primary);
    background: var(--so-bw-primary-light);
    box-shadow: 0 0 0 1px var(--so-bw-primary);
}

.so-bw-card--addon {
    align-items: center;
}

.so-bw-card-icon {
    flex-shrink: 0;
    flex-grow: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    /* Pinned to design-system avatar tokens with all four min/max
       bounds so flex can't squeeze it and inner content can't
       expand it. Mobile @media (section 12b) shrinks the token
       to 40px once for every template. */
    width: var(--so-bw-icon-size);
    height: var(--so-bw-icon-size);
    min-width: var(--so-bw-icon-size);
    min-height: var(--so-bw-icon-size);
    max-width: var(--so-bw-icon-size);
    max-height: var(--so-bw-icon-size);
    background: var(--so-bw-bg-tertiary);
    border-radius: var(--so-bw-radius);
    color: var(--so-bw-text-secondary);
}

/* Normalize the visual weight of the three avatar render modes so
   one card's photo and another card's initials don't read as
   different prominences. Photo already fills the circle via the
   .so-bw-card-img rule below; here we bump the SVG icon and
   initials so they occupy a comparable share of the 48px tile,
   matching the admin Staff card's 18px-on-44px proportion. */
.so-bw-card-icon--avatar > svg,
.so-bw-card-icon--avatar > span > svg {
    /* Cap the glyph at 58% of its tile so it can NEVER exceed the circular
       avatar (which has overflow:hidden) — several templates shrink the tile
       (.so-bw-card-icon to 24/28/32px) without shrinking this token, which
       left a 28px icon overflowing a 24px tile and getting clipped into a
       distorted shape. min() keeps the design-system token where the tile is
       big enough and falls back to a tile-relative size where it isn't. */
    width: min(var(--so-bw-icon-svg-size), 58%);
    height: min(var(--so-bw-icon-svg-size), 58%);
    flex-shrink: 0;
}
.so-bw-card-icon--avatar > span {
    font-size: var(--so-bw-icon-text-size);
    line-height: 1;
}

.so-bw-card--selected .so-bw-card-icon {
    background: rgba(var(--so-bw-primary-rgb), 0.15);
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}

/* Cards across location/service/staff steps use a circular tile to
   match the admin Staff and Customers pages; the default
   .so-bw-card-icon is a small rounded square which read as a "box"
   for headshots and place pins. The inner image fills the circle.

   Initials fallback (no avatar_url): the parent card-icon defaults
   to neutral gray tile + gray text, which read as a flat smudge on
   templates with a tinted body (Salon cream, Spa Mint, etc.). Tint
   the avatar tile with the brand primary so the initials carry the
   page's color identity and the tile feels integrated rather than
   bolted on. The selected-card override below still wins, and the
   image variant inherits a transparent background since the <img>
   covers the full tile. */
.so-bw-card-icon--avatar {
    border-radius: 50%;
    overflow: hidden;
    background: var(--so-bw-primary-light);
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
    font-weight: 700;
}
.so-bw-card-icon--avatar .so-bw-card-img {
    width: 100%;
    height: 100%;
    border-radius: 50%;
    object-fit: cover;
}

/* !! DEAD: neither class is rendered. The avatar <img> is built in
   booking-widget.js (_avatarImgHtml) with INLINE styles — width/height/
   object-fit/border-radius — and no class at all, so even after a rename a
   class rule could not win against the inline declaration. Fixing this means
   dropping the inline styles in the JS and letting .so-bw-card-icon img carry
   it; out of scope here. See the banner in section 13. */
.so-bw-card-img {
    width: 48px;
    height: 48px;
    object-fit: cover;
    border-radius: var(--so-bw-radius);
}

.so-bw-avatar {
    width: 48px;
    height: 48px;
    object-fit: cover;
    border-radius: 50%;
}

.so-bw-card-body {
    flex: 1;
    min-width: 0;
    /* In the row this took whatever was left beside the mark. In the stack the
       mark is above it, so it takes the card — without this it shrinks to its
       longest word and the centred text reads as a narrow ragged strip. */
    width: 100%;
}

.so-bw-card-title {
    font-size: var(--so-bw-card-title-size);
    font-weight: 600;
    color: var(--so-bw-text);
    margin-bottom: 2px;
    /* Prevent very long titles from overflowing their flex cell on
       narrow viewports. overflow-wrap + min-width: 0 on the parent
       .so-bw-card-body already allow break, but word-break: break-word
       is the last-resort guard for titles with no natural break point
       (single long word). overflow: hidden + text-overflow: ellipsis
       would swallow mid-title characters; wrap + hyphen is more
       readable. */
    overflow-wrap: break-word;
    word-break: break-word;
}

.so-bw-card-meta {
    font-size: var(--so-bw-card-meta-size);
    color: var(--so-bw-text-secondary);
    margin-bottom: 4px;
    /* Was a 2-line `-webkit-line-clamp` box (1 line on phones) with
       overflow: hidden + ellipsis. The card is not height-bounded — it sits
       in a grid of auto-height rows inside a scrolling body — so the clamp
       bought no layout stability and cost the customer the end of every
       service description. A three-service, one-of-them-verbose catalogue
       rendered as three identical "Full body treatment including…" stubs.
       The card grows to its text now; the badges below simply move down with
       it, which is what a content-adaptive card is supposed to do. */
    display: block;
    overflow: visible;
    white-space: normal;
    overflow-wrap: anywhere;
    word-break: break-word;
    line-height: 1.45;
}

/* The staff card's rating row — gold stars, the score, the review count —
   sitting between the identity lines and the bio.

   Mirrors the profile popup's `.so-bw-sp__rating` deliberately: the same fact
   should not have two visual languages depending on whether it is read on the
   card or in the modal that opens from it. Only the scale differs, because a
   card in a roster grid has far less room than a dialog header.

   `flex`, not the popup's `inline-flex`: here the rating is one row of the
   card body's vertical stack (role above, bio below), so it takes the full
   width and the bio starts underneath it rather than beside it. */
.so-bw-card-rating {
    display: flex;
    align-items: center;
    gap: 5px;
    margin-bottom: 4px;
    font-size: var(--so-bw-card-meta-size);
    font-weight: 600;
    line-height: 1.2;
    color: var(--so-bw-text-secondary);
}

/* The stars are aria-hidden decoration; the visually-hidden sentence rendered
   beside them carries the same information as text, so a screen reader hears
   "Rated 4.6 out of 5 from 12 reviews" rather than five unlabelled glyphs.
   `flex: none` so a narrow card never squeezes the row of five into
   four-and-a-half. */
.so-bw-card-stars {
    display: inline-flex;
    gap: 1px;
    flex: none;
    /* A rating star is gold in every design language, which is why the popup
       carries the same literal. It stays a var() fallback rather than a bare
       hex so a template defining --so-bw-warning still wins. */
    color: var(--so-bw-warning, #f59e0b);
}
.so-bw-card-stars svg { width: 12px; height: 12px; }

/* Tabular figures, so "4.0" and "4.6" occupy the same width and the counts
   beside them line up down a column of cards instead of jittering. */
.so-bw-card-score { font-family: var(--so-bw-mono); }

.so-bw-card-reviews {
    font-weight: 500;
    color: var(--so-bw-text-tertiary, var(--so-bw-text-secondary));
}

.so-bw-card-badges {
    display: flex;
    flex-wrap: wrap;
    /* Duration left, price right — the two facts at opposite ends of the row
       rather than clustered at one edge. `space-between` and not `margin-left:
       auto` on the price, because the row also renders a Free badge instead of
       a price, and on some templates an add-ons pill: whatever the row holds,
       the first and last item take the edges. Falls back to a plain gapped
       cluster the moment the row wraps, which is what wrapping is for. */
    justify-content: space-between;
    align-items: center;
    gap: 5px;
    margin-top: 8px;
}

.so-bw-card-check {
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    background: var(--so-bw-primary);
    color: #ffffff;
    border-radius: 50%;
    /* ── Out of flow, in the corner ────────────────────────────────────────
       It used to sit in the card's row, `align-self: center`, holding its 28px
       even while hidden so the body's width — and therefore the bio's line
       wrapping — was identical selected and unselected. That reasoning was
       right and the goal is unchanged; the card is a centred STACK now, and in
       a column that reserved box is a third row of 28px plus a gap at the
       bottom of every card, which is worse than the reflow it was avoiding.

       Absolute keeps the promise more completely than the reservation did: out
       of flow it cannot affect the body's width OR its height, so selecting a
       card moves nothing at all. `.so-bw-card` is already `position:
       relative`. Top-right is where a selection mark belongs on a stack — it
       is the one corner the centred content never reaches. */
    position: absolute;
    top: 10px;
    right: 10px;
    z-index: 2;
    visibility: hidden;
    transition: visibility 0s, opacity var(--so-bw-transition);
    opacity: 0;
}
.so-bw-card-check--visible {
    visibility: visible;
    opacity: 1;
}

.so-bw-card-checkbox {
    flex-shrink: 0;
    align-self: center;
}

/* Multi-service: numeric badge in the check circle showing the pick
   order (1, 2, 3, ...). Replaces the bare ✓ glyph once two or more
   services are ticked. Same 28px circle so the card layout doesn't
   reflow when switching between the two visual states. */
.so-bw-card-check-order {
    font-size: 13px;
    font-weight: 700;
    line-height: 1;
    color: #ffffff;
}

/* Multi-service running-total bar at the bottom of the service step. */
.so-bw-multi-summary {
    position: sticky;
    bottom: 0;
    margin-top: 16px;
    padding: 12px 16px;
    background: var(--so-bw-primary, #6366f1);
    color: #ffffff;
    border-radius: var(--so-bw-r-10);
    box-shadow: 0 4px 12px rgba(99, 102, 241, 0.18);
}
.so-bw-multi-summary-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 4px 12px;
    font-size: 14px;
    /* "4 services selected · 3 h 45 min" and the running total share this
       row. Both grow with the selection, so the row wraps rather than
       letting the total slide out of the sticky bar. */
    flex-wrap: wrap;
}
.so-bw-multi-summary-label {
    font-weight: 600;
    min-width: 0;
    overflow-wrap: anywhere;
}
.so-bw-multi-summary-meta {
    font-size: 13px;
    opacity: 0.95;
    min-width: 0;
    overflow-wrap: anywhere;
}

/* Multi-service list rows in the review summary + confirmation
   screen. One line per service with a small meta tail for duration. */
.so-bw-summary-svc-item {
    display: flex;
    align-items: baseline;
    /* One line per service, but a long service name plus its duration tail
       gets two — the review step is a recap and must show the whole name it
       is recapping. */
    flex-wrap: wrap;
    gap: 6px;
    font-size: 13px;
    line-height: 1.45;
    min-width: 0;
    overflow-wrap: anywhere;
}
.so-bw-summary-svc-meta {
    font-size: 12px;
    color: var(--so-bw-text-muted, #6b7280);
    white-space: normal;
}

/* ==========================================================================
   9. Checkbox
   ========================================================================== */

.so-bw-checkbox {
    width: 22px;
    height: 22px;
    border: 2px solid var(--so-bw-border);
    border-radius: var(--so-bw-radius-sm);
    display: flex;
    align-items: center;
    justify-content: center;
    transition: all var(--so-bw-transition);
    /* See .so-bw-input — clean white surface across every template
       so the unchecked state reads consistently. */
    background: var(--so-bw-card-bg);
}

.so-bw-checkbox--checked {
    border-color: var(--so-bw-primary);
    background: var(--so-bw-primary);
    color: #ffffff;
}

/* ==========================================================================
   10. Badges (Price, Duration, Status)
   ========================================================================== */

.so-bw-badge {
    display: inline-flex;
    align-items: center;
    /* justify-content so a wrapped two-line badge stays centred in its pill
       instead of ragging left of the radius. */
    justify-content: center;
    padding: 2px 8px;
    font-size: 12px;
    font-weight: 600;
    border-radius: 9999px;
    /* Badges hold a price, a duration or a status — all translated, and a
       status can be a phrase ("Warten auf Bestätigung"). `nowrap` made the
       pill wider than its card and pushed the row sideways. It only ever
       wraps when it genuinely cannot fit, so short badges are unchanged. */
    white-space: normal;
    overflow-wrap: anywhere;
    text-align: center;
    line-height: 1.35;
    max-width: 100%;
}

/* ── THE FACT PILLS ──────────────────────────────────────────────────────
   A card carries two facts, and they are not the same KIND of fact: the
   duration is something you check, the price is something you decide on. Two
   identical filled chips said they were equal. So the duration is outlined and
   quiet, the price stays filled in the accent tint, and the pair sits at
   opposite ends of the card's bottom row.

   Both were built for Tabbed Rail first and are stated HERE now, on the base
   primitives, because a card's facts are not one architecture's idea — every
   template renders this same markup.

   BASE, deliberately, not `[data-template]`: eleven live architectures
   restyle these two badges on purpose (agenda, bundle, dusk, editorial, frost,
   inline, soft, timeline, weekboard, wide, workspace). At base specificity
   this becomes the default for the twelve with no opinion and leaves those
   eleven exactly as they were designed. Raising the specificity to reach them
   would be overriding a deliberate design with a default, which is backwards. */
/* The pills' geometry, scoped to the card's badge row so the status pill and
   the add-on badges that share these modifiers keep their own. Stated AFTER
   `.so-bw-badge` deliberately: `.so-bw-card-badges .so-bw-badge {` contains the
   base selector as a substring, so a reader — and the cascade suite, which
   looks rules up by their opening text — finds whichever comes first.

   No `white-space: nowrap`: the base badge allows wrapping on purpose, because
   a translated duration or a long currency string made the pill wider than its
   card and pushed the whole row sideways. */
/* The ID is in the selector for a reason: `#so-booking-widget .so-bw-badge`
   restates the badge's spacing above the reset (search: "components were being
   silently discarded") at (1,1,0), which outranks a plain
   `.so-bw-card-badges .so-bw-badge` at (0,2,0) no matter where it sits in the
   file. MEASURED before this: `min-height` applied and `padding` silently did
   not, which is the tell — one declaration landing and its neighbour not. */
#so-booking-widget .so-bw-card-badges .so-bw-badge {
    min-height: 30px;
    /* 9px, not 12. MEASURED on a five-up grid: a 188px card leaves 164px of
       content box, and at 12px of pill padding the duration and the price came
       to ~174px — ten pixels over, so the row wrapped and the pill strip went
       from 30px tall to 74px. That wrap was the single largest thing in the
       card's height, worth more than the cover band.

       The row still allows wrapping as a last resort (the base badge does, for
       a translated duration or a long currency string). This just stops it
       being reached at the width the grid actually produces: MEASURED at 9px
       the pair still came to 164px + a 6px gap against a 164px box — over by
       six — so it is 7px here and the row's gap is 5px, which lands the pair
       at ~156px and leaves the wrap for the cases it was written for. */
    padding: 0 7px;
}

.so-bw-badge--duration {
    background: var(--so-bw-card-bg);
    border: 1px solid var(--so-bw-border);
    color: var(--so-bw-text-secondary);
    font-weight: 500;
}
/* …and inside a CARD it loses the chrome and keeps only the glyph and the text.
   MEASURED, five-up at 188px: as two pills the pair came to 176px against a
   164px content box, so the row wrapped to two lines and the strip went 38px →
   73px — 35px of card height, on every card, to draw a box around a duration.
   "1h 15min" beside "₹1,000.00" simply does not fit as two pills at this width,
   and it is the width the five-across grid produces.

   The hierarchy the two pills existed to state survives, and arguably reads
   better: the price is the only filled thing on the card, and the duration is
   what it always was — a fact you check, in the secondary ink, with a clock in
   front of it. This is a deliberate step away from the comp, which draws both
   as pills at a card width where both as pills do not fit. */
#so-booking-widget .so-bw-card-badges .so-bw-badge--duration {
    padding-left: 0;
    padding-right: 0;
    border: 0;
    background: none;
}

.so-bw-badge--price {
    background: var(--so-bw-primary-light);
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}

/* The clock is a MASK, not markup. The badge span holds a formatted duration
   and nothing else, so an icon would mean a new element in shared markup and
   an empty one in every badge that is not a duration. `currentColor` also
   means it tracks the pill's own text colour — including on the eleven
   templates above, which restyle the pill and get a glyph that still matches.

   Scoped to the card's badge row: the same modifier is used in the add-ons
   list and the summary, where a row of glyphs would be noise. */
.so-bw-card-badges .so-bw-badge--duration::before {
    content: '';
    flex: none;
    width: 13px;
    height: 13px;
    margin-right: 5px;
    background: currentColor;
    -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round'%3E%3Ccircle cx='12' cy='12' r='9'/%3E%3Cpath d='M12 7v5l3.5 2'/%3E%3C/svg%3E");
            mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round'%3E%3Ccircle cx='12' cy='12' r='9'/%3E%3Cpath d='M12 7v5l3.5 2'/%3E%3C/svg%3E");
    -webkit-mask-position: center;
            mask-position: center;
    -webkit-mask-repeat: no-repeat;
            mask-repeat: no-repeat;
    -webkit-mask-size: contain;
            mask-size: contain;
}

.so-bw-badge--free {
    background: var(--so-bw-success-light);
    color: var(--so-bw-success);
}

.so-bw-badge--status {
    background: var(--so-bw-primary-light);
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
    text-transform: capitalize;
}

/* ==========================================================================
   11. Service Groups
   ========================================================================== */

.so-bw-service-group {
    margin-bottom: 20px;
}

.so-bw-service-group:last-child {
    margin-bottom: 0;
}

.so-bw-group-title {
    font-size: 13px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--so-bw-text-secondary);
    margin-bottom: 10px;
    padding-bottom: 6px;
    border-bottom: 1px solid var(--so-bw-border);
}

/* ==========================================================================
   12. Calendar
   ========================================================================== */

/* Date & Time pane — side-by-side calendar + slot list. Mirrors
   the reference design: bordered table-style calendar on the left,
   "Time Slot" panel grouped by Morning / Afternoon / Evening on
   the right. Stacks vertically on narrow viewports so phones get
   one column. */
.so-bw-datetime {
    display: grid;
    /* auto-fit on the ACTUAL available width (not the viewport): the calendar
       needs ~300px to render all 7 weekday columns at a tappable size, so we
       only place a second column (the slot grid) beside it when there's room
       for both. In a narrow content column — two-pane templates (256px rail),
       a small widget size, popups — only one column fits, so the calendar
       takes the full width (all 7 days) and the slot grid stacks beneath it.
       This replaces the old fixed 2-col grid that squeezed the calendar to
       ~150px (clipping it to 4 days) and cramped the slot pills whenever the
       column was narrow, even on a wide desktop window. */
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr));
    gap: 16px;
    align-items: start;
    margin-bottom: 16px;
}
/* Stack at <= 600px (was 720px). The 500-560px range is common for
   Android mid-range and the 720px stack threshold made calendars
   and slot panels both ~240px wide with no usable tap targets.
   At 600px the side-by-side gives ~290px each, still tight but
   workable; below that the single column is clearly the right call. */
/* was @media (max-width: 600px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    #so-booking-widget:where([data-cw~="600"]) .so-bw-datetime{
        grid-template-columns: 1fr;
        gap: 12px;
    }
    /* Slot panel is now full-width — let auto-fit pack as many compact
       columns as fit (no overflow / clipping). */
    #so-booking-widget:where([data-cw~="600"]) .so-bw-slot-list{
        grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
    }


/* Two-pane templates (minimal / dark / studio) render the flow in a narrow
   right-hand column — the 256px summary rail leaves only ~370px for content.
   The side-by-side calendar + slot grid then squeezed the 7-day calendar
   below its usable width, so only 4 of the 7 weekday columns showed and the
   date + time read as two disconnected panels. The viewport @media above
   can't catch this (the WINDOW is wide; only the COLUMN is narrow), so stack
   them here: a full-width calendar (all 7 days) ABOVE a full-width slot grid,
   both on the same step. */
#so-booking-widget[data-template="minimal"] .so-bw-datetime,
#so-booking-widget[data-template="dark"] .so-bw-datetime,
#so-booking-widget[data-template="studio"] .so-bw-datetime {
    grid-template-columns: 1fr;
    gap: 14px;
}
#so-booking-widget[data-template="minimal"] .so-bw-slot-list,
#so-booking-widget[data-template="dark"] .so-bw-slot-list,
#so-booking-widget[data-template="studio"] .so-bw-slot-list {
    grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
}
/* Stacked below the calendar, the slot panel should flow naturally and let
   the card body be the single scroll region — its own 520px inner scroll
   would otherwise nest a second scrollbar inside the fixed-height card. */
#so-booking-widget[data-template="minimal"] .so-bw-slot-panel,
#so-booking-widget[data-template="dark"] .so-bw-slot-panel,
#so-booking-widget[data-template="studio"] .so-bw-slot-panel {
    max-height: none;
    overflow: visible;
}

/* Calendar shell — bordered card, compact internal padding. The
   smaller padding (8-12px instead of the previous 10-16px) lets
   the side-by-side layout fit cleanly without the calendar
   dominating the row. */
.so-bw-calendar {
    width: 100%;
    margin: 0;
    padding: clamp(8px, calc(var(--so-bw-cw, 100vw) * 0.01), 12px);
    background: var(--so-bw-card-bg);
    border: 1px solid var(--so-bw-border);
    border-radius: var(--so-bw-radius-lg);
    box-sizing: border-box;
}

/* Scrollable wrapper around the weekday header + day grid. When
   the parent container is narrower than the day cells need
   (very tall calendar widgets, narrow phones, side-by-side
   layouts), this lets the customer scroll horizontally to reach
   the full grid instead of seeing it overflow or squish. The
   grid keeps a 280px floor so the 7 columns always render at a
   tap-friendly size; on any container wider than that the grid
   naturally fills 100% and no scroll appears. */
.so-bw-calendar-scroll {
    overflow-x: auto;
    overflow-y: hidden;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: thin;
    scrollbar-color: var(--so-bw-border) transparent;
}
.so-bw-calendar-scroll::-webkit-scrollbar {
    height: 6px;
}
.so-bw-calendar-scroll::-webkit-scrollbar-thumb {
    background: var(--so-bw-border);
    border-radius: 999px;
}
.so-bw-calendar-scroll::-webkit-scrollbar-track {
    background: transparent;
}
.so-bw-calendar-scroll-inner {
    /* 7-day grid floor. Lowered 280 -> 248: after the calendar's ~24px inner
       padding, a ~300px datetime column leaves ~277px for the grid, so a
       280px floor overflowed by a few px and clipped the Sunday column on
       the narrower templates (gradient / bubble). 248px keeps the day cells
       ~35px (still comfortably tappable) and fits every column width. */
    min-width: 248px;
}

.so-bw-calendar-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    margin-bottom: clamp(6px, calc(var(--so-bw-cw, 100vw) * 0.008), 12px);
}

.so-bw-calendar-month {
    font-size: clamp(13px, calc(var(--so-bw-cw, 100vw) * 0.012), 15px);
    font-weight: 700;
    color: var(--so-bw-text);
    letter-spacing: -0.01em;
    flex: 1;
    text-align: center;
    min-width: 0;
}

.so-bw-calendar-nav {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: var(--so-bw-calendar-nav-size);
    height: var(--so-bw-calendar-nav-size);
    background: transparent;
    border: 1px solid transparent;
    border-radius: var(--so-bw-calendar-nav-radius);
    cursor: pointer;
    color: var(--so-bw-text-secondary);
    transition: all var(--so-bw-transition);
    flex-shrink: 0;
}
.so-bw-calendar-nav svg {
    width: 16px;
    height: 16px;
}

.so-bw-calendar-nav:hover:not(:disabled) {
    background: var(--so-bw-bg-secondary);
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}

.so-bw-calendar-nav:disabled {
    opacity: 0.25;
    cursor: not-allowed;
}

/* Bordered table-style grid — every cell shares an edge with its
   neighbours so the calendar reads as a clean grid like the
   reference design. The outer border on the wrapper plus the
   inset cell borders use the same colour for visual continuity. */
.so-bw-calendar-weekdays {
    display: grid;
    grid-template-columns: repeat(7, 1fr);
    border: 1px solid var(--so-bw-border);
    border-bottom: none;
    border-top-left-radius: var(--so-bw-r-6);
    border-top-right-radius: var(--so-bw-r-6);
    overflow: hidden;
}

.so-bw-calendar-weekdays span {
    text-align: center;
    font-size: clamp(10px, calc(var(--so-bw-cw, 100vw) * 0.0085), 12px);
    font-weight: 600;
    color: var(--so-bw-text-secondary);
    padding: clamp(6px, calc(var(--so-bw-cw, 100vw) * 0.006999999999999999), 9px) 0;
    border-right: 1px solid var(--so-bw-border);
    background: var(--so-bw-card-bg);
}
.so-bw-calendar-weekdays span:last-child {
    border-right: none;
}

.so-bw-calendar-grid {
    display: grid;
    grid-template-columns: repeat(7, 1fr);
    gap: 0;
    border: 1px solid var(--so-bw-border);
    border-bottom-left-radius: var(--so-bw-r-6);
    border-bottom-right-radius: var(--so-bw-r-6);
    overflow: hidden;
}

/* Explanation shown when every cell in the grid is disabled. A dead calendar
   with no message reads as a broken page rather than a closed service — see
   `noOpenDaysNotice`. Deliberately quiet (no error red): "nothing this month"
   is ordinary, not a fault. */
.so-bw-calendar-empty {
    margin: 12px 0 0;
    padding: 10px 12px;
    border: 1px solid var(--so-bw-border);
    border-radius: var(--so-bw-r-6);
    background: var(--so-bw-surface-alt, rgba(0, 0, 0, 0.03));
    font-size: 13px;
    line-height: 1.5;
    color: var(--so-bw-text-secondary, rgba(0, 0, 0, 0.62));
    text-align: center;
}

/* The weekday restriction, stated on the calendar itself (see openDaysHint).
   Deliberately quieter than .so-bw-calendar-empty above it: that one explains
   a dead month and has to be read, this one explains a mostly-grey one and
   only has to be findable. No border or fill, so it reads as a caption on the
   grid rather than as a second alert competing with it. */
.so-bw-calendar-hint {
    /* No top margin: .so-bw-calendar-header already carries 10px beneath it.
       The 10px BELOW is the load-bearing half — without it the line sat flush
       against the weekday row (measured: hint bottom 586, weekdays top 586)
       and read as a caption stuck to the column headings rather than as a
       statement about the month. */
    margin: 0 0 10px;
    font-size: 12px;
    line-height: 1.5;
    color: var(--so-bw-text-secondary, rgba(0, 0, 0, 0.62));
    text-align: center;
}

.so-bw-calendar-day {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    aspect-ratio: 1;
    min-height: 36px;
    border: none;
    border-right: 1px solid var(--so-bw-border);
    border-bottom: 1px solid var(--so-bw-border);
    background: var(--so-bw-card-bg);
    font-family: var(--so-bw-font);
    font-size: clamp(12px, calc(var(--so-bw-cw, 100vw) * 0.0095), 14px);
    font-weight: 500;
    color: var(--so-bw-text);
    border-radius: 0;
    cursor: pointer;
    transition: background 0.15s ease, color 0.15s ease;
    position: relative;
    line-height: 1;
    padding: 0;
}

/* The day cell's two spans. A month GRID shows only the numeral — its seven
   columns are labelled once by .so-bw-calendar-weekdays, so a weekday in every
   cell would be six redundant "Mon"s down the first column.
   A RAIL layout drops that column header entirely and reveals
   .so-bw-calendar-day-dow instead, which is the only place the weekday is
   then legible. Conversation was the one such layout and is withdrawn, so
   nothing reveals it today — the span is still emitted for every template
   and still hidden here, which is the same contract .so-bw-week-wrap and
   .so-bw-agenda-list follow, and is what a future rail would opt into. */
.so-bw-calendar-day-dow {
    display: none;
}
.so-bw-calendar-day-num {
    display: block;
    line-height: 1;
}

/* Strip the right border on every 7th cell + the bottom border on
   the last row so the grid wraps the cells with one continuous
   outer line instead of doubled borders at the edges. */
.so-bw-calendar-day:nth-child(7n) {
    border-right: none;
}
.so-bw-calendar-day:nth-last-child(-n+7) {
    border-bottom: none;
}

.so-bw-calendar-day--other-month {
    color: var(--so-bw-text-tertiary);
    opacity: 0.55;
    cursor: default;
    pointer-events: none;
    background: var(--so-bw-bg-secondary);
}

.so-bw-calendar-day--disabled {
    color: var(--so-bw-text-tertiary);
    opacity: 0.42;
    cursor: not-allowed;
    pointer-events: none;
    background: var(--so-bw-bg-secondary);
}

/* Business holiday — disabled like other unavailable dates but with a
   distinct rose tint + strikethrough so it reads as "closed" rather
   than merely "no slots". Sits after --disabled so it wins the cascade
   when both classes are present. */
.so-bw-calendar-day--holiday {
    color: #e11d48;
    background: #fff1f2;
    text-decoration: line-through;
    opacity: 0.85;
}

.so-bw-calendar-day--available:hover {
    background: var(--so-bw-primary-light);
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}

.so-bw-calendar-day--today {
    font-weight: 700;
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}
.so-bw-calendar-day--today::after {
    content: '';
    position: absolute;
    bottom: 5px;
    left: 50%;
    transform: translateX(-50%);
    width: 4px;
    height: 4px;
    background: var(--so-bw-primary);
    border-radius: 50%;
}

.so-bw-calendar-day--selected,
.so-bw-calendar-day--selected:hover {
    background: var(--so-bw-primary) !important;
    color: #ffffff !important;
    font-weight: 700;
}

.so-bw-calendar-day--selected::after {
    display: none;
}

/* Time-slot panel (right column on the date-time pane). Each
   slot row is a full-width pill labelled "9:00 AM to 9:45 AM",
   grouped under a "Morning / Afternoon / Evening" caption. The
   panel itself scrolls vertically (not the individual period
   lists) so a customer scrolling through "Morning → Afternoon
   → Evening" stays in one continuous list. The scrollbar is
   always visible (overflow-y: scroll, not auto) and brand-tinted
   so customers notice they can scroll further. */
/* "← Change date" link at the top of the time-slot page (the date and time
   now render as two separate pages within the Date & Time step). */
.so-bw-slot-back {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    background: none;
    border: none;
    cursor: pointer;
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
    font-weight: 600;
    font-size: 13px;
    line-height: 1;
    padding: 2px 0;
    margin-bottom: 12px;
}
.so-bw-slot-back:hover { opacity: 0.78; }
.so-bw-slot-back svg { flex: none; }

.so-bw-slot-panel {
    background: var(--so-bw-card-bg);
    border: 1px solid var(--so-bw-border);
    border-radius: var(--so-bw-radius-lg);
    padding: clamp(10px, calc(var(--so-bw-cw, 100vw) * 0.012), 14px) clamp(12px, calc(var(--so-bw-cw, 100vw) * 0.013999999999999999), 16px);
    box-sizing: border-box;
    min-height: 200px;
    /* No inner max-height / scroll: the panel flows at its natural height and
       the card body provides the single scroll region. Its own 520px inner
       scroll nested a second scrollbar inside the fixed-height card, and —
       now the date-time grid stacks the panel BELOW the calendar in a narrow
       column — a nested scroll there read as a broken, cut-off slot list. */
    display: flex;
    flex-direction: column;
    gap: 10px;
    overscroll-behavior: contain;
    scrollbar-width: thin;
    scrollbar-color: var(--so-bw-primary) var(--so-bw-bg-secondary);
}
.so-bw-slot-panel::-webkit-scrollbar {
    width: 8px;
}
.so-bw-slot-panel::-webkit-scrollbar-track {
    background: var(--so-bw-bg-secondary);
    border-radius: 999px;
    margin: 8px 0;
}
.so-bw-slot-panel::-webkit-scrollbar-thumb {
    background: var(--so-bw-primary);
    border-radius: 999px;
    border: 2px solid var(--so-bw-bg-secondary);
}
.so-bw-slot-panel::-webkit-scrollbar-thumb:hover {
    background: var(--so-bw-primary-hover, var(--so-bw-primary));
}

.so-bw-slot-panel-title {
    margin: 0 0 4px;
    font-size: 16px;
    font-weight: 700;
    color: var(--so-bw-text);
    letter-spacing: -0.01em;
    background: var(--so-bw-card-bg);
    padding-bottom: 4px;
}
.so-bw-slot-panel-titles {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
    flex: 1 1 auto;
}
/* ID-QUALIFIED, and font-size !important, because this one is a <p> sitting
   inside .entry-content on a real WordPress page.

   Measured on a live install: it rendered at 18px/300 rather than 11px, so
   "Staff time zone: GMT+5:30" came out the same size as the "Time Slot"
   heading beside it and stretched the whole panel header.

   TWO different causes, found by inspecting which rules actually matched:

     • font-weight came from ordinary theme paragraph styling, which a class
       selector at (0,1,0) loses to. The id fixes that on its own.
     • font-size came from ANOTHER PLUGIN — wawp-otp-frontend.css ships
       `font-size: 18px !important` on a compound `.entry-content…` selector
       that reaches descendants. !important ignores specificity, so the id
       alone did not help; only !important here wins it back.

   Fighting another plugin's !important with our own is not something to do
   casually, and it is narrowly scoped for that reason: one property, one
   element, inside our own id.

   Note the local test fixture has no theme and no other plugins, so this
   entire class of bug is invisible there — it only appears on a real
   install. Other widget paragraphs still carrying class-only type rules
   (.so-bw-policy-panel-error, .so-bw-recurring-title,
   .so-bw-recurring-preview-title, .so-bw-policy-panel-value--saving) have
   the same exposure and should get the same treatment when next touched. */
#so-booking-widget .so-bw-slot-panel-subtitle {
    margin: 0;
    display: inline-flex;
    align-items: center;
    gap: 4px;
    font-size: 11px !important;
    font-weight: 400;
    line-height: 1.3;
    color: var(--so-bw-text-tertiary, var(--so-bw-text-secondary));
    cursor: help;
}
.so-bw-slot-panel-subtitle svg {
    flex-shrink: 0;
    opacity: 0.7;
}
.so-bw-slot-panel-subtitle strong {
    color: var(--so-bw-text);
    font-weight: 600;
}
/* Sticky header row that keeps the title + timezone picker pinned
   while the slot list scrolls below it. On wide layouts the title
   sits on the left and the tz picker on the right; on narrow screens
   they stack vertically so the dropdown gets full width and the
   IANA label has room to breathe. */
.so-bw-slot-panel-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    flex-wrap: wrap;
    position: sticky;
    top: calc(-1 * clamp(10px, calc(var(--so-bw-cw, 100vw) * 0.012), 14px));
    margin: calc(-1 * clamp(10px, calc(var(--so-bw-cw, 100vw) * 0.012), 14px)) calc(-1 * clamp(12px, calc(var(--so-bw-cw, 100vw) * 0.013999999999999999), 16px)) 0;
    padding: clamp(10px, calc(var(--so-bw-cw, 100vw) * 0.012), 14px) clamp(12px, calc(var(--so-bw-cw, 100vw) * 0.013999999999999999), 16px) 8px;
    /* NO z-index here, and it is load-bearing that there is none.
       This head is a FLEX ITEM of .so-bw-slot-panel, and a flex item honours
       z-index even while `position: static` (CSS Flexbox §5.4) — so the 2 that
       used to sit here was live on every template, including the ones that
       deliberately un-stick the head further down this file. On Agenda it tied
       .so-bw-slot-back (sticky, z-index: 2) and won on tree order, burying the
       "Back to days" button under the head's own title: measured 36px of
       overlap — the button's entire height — at 768px and up, once the slot
       list is scrolled at all, with elementsFromPoint returning the head.
       An unclickable exit from the day view.

       Nothing is lost by dropping it. The head is opaque and, where it is
       still sticky, positioned — so the slot rows underneath it are in-flow
       and paint below it regardless of rank. The only thing the 2 ever
       decided was a fight with other POSITIONED chrome, which it should not
       win. When the timezone menu is open the head is lifted deliberately and
       explicitly by .so-bw-slot-panel-head--menu (z-index: 40, section 17.9),
       which still applies for the same flex-item reason. */
    background: var(--so-bw-card-bg);
    border-bottom: 1px solid var(--so-bw-border, rgba(0,0,0,0.06));
}
.so-bw-tz-picker {
    /* Label stacked ABOVE the combobox so the panel can expand in-flow
       below the trigger without the label drifting (it would float to
       the vertical centre if the two were laid out in a centred row). */
    display: inline-flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 4px;
    /* Font size tracks the select token so label + control are always
       optically matched. Secondary colour keeps it subordinate to the
       "Time Slot" heading beside it. */
    font-size: var(--so-bw-tz-select-font-size);
    color: var(--so-bw-text-secondary, var(--so-bw-text));
    flex-shrink: 0;
    max-width: 100%;
}
.so-bw-tz-picker-label {
    /* "Your local time" is translated, and its picker is capped at 240px —
       nowrap made the label, not the control, decide the panel's minimum
       width. It stacks above the combobox, so wrapping costs nothing. */
    white-space: normal;
    overflow-wrap: anywhere;
    max-width: 100%;
    font-weight: 500;
}
/* ── "Your local time" timezone combobox ──────────────────────────────
   A custom searchable dropdown — the search bar lives INSIDE the opened
   panel (matching the admin AppCombobox / TimezoneSelect pattern). The
   panel expands IN-FLOW directly below the trigger rather than floating:
   the slot panel it sits in is overflow:scroll (would clip an absolutely
   positioned menu) and teleporting out of #so-booking-widget would sever
   the CSS-variable theming. Every surface consumes the shared input/card
   tokens, so all 30 templates (incl. dark) theme it for free; the trigger
   matches the template's input radius while the floating-style panel keeps
   a fixed radius so pill-radius templates don't round the menu into a
   lozenge. */
.so-bw-tz-combo {
    position: relative;
    display: inline-flex;
    flex-direction: column;
    min-width: 0;
    max-width: 240px;
    width: 100%;
}
/* NOTE: the combobox controls that set padding/margin are prefixed with
   #so-booking-widget so they out-specify the `#so-booking-widget * {
   padding:0; margin:0 }` reset (specificity 1,0,0). A bare class (0,1,0)
   loses to that reset — which is exactly why an unprefixed rule left the
   search input with padding:0 and the magnifier overlapping the
   placeholder. (Template button rules win the same way.) */
#so-booking-widget .so-bw-tz-combo-trigger {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    width: 100%;
    box-sizing: border-box;
    font-family: var(--so-bw-font);
    font-size: var(--so-bw-tz-select-font-size);
    line-height: 1.5;
    text-align: left;
    padding: var(--so-bw-tz-select-pad-y) var(--so-bw-tz-select-pad-x);
    border: 1px solid var(--so-bw-border);
    border-radius: var(--so-bw-input-radius);
    background-color: var(--so-bw-card-bg);
    color: var(--so-bw-text);
    cursor: pointer;
    transition: border-color var(--so-bw-transition), box-shadow var(--so-bw-transition);
}
.so-bw-tz-combo-trigger:hover {
    border-color: var(--so-bw-primary);
}
.so-bw-tz-combo--open .so-bw-tz-combo-trigger,
.so-bw-tz-combo-trigger:focus-visible {
    outline: none;
    border-color: var(--so-bw-primary);
    box-shadow: 0 0 0 3px rgba(var(--so-bw-primary-rgb), 0.15);
}
.so-bw-tz-combo-value {
    /* An IANA zone label is the whole point of the control ("America/
       Argentina/Buenos_Aires (GMT−3)"). Truncated to "America/Argentina/…"
       two zones read identically, so the trigger grows to two lines instead.
       The trigger is align-items: center, so a wrapped value stays centred
       against the chevron. */
    white-space: normal;
    overflow-wrap: anywhere;
    min-width: 0;
    text-align: left;
    line-height: 1.35;
}
.so-bw-tz-combo-chevron {
    flex-shrink: 0;
    color: var(--so-bw-text-secondary, var(--so-bw-text));
    transition: transform var(--so-bw-transition);
}
.so-bw-tz-combo--open .so-bw-tz-combo-chevron {
    transform: rotate(180deg);
}
#so-booking-widget .so-bw-tz-combo-panel {
    margin-top: 4px;
    border: 1px solid var(--so-bw-border);
    border-radius: var(--so-bw-r-10);
    background-color: var(--so-bw-card-bg);
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.14);
    overflow: hidden;
    /* No z-index. This block does not position the panel — the rule that does
       (section 17.9) also ranks it, at 30, which is what has to clear the
       commit bar's 6. A `z-index: 5` sat here inertly for a long time and read
       as "the menu is below the nav", which is both wrong and the opposite of
       what the panel needs. */
}
#so-booking-widget .so-bw-tz-combo-search-wrap {
    position: relative;
    padding: 8px 8px 6px;
}
.so-bw-tz-combo-search-icon {
    position: absolute;
    left: 18px;
    top: 50%;
    transform: translateY(-50%);
    color: var(--so-bw-text-secondary, var(--so-bw-text));
    pointer-events: none;
}
#so-booking-widget .so-bw-tz-combo-search {
    width: 100%;
    box-sizing: border-box;
    font-family: var(--so-bw-font);
    font-size: var(--so-bw-tz-select-font-size);
    line-height: 1.5;
    padding: var(--so-bw-tz-select-pad-y) var(--so-bw-tz-select-pad-x) var(--so-bw-tz-select-pad-y) 30px;
    border: 1px solid var(--so-bw-border);
    border-radius: var(--so-bw-input-radius);
    background-color: var(--so-bw-card-bg);
    color: var(--so-bw-text);
    transition: border-color var(--so-bw-transition), box-shadow var(--so-bw-transition);
}
.so-bw-tz-combo-search::placeholder {
    color: var(--so-bw-text-secondary, var(--so-bw-text));
    opacity: 0.7;
}
.so-bw-tz-combo-search:focus,
.so-bw-tz-combo-search:focus-visible {
    outline: none;
    border-color: var(--so-bw-primary);
    box-shadow: 0 0 0 3px rgba(var(--so-bw-primary-rgb), 0.15);
}
#so-booking-widget .so-bw-tz-combo-list {
    list-style: none;
    margin: 0;
    padding: 4px;
    max-height: 200px;
    overflow-y: auto;
}
#so-booking-widget .so-bw-tz-combo-option {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    width: 100%;
    box-sizing: border-box;
    text-align: left;
    font-family: var(--so-bw-font);
    font-size: var(--so-bw-tz-select-font-size);
    line-height: 1.4;
    padding: 7px 10px;
    border: none;
    border-radius: var(--so-bw-r-6);
    background: transparent;
    color: var(--so-bw-text);
    cursor: pointer;
    transition: background-color var(--so-bw-transition);
}
.so-bw-tz-combo-option:hover {
    background-color: rgba(var(--so-bw-primary-rgb), 0.08);
}
.so-bw-tz-combo-option--selected {
    background-color: rgba(var(--so-bw-primary-rgb), 0.12);
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
    font-weight: 600;
}
.so-bw-tz-combo-option > span {
    /* Same reasoning as the trigger's value: the tail of a zone name is what
       distinguishes it. The list is already a 200px scroll region, so a
       two-line option costs a little scroll, not a lost identity. */
    white-space: normal;
    overflow-wrap: anywhere;
    min-width: 0;
}
.so-bw-tz-combo-check {
    flex-shrink: 0;
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}
#so-booking-widget .so-bw-tz-combo-empty {
    padding: 16px 10px;
    text-align: center;
    font-size: var(--so-bw-tz-select-font-size);
    color: var(--so-bw-text-secondary, var(--so-bw-text));
}
.so-bw-tz-hint {
    font-size: 12px;
    color: var(--so-bw-text-tertiary, var(--so-bw-text-secondary));
    margin: 2px 0 6px;
    display: inline-flex;
    align-items: center;
    gap: 4px;
}
.so-bw-tz-hint::before {
    /* Small clock glyph as inline SVG mask so it inherits color and
       doesn't add another DOM node. Decorative — aria-hidden by the
       fact that ::before isn't readable to screen readers. */
    content: '';
    display: inline-block;
    width: 12px; height: 12px;
    background-color: currentColor;
    -webkit-mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><circle cx='12' cy='12' r='9'/><polyline points='12 7 12 12 15 14'/></svg>");
            mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><circle cx='12' cy='12' r='9'/><polyline points='12 7 12 12 15 14'/></svg>");
    -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
    -webkit-mask-size: contain; mask-size: contain;
    flex-shrink: 0;
}
.so-bw-tz-hint strong {
    color: var(--so-bw-text);
    font-weight: 600;
}
/* was @media (max-width: 520px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    #so-booking-widget:where([data-cw~="520"]) .so-bw-slot-panel-head{
        flex-direction: column;
        align-items: stretch;
    }
    #so-booking-widget:where([data-cw~="520"]) .so-bw-tz-picker{
        width: 100%;
        justify-content: space-between;
        /* Mobile: bump token to 16px so iOS doesn't auto-zoom the
           combobox trigger / search input on focus. Same guard applied
           to --so-bw-input-font-size in the section-22 mobile block. */
        --so-bw-tz-select-font-size: 16px;
    }
    #so-booking-widget:where([data-cw~="520"]) .so-bw-tz-combo{
        max-width: none;
        flex: 1 1 auto;
    }

.so-bw-slot-panel-empty {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 6px;
    padding: 20px 8px;
    color: var(--so-bw-text-tertiary);
    text-align: center;
    flex: 1;
}
.so-bw-slot-panel-empty p {
    margin: 0;
    font-size: 13px;
    line-height: 1.5;
}
.so-bw-slot-group {
    display: flex;
    flex-direction: column;
    gap: var(--so-bw-slot-gap, 6px);
}
.so-bw-slot-group-title {
    font-size: 13px;
    font-weight: 600;
    color: var(--so-bw-text);
    margin-bottom: 2px;
}
/* Each period's list renders as a uniform 2-column grid so slots
   sit in tidy rows rather than single stacked full-width pills.
   repeat(2, 1fr) keeps both columns equal regardless of text
   length, matching the look of the time-grid in wider views.
   The OUTER .so-bw-slot-panel still carries scroll, so individual
   groups grow freely without clipping. */
.so-bw-slot-list {
    display: grid;
    /* Responsive columns instead of a hard 2-up grid. Two fixed 1fr columns
       of non-wrapping "9:00 AM to 9:10 AM" pills couldn't fit the narrow
       side-by-side slot panel, so the second column overflowed / was clipped
       and forced a horizontal scrollbar — making those slots unreachable.
       auto-fit drops to fewer columns when there isn't room, so slots always
       fit and stay clickable.

       KNOWN OPEN — uneven slot widths. auto-fit collapses EMPTY tracks to zero
       and lets the survivors stretch, so a daypart holding one opening renders
       a single full-width chip beside a daypart whose two chips are half that.
       Confirmed on Hero and Frost at 760px (measured: 588px vs 141px in the
       same panel). auto-fill is the one-word fix and it does work — but it also
       makes EVERY chip uniform and narrower (~143px here), which changes the
       common two-up case on all nineteen templates. That is a visual decision,
       not a bug fix, so it is deliberately NOT taken here: the per-template
       overrides at [data-template="hero"|"frost"|"weekboard"] carry their own
       auto-fit and would each need re-judging at real widths first. */
    grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
    gap: var(--so-bw-slot-gap, 6px);
}

/* Slot row pill — hardened with #so-booking-widget prefix and
   visibility:visible / opacity:1 because some per-template
   blocks attach `display: none` / opacity rules to BUTTON
   elements inside the step (e.g. `button:not(.so-bw-btn) {
   display: none }` on certain templates), which would
   otherwise invisible-ify the pill text.
   Padding, font-size, radius and gap now consume the design-
   system slot tokens so per-template and responsive overrides
   to those tokens propagate automatically. */
#so-booking-widget .so-bw-slot-row {
    display: flex !important;
    align-items: center;
    justify-content: center;
    width: 100%;
    min-height: 38px;
    padding: var(--so-bw-slot-pad-y, 10px) var(--so-bw-slot-pad-x, 14px);
    border: 1px solid var(--so-bw-border);
    border-radius: var(--so-bw-slot-radius, var(--so-bw-radius));
    background: var(--so-bw-card-bg);
    font-family: var(--so-bw-font);
    font-size: var(--so-bw-slot-font-size, 13px);
    font-weight: 500;
    color: var(--so-bw-text);
    cursor: pointer;
    transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;
    text-align: center;
    line-height: 1.2;
    /* Allow the label to wrap and the pill to shrink below its content width
       so it can never overflow its grid column (which clipped slots before). */
    white-space: normal;
    min-width: 0;
    visibility: visible;
    opacity: 1;
    box-sizing: border-box;
}
/* THE SLOT LABEL IS TWO SPANS, and by default it must not look like it.
   `.so-bw-slot-time` is the start, `.so-bw-slot-till` the "to <end>" tail;
   the markup wraps both so the space between them survives flex layout (the
   reason is written out next to the markup). Everything here is inherit /
   inline, so for the nineteen templates that do not restyle them the button
   renders the identical string it did when the label was one text node.
   Wide Canvas is the one template that pulls them onto separate lines. */
#so-booking-widget .so-bw-slot-range {
    display: inline;
    min-width: 0;
}
#so-booking-widget .so-bw-slot-time,
#so-booking-widget .so-bw-slot-till {
    font: inherit;
    color: inherit;
    letter-spacing: inherit;
}
.so-bw-slot-row:hover {
    border-color: var(--so-bw-primary);
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
    background: var(--so-bw-primary-light);
}
.so-bw-slot-row--selected,
.so-bw-slot-row--selected:hover {
    background: var(--so-bw-primary);
    border-color: var(--so-bw-primary);
    color: #ffffff;
    font-weight: 700;
    box-shadow: 0 4px 10px rgba(var(--so-bw-primary-rgb, 99, 102, 241), 0.32);
}
/* Booked overlay chip: surfaces a slot the chosen staff is already
   reserved at as a plain disabled chip — same border/background as a
   normal slot, just dimmed and unclickable. No red, no badge, no
   strikethrough; the cursor + reduced opacity are the only cues. */
#so-booking-widget .so-bw-slot-row--booked,
#so-booking-widget .so-bw-slot-row--booked:hover {
    background: var(--so-bw-card-bg);
    border-color: var(--so-bw-border);
    color: var(--so-bw-text);
    cursor: not-allowed;
    opacity: 0.45;
    box-shadow: none;
}
#so-booking-widget .so-bw-slot-row-flag {
    display: none;
}

/* A booked slot the customer can still tap to JOIN THE WAITLIST for that exact
   time (Global Waitlist enabled). Unlike a plain booked chip it stays
   interactive: full opacity, pointer cursor, a "Join waitlist" badge, and a
   primary-tinted hover. */
#so-booking-widget .so-bw-slot-row--waitlistable,
#so-booking-widget .so-bw-slot-row--waitlistable:hover {
    cursor: pointer;
    opacity: 1;
}
#so-booking-widget .so-bw-slot-row--waitlistable:hover {
    border-color: var(--so-bw-primary);
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
    background: var(--so-bw-primary-light);
}
#so-booking-widget .so-bw-slot-row--waitlistable .so-bw-slot-row-flag {
    display: inline-flex;
    align-items: center;
    margin-left: auto;
    padding: 1px 7px;
    border-radius: 999px;
    font-size: 10px;
    font-weight: 600;
    background: #fef3c7;
    color: #b45309;
    /* "Join waitlist" is translated and the chip shares a row with the time.
       Wrapping keeps both inside the row; nowrap pushed the flag past the
       slot's right edge on the narrow single-column slot layouts. */
    white-space: normal;
    overflow-wrap: anywhere;
    text-align: center;
    line-height: 1.3;
    max-width: 100%;
}

/* ==========================================================================
   13. Time Slots

   !! DEAD SECTION — nothing below this banner renders, down to `.so-bw-form`.
   Every selector here (.so-bw-timeslots, .so-bw-timeslots-title,
   .so-bw-time-grid, .so-bw-time-slot and its modifiers — ~55 rules) names a
   class the widget does not build. booking-widget.js renders:

       .so-bw-timeslots        ->  .so-bw-slot-panel
       .so-bw-timeslots-title  ->  .so-bw-slot-panel-title
       .so-bw-time-grid        ->  .so-bw-slot-list
       .so-bw-time-slot        ->  .so-bw-slot-row

   So the "modernised slot grid" below — the uniform pill chips, the fluid
   clamp() columns, the scrolling region — has never been what customers see.
   The live equivalents are styled further down the file and in section 14.

   NOT renamed in place, deliberately: each live counterpart already declares
   the same properties (.so-bw-slot-row alone clashes on 24 of them, including
   padding, border-radius and font-size), so a rename is a contested cascade
   change and needs visual verification across the architectures — not a
   find-and-replace. Do that as its own pass, with screenshots, and delete
   whichever side loses. Same applies to the smaller orphans marked elsewhere:
   .so-bw-service-card/-grid, .so-bw-card-img, .so-bw-step-title/-counter,
   .so-bw-datetime-grid, .so-bw-calendar-dow, .so-bw-slot-group-label.
   ========================================================================== */

.so-bw-timeslots {
    margin-top: 12px;
}

.so-bw-timeslots-title {
    font-size: 13px;
    font-weight: 700;
    color: var(--so-bw-text);
    margin-bottom: 10px;
    letter-spacing: -0.01em;
}
.so-bw-timeslots-title span {
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}

/* Modernised slot grid — uniform pill chips, fluid column min-
   width so the row reflows smoothly from phone to studio
   container. clamp() keeps slots tap-friendly on phones (74px)
   without exploding to 4-column rows on the studio layout.
   max-height + overflow-y makes long slot lists scroll within
   their own region instead of pushing the rest of the step
   below the fold. The 320px ceiling is roughly 4-5 rows of
   pills which is more than enough to scan at a glance. */
.so-bw-time-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(clamp(74px, calc(var(--so-bw-cw, 100vw) * 0.09), 96px), 1fr));
    gap: var(--so-bw-slot-gap);
    max-height: 320px;
    overflow-y: auto;
    padding-right: 4px;
    scrollbar-width: thin;
    scrollbar-color: var(--so-bw-border) transparent;
}
.so-bw-time-grid::-webkit-scrollbar {
    width: 6px;
}
.so-bw-time-grid::-webkit-scrollbar-thumb {
    background: var(--so-bw-border);
    border-radius: 999px;
}
.so-bw-time-grid::-webkit-scrollbar-track {
    background: transparent;
}

.so-bw-time-slot {
    display: flex;
    align-items: center;
    justify-content: center;
    padding: clamp(8px, calc(var(--so-bw-cw, 100vw) * 0.01), 11px) clamp(8px, calc(var(--so-bw-cw, 100vw) * 0.012), 14px);
    border: 1px solid var(--so-bw-border);
    border-radius: var(--so-bw-slot-radius);
    background: var(--so-bw-card-bg);
    font-family: var(--so-bw-font);
    font-size: clamp(12px, calc(var(--so-bw-cw, 100vw) * 0.01), 14px);
    font-weight: 600;
    color: var(--so-bw-text);
    cursor: pointer;
    transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease, transform 0.15s ease, box-shadow 0.15s ease;
    letter-spacing: -0.01em;
    line-height: 1.2;
    /* A slot chip is usually "09:30", but a 12-hour locale with a long
       meridiem, a range format ("09:30 – 10:15") or a duration suffix all
       run longer than the 74px column floor. min-height is a FLOOR for the
       tap target, not a cap — the chip grows to two lines when it has to and
       the grid row grows with it. */
    white-space: normal;
    overflow-wrap: anywhere;
    text-align: center;
    height: auto;
    min-height: 38px;
}

.so-bw-time-slot:hover {
    border-color: var(--so-bw-primary);
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
    background: var(--so-bw-primary-light);
    transform: translateY(-1px);
}

.so-bw-time-slot--selected {
    background: var(--so-bw-primary);
    border-color: var(--so-bw-primary);
    color: #ffffff;
    font-weight: 700;
    box-shadow: 0 4px 10px rgba(var(--so-bw-primary-rgb, 99, 102, 241), 0.32);
    transform: none;
}

.so-bw-time-slot--selected:hover {
    background: var(--so-bw-primary-hover, var(--so-bw-primary));
    color: #ffffff;
    transform: translateY(-1px);
}

/* ==========================================================================
   14. Forms
   ========================================================================== */

/* Step-6 form lays out on a 12-column grid so the public widget
   honors the column_span the admin picked in Form Builder
   (1 = Full, 2 = Half, 3 = Third). Without this the eight-field
   intake forms stacked one per row and pushed the bottom Continue
   button below the fold; admins setting half-width fields didn't
   see those reflected on the public side. */
.so-bw-form {
    display: grid;
    grid-template-columns: repeat(12, 1fr);
    gap: 14px 16px;
}

.so-bw-form-row {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 16px;
    /* As a grid item inside .so-bw-form (repeat(12, 1fr)), span all
       12 columns so the row stretches full width. Without this it
       occupies only 1 of 12 tracks (~1/12 of the form width). */
    grid-column: 1 / -1;
}

.so-bw-form-group {
    /* Default — Full width. Per-field modifier classes below
       narrow the span when the admin picked Half / Third in the
       Form Builder. */
    grid-column: span 12;
}

.so-bw-form-group--half {
    grid-column: span 6;
}

.so-bw-form-group--third {
    grid-column: span 4;
}

/* On narrow viewports collapse everything back to full-width so
   the form stays readable without horizontal compression. */
/* was @media (max-width: 540px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    #so-booking-widget:where([data-cw~="540"]) .so-bw-form-group--half,
    #so-booking-widget:where([data-cw~="540"]) .so-bw-form-group--third{
        grid-column: span 12;
    }


/* .so-bw-form uses repeat(12, 1fr) so span 12 = full width.
   .so-bw-form-row uses 1fr 1fr for the first/last name pair — a
   2-col grid. span 12 inside a 2-col grid forces the browser to
   auto-create 10 implicit zero-width tracks, collapsing the form
   to ~9px. Override to `auto` so the 2-col grid flows naturally. */
.so-bw-form-row .so-bw-form-group {
    grid-column: auto;
}

.so-bw-label {
    display: block;
    font-size: var(--so-bw-input-label-size);
    font-weight: 600;
    color: var(--so-bw-text);
    margin-bottom: 6px;
    /* Admin-typed labels can contain long unbroken runs — wrap them
       instead of overflowing the field column. */
    overflow-wrap: anywhere;
}

/* Specificity guard: #so-booking-widget * { margin: 0 } at (1,0,0) beats
   plain-class .so-bw-label { margin-bottom } at (0,1,0). Prefix with
   #so-booking-widget to lift to (1,1,0). */
#so-booking-widget .so-bw-label {
    margin-bottom: 6px;
}

.so-bw-required {
    color: var(--so-bw-error);
    margin-left: 2px;
}

/* Specificity guard: #so-booking-widget * { padding: 0 } at (1,0,0) beats
   plain-class .so-bw-input { padding } at (0,1,0). Prefix with
   #so-booking-widget to lift to (1,1,0) so the token-based padding survives.
   Without this, form inputs render with zero padding (text flush to border)
   on every template — most visible on mobile where the reset trap is hit. */
#so-booking-widget .so-bw-input {
    padding: var(--so-bw-input-pad-y) var(--so-bw-input-pad-x);
}

.so-bw-input {
    width: 100%;
    padding: var(--so-bw-input-pad-y) var(--so-bw-input-pad-x);
    border: 1px solid var(--so-bw-border);
    border-radius: var(--so-bw-input-radius);
    font-family: var(--so-bw-font);
    font-size: var(--so-bw-input-font-size);
    color: var(--so-bw-text);
    /* Bound to --so-bw-card-bg (white) so inputs stay clean on
       themed templates with a tinted --so-bw-bg (Fitness Red's
       cream pink, Cafe Coffee's yellow, Art Violet's violet, etc.).
       Was --so-bw-bg, which made inputs render in the body's tint
       and read as ill-defined chips on the form step. */
    background: var(--so-bw-card-bg);
    transition: border-color var(--so-bw-transition), box-shadow var(--so-bw-transition);
    line-height: 1.5;
}

.so-bw-input:focus {
    outline: none;
    border-color: var(--so-bw-border-focus);
    box-shadow: 0 0 0 3px rgba(var(--so-bw-primary-rgb), 0.15);
}

.so-bw-input::placeholder {
    color: var(--so-bw-text-tertiary);
}

.so-bw-textarea {
    resize: vertical;
    min-height: 88px;
}

.so-bw-select {
    appearance: none;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 12px center;
    padding-right: 36px;
}

/* Specificity guard: #so-booking-widget * { padding: 0 } (1,0,0) beats
   .so-bw-select { padding-right } (0,1,0). */
#so-booking-widget .so-bw-select {
    padding-right: 36px;
}

.so-bw-field-help {
    font-size: 12px;
    color: var(--so-bw-text-tertiary);
    margin-top: 4px;
}

/* Specificity guard for field help margin */
#so-booking-widget .so-bw-field-help {
    margin-top: 4px;
}

/* Checkbox label */
.so-bw-checkbox-label {
    display: flex;
    align-items: center;
    gap: 8px;
    cursor: pointer;
    font-size: 14px;
    color: var(--so-bw-text);
    overflow-wrap: anywhere;
}
.so-bw-checkbox-label > span {
    min-width: 0;
    overflow-wrap: anywhere;
}

.so-bw-checkbox-input {
    width: 18px;
    height: 18px;
    accent-color: var(--so-bw-primary);
    cursor: pointer;
}

/* Radio group */
.so-bw-radio-group {
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.so-bw-radio-label {
    display: flex;
    align-items: center;
    gap: 8px;
    cursor: pointer;
    font-size: 14px;
    color: var(--so-bw-text);
}

.so-bw-radio-label input[type="radio"] {
    width: 18px;
    height: 18px;
    accent-color: var(--so-bw-primary);
    cursor: pointer;
}

/* ==========================================================================
   15. Buttons
   ========================================================================== */

.so-bw-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    /* Structure tokens — set once at :root, overridden by templates
       only via the matching --so-bw-btn-* tokens. Padding, radius,
       font-size, weight, transform, letter-spacing never set here
       as raw values, so a template can change any one knob without
       breaking the others. */
    padding: var(--so-bw-btn-pad-y) var(--so-bw-btn-pad-x);
    border: none;
    border-radius: var(--so-bw-btn-radius);
    font-family: var(--so-bw-font);
    font-size: var(--so-bw-btn-font-size);
    font-weight: var(--so-bw-btn-font-weight);
    letter-spacing: var(--so-bw-btn-letter-spacing);
    text-transform: var(--so-bw-btn-text-transform);
    cursor: pointer;
    transition: all var(--so-bw-transition);
    line-height: 1.5;
    /* Button labels are the most translated strings in the widget, and the
       nav row holds two of them side by side. `nowrap` meant a German or
       Finnish "Continue"/"Confirm booking" pair overflowed the row and got
       clipped by the container's overflow: hidden — the customer could not
       see what they were about to press. Labels wrap and the button grows;
       height is never fixed, so the row follows. */
    white-space: normal;
    overflow-wrap: anywhere;
    text-align: center;
    height: auto;
    min-width: 0;
    max-width: 100%;
    text-decoration: none;
}

.so-bw-btn--primary {
    background: var(--so-bw-primary);
    /* The label on an accent FILL. #ffffff was safe while every accent came
       from the template catalogue, all of which are dark enough to carry it.
       A custom widget theme makes a light accent reachable for the first time,
       and a white label on one is invisible — so the shortcode derives the
       readable ink per accent and publishes it here. The fallback keeps every
       unthemed install on exactly the colour it had. */
    color: var(--so-bw-on-primary, #ffffff);
    /* 1px transparent border so the height matches the secondary
       (Back) button — the secondary uses an outline border for its
       visual identity, and without a matching border on primary the
       two buttons would render 1px apart in height side-by-side. */
    border: 1px solid var(--so-bw-primary);
}

.so-bw-btn--primary:hover:not(:disabled) {
    background: var(--so-bw-primary-hover);
    border-color: var(--so-bw-primary-hover);
    box-shadow: var(--so-bw-shadow);
}

/* ── A disabled CTA still has to be READABLE ─────────────────────────────
   This was `opacity: 0.5`, and opacity is the one thing that cannot express
   "inactive" without destroying legibility: it fades the label and the fill by
   the SAME factor, so both slide toward the surface behind them and the
   contrast BETWEEN them collapses. It is the ratio that carries the text, and
   the ratio is what opacity does not preserve.

   Measured on the Continue button at "Your Details", label against its own
   fill, enabled -> disabled:

       Agenda      5.62:1 -> 2.17:1
       Centred     5.62:1 -> 2.17:1
       Editorial   5.62:1 -> 2.20:1
       Coral Pop   5.62:1 -> 2.21:1
       Dusk        2.97:1 -> 1.62:1

   Every one lands under the 3:1 floor for non-text, let alone the 4.5:1 for
   text, and Dusk at 1.62:1 is effectively a blank lozenge — which is exactly
   the "button is not visible" report, and why the step reads as having no
   forward control at all rather than one that is waiting on the form.

   So: state the disabled colours instead of fading them. Both come off the
   design-system neutral ramp, which the shortcode re-derives per theme
   (--so-bw-ds-n100/n500 step off the chosen background toward its ink), so
   this inverts correctly on the dark-first templates instead of needing a
   second rule for them. n500 on n100 measures 5.25:1 on the stock ramp.

   Stated twice: plain for the base component, and again at (1,3,0) so it
   outranks the per-template `#so-booking-widget[data-template="x"]
   .so-bw-btn--primary` fills that would otherwise keep the accent background
   underneath the new label colour — which would be worse than what it
   replaces. */
.so-bw-btn--primary:disabled,
#so-booking-widget[data-template] .so-bw-btn--primary:disabled {
    opacity: 1;
    background: var(--so-bw-ds-n100);
    border-color: var(--so-bw-ds-n100);
    color: var(--so-bw-ds-n500);
    box-shadow: none;
    cursor: not-allowed;
}

/* Back button: identical to the primary button by request — same
   fill, same border, same hover, same height. The two nav buttons
   are still distinguishable by their leading/trailing chevron
   icons and labels ("Back" vs "Continue"), so the styling can be
   uniform without losing affordance. The .so-bw-btn template
   overrides (classic / bubble / compact) all key on the base
   `.so-bw-btn` selector, so this uniformity carries through to
   every template automatically. */
.so-bw-btn--secondary {
    background: var(--so-bw-primary);
    color: #ffffff;
    border: 1px solid var(--so-bw-primary);
}

.so-bw-btn--secondary:hover:not(:disabled) {
    background: var(--so-bw-primary-hover);
    border-color: var(--so-bw-primary-hover);
    box-shadow: var(--so-bw-shadow);
}

.so-bw-btn--confirm {
    /* Wider than Continue/Back for emphasis, but the SAME height — it
       inherits the base button's vertical padding + font-size so all
       three footer buttons line up on one baseline. (Was `padding:12px
       32px; font-size:16px`, which made Confirm taller than Back and
       read as misaligned.) */
    padding-left: 32px;
    padding-right: 32px;
}

.so-bw-btn-loading {
    display: inline-flex;
    align-items: center;
    gap: 8px;
}

/* ==========================================================================
   16. Navigation (Prev / Next)
   ========================================================================== */

/* Payment retry strip — sits between the review body and the nav when a
   Razorpay checkout failed or was dismissed. Reads as a warning rather than a
   hard error: the booking is still held and the customer can just try again. */
.so-bw-pay-retry {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    flex-wrap: wrap;
    padding: 14px 24px;
    border-top: 1px solid var(--so-bw-border);
    background: #fffbeb;
}

.so-bw-pay-retry-msg {
    flex: 1 1 200px;
    min-width: 0;
    font-size: 13px;
    line-height: 1.45;
    color: #92400e;
}

.so-bw-nav {
    display: flex;
    align-items: center;
    justify-content: space-between;
    /* Gap so Back and Continue/Confirm never touch when the row gets
       tight (and it's the spacing used once they go equal-width on
       mobile). */
    gap: 12px;
    /* Last resort when two long labels cannot share the row even after
       wrapping their own text: Confirm drops below Back rather than either
       button being clipped by the container. Reached only on very narrow
       viewports with very long translations. */
    flex-wrap: wrap;
    padding: 16px 24px;
    border-top: 1px solid var(--so-bw-border);
    /* Bound to --so-bw-card-bg (white) so the bottom nav reads as
       part of the same lifted surface as the cards above on every
       themed template. Was --so-bw-bg-secondary (gray), which on
       Cafe Coffee's cream-yellow / Boutique Rose's pink / Art
       Violet's violet body looked like a disconnected gray strip
       at the bottom. Dark-theme override flips it to the dark
       card surface automatically. */
    background: var(--so-bw-card-bg);
    /* Sticky to the bottom of the viewport so the Continue /
       Confirm button stays visible no matter how long the
       current step is (the 8-field Form Builder intake form
       was previously pushing it below the fold on narrow
       screens). z-index keeps it above the calendar / card
       grids. */
    position: sticky;
    bottom: 0;
    z-index: 5;
}

/* Powered-by badge (Studio → Appearance → Show "Powered by Schedulerity").
   Deliberately quiet: it is the last thing in the widget and must never
   compete with the Continue/Confirm button directly above it. Uses the
   muted ink token so it inverts correctly on the dark templates instead of
   turning into invisible near-black on a dark surface — the same trap the
   heading tokens hit. No top border: .so-bw-nav already draws one, and a
   second rule 40px below read as a stray divider. */
.so-bw-powered {
    padding: 10px 24px 14px;
    text-align: center;
    font-size: 11.5px;
    line-height: 1.4;
    letter-spacing: 0.01em;
    color: var(--so-bw-text-secondary, #6b7280);
    background: var(--so-bw-card-bg);
}

.so-bw-powered__name {
    font-weight: 600;
    /* Not the accent colour — a coloured brand name next to a coloured CTA
       makes the badge read as a second action. Weight alone is enough. */
    color: inherit;
}

/* ==========================================================================
   17. Review / Summary
   ========================================================================== */

.so-bw-summary {
    /* Bound to --so-bw-card-bg so themed templates show a clean
       white summary panel against their tinted body. Was
       --so-bw-bg-secondary (gray) which on Cafe Coffee / Boutique
       Rose / Art Violet read as a disconnected gray block. */
    background: var(--so-bw-card-bg);
    border: 1px solid var(--so-bw-border);
    border-radius: var(--so-bw-summary-radius);
    overflow: hidden;
}

/* THE TWO SUMMARY GROUPS ARE INVISIBLE BY DEFAULT.
   The review markup wraps its blocks in .so-bw-summary-main (what was booked)
   and .so-bw-summary-pay (what it costs); `display: contents` takes both boxes
   out of the layout so their children remain direct participants in whatever
   the summary is — one column for nineteen templates, a two-pane grid for Wide
   Canvas, which is the only one that gives them a box back. Neither wrapper
   carries a background, border or padding, so there is nothing for
   `display: contents` to drop. The reason they exist at all is written next to
   the markup in booking-widget.js. */
#so-booking-widget .so-bw-summary-main,
#so-booking-widget .so-bw-summary-pay {
    display: contents;
}

/* Sectioned review layout — group "Booking details" and "Your
   details" into separate panels with a subtle header so the
   review step reads top-to-bottom as a recap rather than as one
   long undifferentiated table of label/value pairs. Each section
   has its own header strip that matches --so-bw-bg-secondary so
   it tints with the active template. */
.so-bw-summary-section {
    border-bottom: 1px solid var(--so-bw-border);
}
.so-bw-summary-section:last-of-type {
    border-bottom: none;
}
.so-bw-summary-section-title {
    font-size: 11px;
    font-weight: 700;
    color: var(--so-bw-text-secondary);
    text-transform: uppercase;
    letter-spacing: 0.06em;
    line-height: 1.3;
    padding: 10px var(--so-bw-summary-pad-x);
    background: var(--so-bw-bg-secondary);
    border-bottom: 1px solid var(--so-bw-border);
}

/* Service-policies block. Mirrors the admin BookingsPage's
   wizardActivePolicyHints display. Used in two places:
   - step 5 (Date & Time) as `--inline` so the customer sees
     constraints BEFORE picking a date.
   - step 8 (Review) as `--review` for the final summary.
   Default styling is the standalone card; the modifiers tweak
   spacing only. */
.so-bw-policies {
    padding: 14px 16px;
    background: var(--so-bw-bg-secondary);
    border: 1px solid var(--so-bw-border);
    border-radius: var(--so-bw-radius);
}
.so-bw-policies--inline {
    /* Sits between step description and calendar — give it a
       little breathing room without pushing the calendar down
       too far on narrow viewports. */
    margin: 0 0 16px;
}
.so-bw-policies--review {
    /* Below .so-bw-summary on the review step. Shares the
       summary's horizontal alignment but stays visually
       separate (its own card). */
    margin: 16px 0 0;
}
.so-bw-policies-title {
    font-size: 13px;
    font-weight: 700;
    color: var(--so-bw-text);
    margin-bottom: 12px;
    text-transform: uppercase;
    letter-spacing: 0.04em;
}
.so-bw-policies-group {
    margin-bottom: 12px;
}
.so-bw-policies-group:last-child {
    margin-bottom: 0;
}
.so-bw-policies-group-title {
    font-size: 12px;
    font-weight: 600;
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
    margin-bottom: 4px;
    text-transform: uppercase;
    letter-spacing: 0.03em;
}
.so-bw-policies-items {
    list-style: none;
    margin: 0;
    padding: 0;
}
.so-bw-policies-item {
    font-size: 13px;
    color: var(--so-bw-text-secondary);
    line-height: 1.5;
    margin-bottom: 4px;
    word-break: break-word;
}
.so-bw-policies-item:last-child {
    margin-bottom: 0;
}

.so-bw-summary-row {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    padding: var(--so-bw-summary-pad-y) var(--so-bw-summary-pad-x);
    gap: 4px 12px;
    border-bottom: 1px solid var(--so-bw-border);
    /* A review row is label + customer data. When a translated label and a
       long value (a four-service list, a full address, an email) cannot share
       the row, the value takes its own line under the label instead of
       compressing to a one-word column. */
    flex-wrap: wrap;
}
.so-bw-summary-row:last-child {
    border-bottom: none;
}

.so-bw-summary-row--total {
    background: var(--so-bw-bg-tertiary);
    padding: 14px var(--so-bw-summary-pad-x);
    border-bottom: none;
}

/* Label + value share the same font-size by default so rows read as
   a balanced two-column list. The label is muted (text-secondary)
   and the value is full-strength (text); they line up on the
   baseline so different content lengths still read evenly. */
.so-bw-summary-label {
    font-size: 13px;
    font-weight: 600;
    color: var(--so-bw-text-secondary);
    /* flex-basis keeps the two-column rhythm while the row has room; the
       label may shrink and wrap once it doesn't, which `flex-shrink: 0`
       previously forbade — that is what forced the VALUE to absorb every
       overflow. min-width:0 releases the automatic minimum so a long
       single-word label ("Terminvereinbarung") wraps instead of widening
       the panel. */
    flex: 0 1 40%;
    max-width: 45%;
    /* THE FLOOR MATTERS MORE THAN THE CEILING.
       `min-width: 0` plus `overflow-wrap: anywhere` is the pair that set
       "Service" as S/e/r/v/i/c/e on the review screen of six templates: the
       first removes the flex item's automatic min-content floor, the second
       tells the text it may break between any two characters, and together
       they let a long value squeeze the label to 8px rather than wrap to its
       own line.

       `min-content` keeps the localisation guard the note above is about — a
       label like "Terminvereinbarung" still wraps rather than widening the
       panel — while `break-word` keeps a single over-long word breakable
       WITHOUT that break opportunity counting toward the intrinsic minimum.
       That distinction between `anywhere` and `break-word` is the whole fix:
       both break the word, only one lets the box collapse first. */
    min-width: min-content;
    overflow-wrap: break-word;
    line-height: 1.4;
}

.so-bw-summary-tz {
    color: var(--so-bw-text-secondary, var(--so-bw-text));
    font-weight: 500;
    font-size: 12px;
    margin-left: 2px;
}
.so-bw-summary-value {
    font-size: 13px;
    font-weight: 500;
    color: var(--so-bw-text);
    text-align: right;
    word-break: break-word;
    overflow-wrap: anywhere;
    min-width: 0;
    flex: 1 1 auto;
    line-height: 1.4;
}

/* Total row gets a slight bump (1 step up) on label + price so
   the total still reads as the row's emphasis without the
   18px-vs-13px mismatch the previous styling produced. */
.so-bw-summary-row--total .so-bw-summary-label {
    font-size: 14px;
    font-weight: 700;
    color: var(--so-bw-text);
}
.so-bw-summary-value--price {
    font-size: 16px;
    font-weight: 700;
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}

.so-bw-summary-divider {
    height: 1px;
    background: var(--so-bw-border);
    margin: 4px 20px;
}

/* ==========================================================================
   17a. Booking-Policy Panels (Step 5)
   ==========================================================================
   Generic panel used by package / group-event / multi-day / waitlist /
   time-range. All five share the same shell shape; tints come from
   per-modifier border + accent rules. Tokens drive colours so the
   active template paints them automatically. */

.so-bw-policy-panel {
    margin-top: 16px;
    border: 1px solid var(--so-bw-border);
    border-radius: var(--so-bw-radius);
    background: var(--so-bw-card-bg);
    padding: 16px;
    display: flex;
    flex-direction: column;
    gap: 12px;
}
.so-bw-policy-panel-head {
    display: flex;
    align-items: center;
    gap: 10px;
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}
.so-bw-policy-panel-head svg {
    flex-shrink: 0;
}
.so-bw-policy-panel-title {
    margin: 0;
    font-size: 13px;
    font-weight: 700;
    color: var(--so-bw-text);
}
.so-bw-policy-panel-text {
    margin: 0;
    font-size: 12px;
    color: var(--so-bw-text-secondary);
    line-height: 1.5;
}
.so-bw-policy-panel-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
    gap: 12px;
}
.so-bw-policy-panel-grid--3col {
    grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
}
.so-bw-policy-panel-grid--2col {
    /* Was a hard `repeat(2, 1fr)`, which held two columns down to a 288px
       viewport and squeezed each label into a four-character column. auto-fit
       with a min(100%, …) floor keeps two columns while they fit and folds to
       one when they don't — the same pattern the base grid and the daypart
       panel already use. */
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 150px), 1fr));
}
.so-bw-policy-panel-cell {
    display: flex;
    flex-direction: column;
    gap: 2px;
    padding: 8px 12px;
    border-radius: var(--so-bw-r-8);
    background: var(--so-bw-bg-secondary);
    border: 1px solid var(--so-bw-border);
}
.so-bw-policy-panel-row {
    display: grid;
    /* Same reasoning as --2col: two fields while there is room for two. */
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 150px), 1fr));
    gap: 12px;
    align-items: end;
}
.so-bw-policy-panel-field {
    display: flex;
    flex-direction: column;
    gap: 4px;
    min-width: 0;
}
.so-bw-policy-panel-field-info {
    display: flex;
    align-items: center;
    padding: 8px 12px;
    border-radius: var(--so-bw-r-8);
    background: var(--so-bw-bg-secondary);
    border: 1px solid var(--so-bw-border);
    font-size: 12px;
    color: var(--so-bw-text-secondary);
}
.so-bw-policy-panel-label {
    margin: 0;
    font-size: 11px;
    font-weight: 600;
    color: var(--so-bw-text-secondary);
    text-transform: uppercase;
    letter-spacing: 0.04em;
}
.so-bw-policy-panel-value {
    margin: 2px 0 0;
    font-size: 14px;
    font-weight: 700;
    color: var(--so-bw-text);
}
.so-bw-policy-panel-value--saving {
    color: var(--so-bw-success, #16a34a);
}
.so-bw-policy-panel-input {
    width: 100%;
    padding: 8px 12px;
    border-radius: var(--so-bw-r-8);
    border: 1px solid var(--so-bw-border);
    background: var(--so-bw-input-bg, var(--so-bw-card-bg));
    font-size: 14px;
    color: var(--so-bw-text);
    box-sizing: border-box;
    font-family: inherit;
}
.so-bw-policy-panel-input--readonly {
    background: var(--so-bw-bg-secondary);
    color: var(--so-bw-text-secondary);
}
.so-bw-policy-panel-textarea {
    resize: vertical;
    min-height: 60px;
}
.so-bw-policy-panel-error {
    margin: 4px 0 0;
    font-size: 11px;
    color: var(--so-bw-danger, #b91c1c);
}
.so-bw-policy-panel-total {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 10px 12px;
    border-radius: var(--so-bw-r-8);
    background: var(--so-bw-bg-secondary);
    border: 1px solid var(--so-bw-border);
    font-size: 13px;
    font-weight: 700;
    color: var(--so-bw-text);
}

/* Modifier tints — use a primary-tinted left edge to differentiate
   panels at a glance without going against the active template's
   palette. Falls back to the primary token for templates that don't
   define semantic accents. */
.so-bw-policy-panel--package      .so-bw-policy-panel-head { color: var(--so-bw-success, #0d9488); }
.so-bw-policy-panel--group        .so-bw-policy-panel-head { color: var(--so-bw-primary); }
.so-bw-policy-panel--multi-day    .so-bw-policy-panel-head { color: var(--so-bw-warning, #ea580c); }
.so-bw-policy-panel--waitlist     .so-bw-policy-panel-head { color: var(--so-bw-danger,  #dc2626); }
.so-bw-policy-panel--time-range   .so-bw-policy-panel-head { color: var(--so-bw-primary); }

/* Time-range button grid — replaces the regular time-slot grid for
   services with the time_range policy. Matches the slot button
   shape but with a label + window display. */
.so-bw-time-range-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
    gap: 8px;
}
.so-bw-time-range {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 2px;
    padding: 12px 14px;
    border-radius: var(--so-bw-r-10);
    border: 2px solid var(--so-bw-border);
    background: var(--so-bw-card-bg);
    color: var(--so-bw-text);
    cursor: pointer;
    transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
    font-family: inherit;
    text-align: left;
}
.so-bw-time-range:hover {
    border-color: var(--so-bw-primary);
}
.so-bw-time-range--selected {
    border-color: var(--so-bw-primary);
    background: var(--so-bw-primary-light, color-mix(in srgb, var(--so-bw-primary) 12%, transparent));
}
.so-bw-time-range-label {
    font-size: 13px;
    font-weight: 700;
    color: var(--so-bw-text);
}
.so-bw-time-range--selected .so-bw-time-range-label {
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}
.so-bw-time-range-times {
    font-size: 11px;
    font-weight: 600;
    color: var(--so-bw-text-secondary);
}
.so-bw-time-range--selected .so-bw-time-range-times {
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}

/* Availability-window banner on Step 5 — surfaces the bookable
   hours + optional break so customers understand why slots outside
   the window don't appear. Mirrors the admin's blue helper banner. */
.so-bw-availability-banner {
    display: flex;
    align-items: flex-start;
    gap: 10px;
    padding: 10px 14px;
    border-radius: var(--so-bw-radius);
    background: var(--so-bw-info-light, color-mix(in srgb, var(--so-bw-primary) 8%, transparent));
    border: 1px solid var(--so-bw-info, color-mix(in srgb, var(--so-bw-primary) 35%, var(--so-bw-border)));
    color: var(--so-bw-text);
    margin-top: 12px;
}
.so-bw-availability-banner svg {
    flex-shrink: 0;
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
    margin-top: 2px;
}
.so-bw-availability-banner p {
    margin: 0;
    font-size: 12px;
    color: var(--so-bw-text);
    line-height: 1.5;
}

/* Approval-required banner on Step 8 (review). */
.so-bw-approval-banner {
    display: flex;
    align-items: flex-start;
    gap: 10px;
    padding: 12px 14px;
    border-radius: var(--so-bw-radius);
    background: color-mix(in srgb, var(--so-bw-warning, #f59e0b) 10%, transparent);
    border: 1px solid var(--so-bw-warning, #f59e0b);
    color: var(--so-bw-text);
    margin-bottom: 16px;
}
.so-bw-approval-banner svg {
    flex-shrink: 0;
    color: var(--so-bw-warning, #b45309);
    margin-top: 2px;
}
.so-bw-approval-banner strong {
    font-size: 13px;
    color: var(--so-bw-text);
}
.so-bw-approval-banner p {
    margin: 4px 0 0;
    font-size: 12px;
    color: var(--so-bw-text-secondary);
    line-height: 1.5;
}

/* Mobile: collapse 2-column rows in policy panels to single column
   so labels + inputs don't get squashed. The base
   .so-bw-policy-panel-grid uses auto-fit minmax(140px, 1fr) which
   produces 2-col on 320px viewports (288px / 140px = 2 cols). The
   resulting 144px cells are too tight for label+value content, so
   force 1-col here. The --3col variant was already grid-template
   set above; keep its responsive collapse explicit too. */
/* was @media (max-width: 480px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    #so-booking-widget:where([data-cw~="480"]) .so-bw-policy-panel-grid,
    #so-booking-widget:where([data-cw~="480"]) .so-bw-policy-panel-grid--2col,
    #so-booking-widget:where([data-cw~="480"]) .so-bw-policy-panel-grid--3col,
    #so-booking-widget:where([data-cw~="480"]) .so-bw-policy-panel-row{
        grid-template-columns: 1fr;
    }


/* ==========================================================================
   17b. Recurring Booking Picker (Step 5)
   ==========================================================================
   Customer-facing equivalent of the admin BookingsPage's "How often
   should this repeat?" panel. Card layout with toggle header and
   reveal body, mirroring the admin shape so the same product
   spec applies to both audiences. Uses design-system tokens so
   themed templates pick up the active brand colour without
   per-template overrides. */

.so-bw-recurring {
    margin-top: 20px;
    border: 1px solid var(--so-bw-border);
    border-radius: var(--so-bw-radius);
    overflow: hidden;
    background: var(--so-bw-card-bg);
}
.so-bw-recurring-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 14px 16px;
    background: var(--so-bw-bg-secondary);
    border-bottom: 1px solid var(--so-bw-border);
    cursor: pointer;
    user-select: none;
}
.so-bw-recurring-header-info {
    display: flex;
    align-items: center;
    gap: 10px;
    min-width: 0;
}
.so-bw-recurring-header-icon {
    color: var(--so-bw-text-secondary);
    flex-shrink: 0;
    transition: color 0.15s ease;
}
.so-bw-recurring-header-icon--on {
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}
.so-bw-recurring-title {
    margin: 0;
    font-size: 13px;
    font-weight: 700;
    color: var(--so-bw-text);
}
.so-bw-recurring-subtitle {
    margin: 2px 0 0;
    font-size: 11px;
    color: var(--so-bw-text-secondary);
}

.so-bw-recurring-toggle {
    position: relative;
    width: 44px;
    height: 24px;
    border-radius: var(--so-bw-r-12);
    border: none;
    cursor: pointer;
    flex-shrink: 0;
    background: var(--so-bw-border);
    transition: background 0.2s ease;
}
.so-bw-recurring-toggle--on {
    background: var(--so-bw-primary);
}
.so-bw-recurring-toggle-knob {
    position: absolute;
    top: 2px;
    left: 2px;
    width: 20px;
    height: 20px;
    border-radius: 50%;
    background: #fff;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
    transition: left 0.2s ease;
}
.so-bw-recurring-toggle-knob--on {
    left: 22px;
}

.so-bw-recurring-body {
    padding: 16px;
    display: flex;
    flex-direction: column;
    gap: 16px;
}
.so-bw-recurring-section {
    display: flex;
    flex-direction: column;
    gap: 8px;
}
.so-bw-recurring-label {
    font-size: 12px;
    font-weight: 600;
    color: var(--so-bw-text-secondary);
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

/* Frequency picker — three big cards, one per cadence. Tokenized
   so the selected card uses the active template's primary colour
   without per-template overrides. */
.so-bw-recurring-freq-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
    gap: 8px;
}
.so-bw-recurring-freq {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 2px;
    padding: 12px 14px;
    border-radius: var(--so-bw-r-10);
    border: 1.5px solid var(--so-bw-border);
    background: var(--so-bw-card-bg);
    color: var(--so-bw-text);
    cursor: pointer;
    transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
    font-family: inherit;
    text-align: left;
}
.so-bw-recurring-freq:hover {
    border-color: var(--so-bw-primary);
}
.so-bw-recurring-freq--selected {
    border-color: var(--so-bw-primary);
    background: var(--so-bw-primary-light, color-mix(in srgb, var(--so-bw-primary) 12%, transparent));
}
.so-bw-recurring-freq-label {
    font-size: 13px;
    font-weight: 700;
    color: var(--so-bw-text);
}
.so-bw-recurring-freq--selected .so-bw-recurring-freq-label {
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}
.so-bw-recurring-freq-helper {
    font-size: 11px;
    color: var(--so-bw-text-secondary);
}

/* Day-of-week chips for weekly cadence. */
.so-bw-recurring-days {
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
}
.so-bw-recurring-day {
    /* A day chip holds a localised weekday abbreviation — two characters in
       English, three or more in plenty of locales. Fixed 38px boxes with
       border-radius: 50% clipped those. min-* keeps the same 38px circle
       whenever the label fits (a square at radius 999px IS a circle) and
       lets it relax into a pill when it doesn't. */
    min-width: 38px;
    min-height: 38px;
    padding: 0 6px;
    border-radius: 999px;
    font-size: 11px;
    font-weight: 700;
    /* Deliberate, and the reason the chip can grow: a three-letter
       abbreviation must widen the pill, not stack one letter per line. */
    white-space: nowrap;
    cursor: pointer;
    border: 1.5px solid var(--so-bw-border);
    background: var(--so-bw-card-bg);
    color: var(--so-bw-text-secondary);
    display: flex;
    align-items: center;
    justify-content: center;
    transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
    font-family: inherit;
}
.so-bw-recurring-day:hover {
    border-color: var(--so-bw-primary);
}
.so-bw-recurring-day--selected {
    background: var(--so-bw-primary);
    border-color: var(--so-bw-primary);
    color: #fff;
}

/* End condition radio rows. */
.so-bw-recurring-end {
    display: flex;
    flex-direction: column;
    gap: 8px;
}
.so-bw-recurring-end-row {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 8px;
    font-size: 13px;
    color: var(--so-bw-text);
    cursor: pointer;
}
.so-bw-recurring-end-row input[type="radio"] {
    accent-color: var(--so-bw-primary);
    width: 15px;
    height: 15px;
    margin: 0;
}
.so-bw-recurring-end-count {
    width: 64px;
    padding: 6px 8px;
    border: 1px solid var(--so-bw-border);
    border-radius: var(--so-bw-r-6);
    font-size: 13px;
    text-align: center;
    background: var(--so-bw-input-bg, var(--so-bw-card-bg));
    color: var(--so-bw-text);
}
.so-bw-recurring-end-date {
    padding: 6px 10px;
    border: 1px solid var(--so-bw-border);
    border-radius: var(--so-bw-r-6);
    font-size: 13px;
    background: var(--so-bw-input-bg, var(--so-bw-card-bg));
    color: var(--so-bw-text);
}
.so-bw-recurring-end-count:disabled,
.so-bw-recurring-end-date:disabled {
    opacity: 0.45;
}
.so-bw-recurring-end-hint {
    font-size: 11px;
    color: var(--so-bw-text-secondary);
}

/* Preview list — upcoming session dates. */
.so-bw-recurring-preview {
    border-top: 1px solid var(--so-bw-border);
    padding-top: 12px;
}
.so-bw-recurring-preview-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 8px;
}
.so-bw-recurring-preview-title {
    margin: 0;
    font-size: 12px;
    font-weight: 700;
    color: var(--so-bw-text);
}
.so-bw-recurring-preview-count {
    font-weight: 500;
    color: var(--so-bw-text-secondary);
    margin-left: 4px;
}
.so-bw-recurring-preview-refresh {
    font-size: 11px;
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
    background: none;
    border: none;
    cursor: pointer;
    text-decoration: underline;
    padding: 0;
    font-family: inherit;
}
.so-bw-recurring-preview-loading {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    padding: 12px;
    color: var(--so-bw-text-secondary);
    font-size: 12px;
}
.so-bw-recurring-preview-list {
    max-height: 220px;
    overflow-y: auto;
    display: flex;
    flex-direction: column;
    gap: 2px;
}
.so-bw-recurring-preview-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    padding: 6px 10px;
    border-radius: var(--so-bw-r-6);
    font-size: 12px;
    background: var(--so-bw-card-bg);
}
.so-bw-recurring-preview-row:nth-child(even) {
    background: var(--so-bw-bg-secondary);
}
.so-bw-recurring-preview-row--first {
    background: var(--so-bw-primary-light, color-mix(in srgb, var(--so-bw-primary) 12%, transparent)) !important;
}
.so-bw-recurring-preview-row-info {
    display: flex;
    align-items: center;
    gap: 8px;
    min-width: 0;
}
.so-bw-recurring-preview-num {
    width: 20px;
    height: 20px;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 10px;
    font-weight: 700;
    background: var(--so-bw-border);
    color: var(--so-bw-text-secondary);
    flex-shrink: 0;
}
.so-bw-recurring-preview-num--first {
    background: var(--so-bw-primary);
    color: #fff;
}
.so-bw-recurring-preview-date {
    font-weight: 500;
    color: var(--so-bw-text);
    min-width: 0;
    word-break: break-word;
}
.so-bw-recurring-preview-date--first {
    font-weight: 700;
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}
.so-bw-recurring-preview-skip {
    font-size: 10px;
    color: var(--so-bw-danger, #dc2626);
    background: none;
    border: none;
    cursor: pointer;
    padding: 2px 6px;
    border-radius: var(--so-bw-r-4);
    font-family: inherit;
}
.so-bw-recurring-preview-skip:hover {
    background: var(--so-bw-danger-light, #fef2f2);
}
.so-bw-recurring-preview-first-tag {
    font-size: 10px;
    font-weight: 600;
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
    flex-shrink: 0;
}

/* Skipped dates pill row. */
.so-bw-recurring-skipped {
    margin-top: 8px;
    padding-top: 8px;
    border-top: 1px dashed var(--so-bw-border);
}
.so-bw-recurring-skipped-title {
    margin: 0 0 4px;
    font-size: 11px;
    font-weight: 600;
    color: var(--so-bw-text-secondary);
}
.so-bw-recurring-skipped-list {
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
}
.so-bw-recurring-skipped-pill {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 2px 8px;
    border-radius: 999px;
    background: var(--so-bw-danger-light, #fef2f2);
    font-size: 11px;
    color: var(--so-bw-danger, #dc2626);
}
.so-bw-recurring-skipped-pill button {
    border: none;
    background: none;
    cursor: pointer;
    color: inherit;
    font-size: 14px;
    line-height: 1;
    padding: 0;
}

/* ==========================================================================
   18. Confirmation
   ========================================================================== */

/* The confirmation page is entirely dynamic customer data — booking
   reference, the services they picked, staff, location, their own name and
   email — so every text node here is sized by content, and every row wraps.
   Nothing on this screen is height-bounded. */
.so-bw-confirmation {
    text-align: center;
    padding: 32px 24px;
    height: auto;
    overflow: visible;
}

.so-bw-confirmation-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    /* A decorative circle around a fixed-size glyph — the one fixed box on
       this screen, and it holds no text. flex-shrink so a narrow viewport
       can't deform it into an ellipse. */
    width: 72px;
    height: 72px;
    flex-shrink: 0;
    border-radius: 50%;
    background: var(--so-bw-success-light);
    color: var(--so-bw-success);
    margin-bottom: 20px;
}

.so-bw-confirmation-title {
    font-size: 24px;
    font-weight: 700;
    color: var(--so-bw-text);
    margin-bottom: 8px;
    line-height: 1.25;
    overflow-wrap: anywhere;
}

.so-bw-confirmation-text {
    font-size: 15px;
    color: var(--so-bw-text-secondary);
    margin-bottom: 24px;
    max-width: 420px;
    margin-left: auto;
    margin-right: auto;
    line-height: 1.6;
    overflow-wrap: anywhere;
}

.so-bw-confirmation-details {
    background: var(--so-bw-bg-secondary);
    border: 1px solid var(--so-bw-border);
    border-radius: var(--so-bw-radius-lg);
    padding: 4px 0;
    margin-bottom: 24px;
    text-align: left;
}

/* ── THE AFTER-BOOKING ACTIONS ARE ONE SET, SO THEY ARE ONE ROW ──────────
   REPORTED from the confirmation screen: the actions were centred and still
   looked untidy. They were centred INDIVIDUALLY, which is not the same thing.

   The block stacked three things of three different widths down the middle:
   a 3-chip calendar row (Google 92px, Apple 76, Outlook 97 — each shrink-
   wrapped to its own word), then "Share booking" alone on the next line as a
   full `.so-bw-btn` at a different height again, then the primary CTA wider
   than both. Four rows, four widths, one axis — a centred pyramid with a
   ragged edge on both sides.

   Every one of them is the same KIND of thing: an optional action the
   customer may take now that the booking exists. So they get one chip
   geometry and sit on one wrapping row, and the only element that stays on
   its own line is the primary CTA, which is the one thing that is not
   optional. `min-width` is what makes the row read as a set rather than as
   four accidents — the widest label sets the measure and the rest match it.

   Geometry only. Each chip keeps whatever colour its own markup gives it
   (WhatsApp green, the calendar links' card background), so nothing here
   overrides a brand or a theme token. */
.so-bw-conf-chip {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    /* 116, not a round 100: it is what the widest single-word chip in the set
       measures ("WhatsApp" plus its 15px glyph at 13px/600). Below that the
       floor stops binding on exactly one chip per group and the row goes
       uneven again — which is the state being fixed. */
    min-width: 116px;
    min-height: 38px;
    padding: 8px 14px;
    border-radius: var(--so-bw-r-8, 8px);
    font-size: 13px;
    font-weight: 600;
    line-height: 1.2;
    text-align: center;
    text-decoration: none;
    white-space: nowrap;
    cursor: pointer;
}
/* The share button is a `.so-bw-btn--secondary`, whose own rule sets a taller
   min-height and a wider padding for a primary-scale control. Beside three
   calendar chips it is not one, so the chip geometry wins — stated at the
   same (0,2,0) the button pair scores, and later in the file. */
.so-bw-btn.so-bw-conf-chip {
    min-height: 38px;
    padding: 8px 14px;
    font-size: 13px;
}
/* The row itself. `align-items: stretch` so a chip that wraps to two lines on
   a narrow phone does not leave its neighbours shorter than it. */
.so-bw-conf-actions-row {
    display: flex;
    flex-wrap: wrap;
    align-items: stretch;
    justify-content: center;
    gap: 8px;
}
/* "Add to calendar" labels the three chips that follow it, so it rides on the
   same row rather than floating above the whole set. It drops to its own line
   with them when the row wraps. */
.so-bw-conf-actions-label {
    display: inline-flex;
    align-items: center;
    min-height: 38px;
    font-size: 13px;
    font-weight: 600;
    color: var(--so-bw-text-muted, var(--so-bw-text-secondary, #6b7280));
}
/* And the CTA gets a floor wide enough that it never reads as narrower than
   the row of chips above it. It is the widest label on the screen anyway;
   this only matters once a translation shortens it. */
#so-booking-widget .so-bw-confirmation > .so-bw-btn--primary {
    min-width: 220px;
}
/* On a phone the row wraps to two chips a line, and shrink-to-fit widths then
   leave every line a different length with ragged ends — the same fault one
   tier down. Growing them shares each line equally instead, so a wrapped set
   still reads as a grid. Desktop keeps intrinsic widths: stretching four chips
   across 900px would turn a set of small actions into four banners.
   Container width, not viewport — a 420px floating panel on a desktop wraps
   exactly like a phone (see _wireContainerWidth). */
#so-booking-widget:where([data-cw~="480"]) .so-bw-conf-chip {
    flex: 1 1 116px;
    max-width: 100%;
}

.so-bw-confirmation-detail {
    display: flex;
    /* baseline, not centre: once a value wraps to two lines, centring it
       against a one-line label knocks the pair out of alignment. */
    align-items: baseline;
    justify-content: space-between;
    padding: 10px 20px;
    gap: 4px 12px;
    /* Long values (a multi-service list, a full street address, a
       booking reference with a suffix) take their own line rather than
       squeezing the label to nothing. */
    flex-wrap: wrap;
}

.so-bw-confirmation-detail + .so-bw-confirmation-detail {
    border-top: 1px solid var(--so-bw-border);
}

/* THE LABEL THAT SET ITSELF ONE CHARACTER PER LINE.
   On the confirmation — the last screen a customer ever sees — "Service"
   rendered as S/e/r/v/i/c/e down a 9px column, and "Date & Time" as nine
   stacked characters, on most of the nineteen templates. Found by looking at
   a screenshot; no geometric check could see it, because nothing overflowed
   and nothing clipped. The text simply wrapped, one letter at a time.

   The cause is the three declarations below working together, and it is worth
   stating precisely because it is counter-intuitive:

     · the row is `flex-wrap: wrap`, so a long value is SUPPOSED to drop to
       its own line rather than squeeze the label;
     · but flexbox only wraps when the items cannot fit at their MINIMUM
       widths, and `overflow-wrap: anywhere` sets a text item's minimum to a
       single character;
     · so the label could always "fit" — at 9px — and the row never wrapped.

   `overflow-wrap: anywhere` was added to stop a long label overflowing. It
   did that, by making the label infinitely squeezable, which is the more
   damaging of the two failures.

   These labels are short, fixed strings the widget writes itself — Service,
   Staff, Location, Date & Time, Your timezone, Status. Refusing to break them
   restores a real minimum, so the row wraps the way it was always meant to
   and the value takes its own line. Long CUSTOMER text lives in the value,
   which still wraps freely. */
.so-bw-confirmation-detail-label {
    font-size: 13px;
    font-weight: 600;
    color: var(--so-bw-text-secondary);
    flex: 0 1 auto;
    min-width: min-content;
    overflow-wrap: break-word;
    line-height: 1.4;
}

.so-bw-confirmation-detail-value {
    font-size: 14px;
    font-weight: 500;
    color: var(--so-bw-text);
    /* Takes the remaining width and wraps within it. text-align: right keeps
       the value column reading down the right edge while it fits on one line,
       and reads naturally from the left once it wraps to its own row. */
    flex: 1 1 auto;
    min-width: 0;
    text-align: right;
    overflow-wrap: anywhere;
    word-break: break-word;
    line-height: 1.4;
}

/* ==========================================================================
   19. Alpine.js Transitions
   ========================================================================== */

.so-bw-step[x-transition] {
    transition: opacity 200ms ease, transform 200ms ease;
}

/* ==========================================================================
   20. Responsive: Tablet (>= 560px)
   ========================================================================== */

/* was @media (min-width: 560px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    /* .so-bw-card-grid intentionally omitted -- its base rule is
       container-driven (auto-fill), so a viewport breakpoint must not
       force a column count the pane isn't wide enough for. */

    #so-booking-widget:where([data-cwmin~="560"]) .so-bw-time-grid{
        grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
    }


/* Intermediate tablet range (481px - 767px) - between mobile snap and
   desktop snap. The layout already gets cards-in-2-col at ≥560px from
   the rule above, but icon and header sizes were jumping straight from
   their mobile values to the desktop values. This block introduces a
   middle tier so 481-767px gets icons / header padding / glyph sizes
   that read between the two extremes -- avoids the "feels too dense
   for tablet, too sparse for phone" feeling on iPads / mid-size
   browsers. Token-only so every template inherits automatically. */
/* was @media (min-width: 481px) and (max-width: 767px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    #so-booking-widget:where([data-cwmin~="481"][data-cw~="767"]){
        --so-bw-icon-size: 44px;
        --so-bw-icon-svg-size: 26px;
        --so-bw-icon-text-size: 17px;
        --so-bw-header-pad-y-top: 20px;
        /* No `--so-bw-header-pad-x` here. It used to say 32px, against a step
           and a nav that were both at 20px in this tier — the header's own
           HORIZONTAL inset is owned by `.so-bw-container` and bound to the
           gutter (see the design-system container block), which is the only
           element the gutter override actually lives on. A value set here, on
           `#so-booking-widget`, is shadowed by that nearer declaration
           anyway; it was rendering only because the container binding did not
           exist yet. Vertical padding stays a tier's business. */
        --so-bw-header-pad-y-bottom: 14px;
        --so-bw-header-label-size: 17px;
        --so-bw-step-pad: 20px;
        --so-bw-card-pad: 14px;
        --so-bw-card-gap: 14px;
    }


/* ==========================================================================
   21. Responsive: Desktop (>= 768px)
   ========================================================================== */

/* was @media (min-width: 768px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    #so-booking-widget:where([data-cwmin~="768"]) .so-bw-container{
        max-width: 720px;
    }

    /* Card columns are container-driven (see the base .so-bw-card-grid
       rule) -- a wide viewport does not mean a wide card pane. */


/* was @media (min-width: 960px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    #so-booking-widget:where([data-cwmin~="960"]) .so-bw-container{
        max-width: 800px;
    }

    /* Location and Staff cards carry less text, so they may pack tighter
       -- but still only when their own container has the room. */
    /* Through the token, not a literal. This tier exists to let the Location
       and Staff steps pack tighter than the service catalogue, and 220px was
       tighter than the 260px base it was written against. The base is 180px
       now, so a literal 220 here would make these two steps LOOSER than the
       rest — the opposite of the rule's purpose — and would pin them while
       every other grid follows the container. Same track expression as the
       base; the tier keeps only its own selector. */
    #so-booking-widget:where([data-cwmin~="960"]) .so-bw-step:nth-child(1) .so-bw-card-grid,
    #so-booking-widget:where([data-cwmin~="960"]) .so-bw-step:nth-child(4) .so-bw-card-grid{
        grid-template-columns: repeat(auto-fill, minmax(min(100%, var(--so-bw-card-min)), 1fr));
    }

    #so-booking-widget:where([data-cwmin~="960"]) .so-bw-time-grid{
        grid-template-columns: repeat(auto-fill, minmax(95px, 1fr));
    }


/* ==========================================================================
   22. Responsive: Mobile (<= 480px)
   ========================================================================== */

/* was @media (max-width: 480px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    /* Edge-to-edge on mobile so the widget feels native to the
       viewport. Container border / shadow gone, --so-bw-radius-xl
       reset to 0 cascades to the bubble / classic per-template
       container radii too. */
    #so-booking-widget:where([data-cw~="480"]) .so-bw-container{
        border-radius: 0;
        border-left: none;
        border-right: none;
        box-shadow: none;
    }

    /* Mobile structure tokens — applied in this section so the base
       components pick them up. Per-template mobile fine-tuning lives
       in section 12b (AFTER per-template overrides) so equal-specificity
       per-template rules don't win the cascade against mobile fixes. */
    #so-booking-widget:where([data-cw~="480"]){
        --so-bw-radius-xl: 0;
        --so-bw-header-pad-y-top: 16px;
        /* No `--so-bw-header-pad-x` here either — 22px against a 16px step and
           nav. Same reason as the tablet tier above. */
        --so-bw-header-pad-y-bottom: 12px;
        --so-bw-header-label-size: 16px;
        --so-bw-step-pad: 16px;
        --so-bw-step-margin: 0;
        --so-bw-card-pad: 14px;
        --so-bw-card-gap: 12px;
        --so-bw-icon-size: 40px;
        --so-bw-icon-svg-size: 24px;
        --so-bw-icon-text-size: 16px;
        /* Form / time-slot / calendar / summary mobile tightening.
           Stays comfortably tappable (44px target) but trims the
           horizontal padding to fit phone widths. iOS auto-zooms
           inputs whose font-size is below 16px on focus, so input
           font goes UP to 16px specifically on mobile. */
        --so-bw-input-pad-y: 12px;
        --so-bw-input-pad-x: 12px;
        --so-bw-input-font-size: 16px;
        --so-bw-slot-pad-y: 12px;
        --so-bw-slot-pad-x: 8px;
        --so-bw-slot-font-size: 13px;
        --so-bw-slot-gap: 6px;
        --so-bw-calendar-day-font-size: 13px;
        --so-bw-calendar-nav-size: 40px;
        --so-bw-summary-pad-y: 10px;
        --so-bw-summary-pad-x: 16px;
        /* Timezone select font bumped to 16px on mobile so iOS
           Safari doesn't trigger the auto-zoom behaviour on focus.
           Same guard as --so-bw-input-font-size above. */
        --so-bw-tz-select-font-size: 16px;
    }

    /* Bubble's header has its own explicit border-radius (24/24/0/0)
       so we collapse that too, otherwise we'd get a coloured wedge
       above the now-flat container edge. */
    #so-booking-widget:where([data-cw~="480"])[data-template="bubble"] .so-bw-header{
        border-radius: 0;
    }

    #so-booking-widget:where([data-cw~="480"]) .so-bw-card-grid{
        grid-template-columns: 1fr;
    }

    #so-booking-widget:where([data-cw~="480"]) .so-bw-form-row{
        grid-template-columns: 1fr;
        gap: 0;
    }

    #so-booking-widget:where([data-cw~="480"]) .so-bw-time-grid{
        grid-template-columns: repeat(3, 1fr);
    }

    /* On phones (≤480px) the slot panel is full-width but narrow.
       Reduce from 3 cols (set at ≤600px) back to 2 cols so
       time labels don't truncate. */
    #so-booking-widget:where([data-cw~="480"]) .so-bw-slot-list{
        grid-template-columns: repeat(2, 1fr);
    }

    #so-booking-widget:where([data-cw~="480"]) .so-bw-nav{
        padding: 12px 16px;
    }
    /* Responsive footer: on phones the Back + Continue/Confirm buttons
       share the row as equal halves so a long label (e.g. "Confirm
       Booking") can never overflow / get clipped off the right edge.
       `min-width:0` lets them shrink below their content width; the
       empty step-1 spacer keeps flex:0 so a lone Continue fills the row. */
    #so-booking-widget:where([data-cw~="480"]) .so-bw-nav .so-bw-btn{
        flex: 1 1 0;
        min-width: 0;
    }

    /* Per-template mobile fine-tuning is in section 12b (AFTER all
       per-template desktop overrides) so it wins on equal specificity
       via later source order. Adding per-template mobile rules here
       would lose to the desktop per-template overrides further down. */

    #so-booking-widget:where([data-cw~="480"]) .so-bw-btn--confirm{
        /* Horizontal-only so Confirm keeps the same height as Back on
           mobile too (no font-size / vertical-padding bump). */
        padding-left: 16px;
        padding-right: 16px;
    }

    #so-booking-widget:where([data-cw~="480"]) .so-bw-confirmation{
        padding: 24px 16px;
    }

    #so-booking-widget:where([data-cw~="480"]) .so-bw-confirmation-title{
        font-size: 20px;
    }

    #so-booking-widget:where([data-cw~="480"]) .so-bw-summary-row{
        flex-direction: column;
        align-items: flex-start;
        gap: 2px;
        padding: 10px 16px;
    }

    #so-booking-widget:where([data-cw~="480"]) .so-bw-summary-label{
        flex-basis: auto;
        max-width: 100%;
        font-size: 12px;
    }

    #so-booking-widget:where([data-cw~="480"]) .so-bw-summary-value{
        text-align: left;
        font-size: 14px;
        font-weight: 600;
    }

    /* Total row stays horizontal on mobile (label left / price right)
       since "Total: $X" reads cleaner inline than stacked. */
    #so-booking-widget:where([data-cw~="480"]) .so-bw-summary-row--total{
        flex-direction: row;
        align-items: baseline;
        gap: 12px;
        padding: 14px 16px;
    }
    #so-booking-widget:where([data-cw~="480"]) .so-bw-summary-row--total .so-bw-summary-value{
        text-align: right;
        font-size: 16px;
    }

    /* Section title needs to align with the new summary horizontal
       padding on mobile (--so-bw-summary-pad-x = 16px). */
    #so-booking-widget:where([data-cw~="480"]) .so-bw-summary-section-title{
        padding: 10px 16px;
        font-size: 10.5px;
    }

    /* Tighten policies card on mobile so it doesn't dwarf the
       calendar / summary cards. */
    #so-booking-widget:where([data-cw~="480"]) .so-bw-policies{
        padding: 12px 14px;
    }
    #so-booking-widget:where([data-cw~="480"]) .so-bw-policies-item{
        font-size: 12.5px;
    }

    /* Calendar / slot grid pick up phone-sized columns from the
       clamp() rules already in section 12/13 — only carry the
       per-template tweaks below this point. The clamp() floors
       (74px slot, ~32px day cell) keep tap targets ≥ 38px on a
       360px viewport without needing per-breakpoint overrides. */

    #so-booking-widget:where([data-cw~="480"]) .so-bw-error-content{
        font-size: 13px;
    }

    /* The 1-line clamp that used to sit here is gone with the base clamp —
       on the narrowest cards it was the most destructive of all, leaving a
       220px stub of every description. Cards are content-adaptive at every
       width; a phone just gets more lines. */
    /* Card title: avoid hyphenation artifacts (CSS auto-hyphen can
       break "Consultation" into "Consul-tation" mid-word). Allow word
       wrap but not auto-hyphens. */
    #so-booking-widget:where([data-cw~="480"]) .so-bw-card-title{
        hyphens: none;
        overflow-wrap: break-word;
    }


/* ==========================================================================
   23. Print Styles
   ========================================================================== */

@media print {
    .so-bw-nav,
    .so-bw-progress,
    .so-bw-hold-timer {
        display: none !important;
    }

    .so-bw-container {
        box-shadow: none;
        border: 1px solid #ccc;
    }
}

/* ==========================================================================
   24. Reduced Motion
   ========================================================================== */

@media (prefers-reduced-motion: reduce) {
    .so-bw-spinner {
        animation-duration: 1.5s;
    }

    .so-bw-progress-bar {
        transition: none;
    }

    .so-bw-card,
    .so-bw-time-slot,
    .so-bw-calendar-day,
    .so-bw-btn,
    .so-bw-input {
        transition: none;
    }
}

/* ==========================================================================
   25. High-Contrast Mode
   ========================================================================== */

@media (forced-colors: active) {
    .so-bw-card--selected {
        outline: 3px solid Highlight;
    }

    .so-bw-time-slot--selected {
        outline: 2px solid Highlight;
    }

    .so-bw-calendar-day--selected {
        outline: 2px solid Highlight;
    }

    .so-bw-btn--primary {
        border: 2px solid ButtonText;
    }
}

/* ==========================================================================
   11b. BASE SPACING — restored above the universal reset
   ==========================================================================

   `#so-booking-widget *, ::before, ::after { margin: 0; padding: 0 }` at the
   top of this file is specificity (1,0,0). A bare component rule like
   `.so-bw-policies { padding: 14px 16px }` is (0,1,0) and therefore LOSES to
   it. The combobox block further up already documents the trap ("prefixed
   with #so-booking-widget so they out-specify the reset"), but the prefix was
   only ever applied ad hoc, so 54 component rules silently computed to zero:

     - the Service Policies card had no padding at all, so its heading sat
       outside the panel it was supposed to be inside and its rows ran
       together with no separation — this is what was reported;
     - `.so-bw-step-desc`'s 20px margin-bottom was gone, so the step
       description was flush against whatever followed it;
     - `.so-bw-slot-panel`, `.so-bw-slot-panel-head`, `.so-bw-summary-row`,
       `.so-bw-confirmation`, `.so-bw-confirmation-details`, `.so-bw-rail-row`,
       the policy-panel cells and `.so-bw-badge` had no padding either.

   The reset itself is deliberately left alone. It exists to neutralise the
   host THEME's defaults on arbitrary elements inside the widget, and a plugin
   cannot predict which elements a theme will style; narrowing it to a tag list
   would reopen that hole.

   So the intent is re-declared here at (1,1,0), which clears the reset, and
   the block is positioned deliberately:

     AFTER  every base section (1-11 plus the responsive, print and contrast
            passes), so it wins over the dead bare rules it revives — the
            values are identical, so nothing moves that was already correct;
     BEFORE section 12 and everything after it, so all nineteen per-template
            treatments, the design system, the architectures and the mobile
            overrides keep overriding spacing exactly as they did.

   The @media blocks at the end restate the responsive spacing for the same
   reason: those overrides are bare classes too, so without them at (1,1,0)
   this block would out-rank them and freeze phone spacing at desktop values.

   Values are copied verbatim from the rules they revive — this changes what
   REACHES the page, never what the design asks for. Verified by measuring
   computed padding/margin on every element in this list across all nineteen
   layouts at two widths on all nine steps, before and after: every change is
   a zero becoming the declared value, and nothing else moved.

   Give any new component rule the `#so-booking-widget` prefix and none of
   this is needed again.
   ────────────────────────────────────────────────────────────────────────── */

#so-booking-widget .so-bw-step-indicator {
    margin-bottom: 12px;
}
#so-booking-widget .so-bw-error-content {
    padding: 12px 16px;
    margin-top: 16px;
}
#so-booking-widget .so-bw-step {
    padding: var(--so-bw-step-pad);
    margin: var(--so-bw-step-margin);
}
#so-booking-widget .so-bw-step-desc {
    margin-bottom: 20px;
}
#so-booking-widget .so-bw-unavailable svg {
    margin-top: 2px;
}
#so-booking-widget .so-bw-unavailable strong {
    margin-bottom: 4px;
}
#so-booking-widget .so-bw-card-extras-count {
    padding: 0 5px;
    margin-left: 4px;
}
#so-booking-widget .so-bw-card-extra-row {
    padding: 9px 12px;
}
#so-booking-widget .so-bw-card-title {
    margin-bottom: 2px;
}
#so-booking-widget .so-bw-card-meta {
    margin-bottom: 4px;
}
#so-booking-widget .so-bw-card-badges {
    margin-top: 8px;
}
#so-booking-widget .so-bw-badge {
    padding: 2px 8px;
}
#so-booking-widget .so-bw-service-group {
    margin-bottom: 20px;
}
#so-booking-widget .so-bw-service-group:last-child {
    margin-bottom: 0;
}
#so-booking-widget .so-bw-datetime {
    margin-bottom: 16px;
}
#so-booking-widget .so-bw-calendar-header {
    margin-bottom: clamp(6px, calc(var(--so-bw-cw, 100vw) * 0.008), 12px);
}
#so-booking-widget .so-bw-slot-back {
    padding: 2px 0;
    margin-bottom: 12px;
}
#so-booking-widget .so-bw-slot-panel {
    padding: clamp(10px, calc(var(--so-bw-cw, 100vw) * 0.012), 14px) clamp(12px, calc(var(--so-bw-cw, 100vw) * 0.013999999999999999), 16px);
}
#so-booking-widget .so-bw-slot-panel::-webkit-scrollbar-track {
    margin: 8px 0;
}
#so-booking-widget .so-bw-slot-panel-title {
    margin: 0 0 4px;
    padding-bottom: 4px;
}
#so-booking-widget .so-bw-slot-panel-head {
    margin: calc(-1 * clamp(10px, calc(var(--so-bw-cw, 100vw) * 0.012), 14px)) calc(-1 * clamp(12px, calc(var(--so-bw-cw, 100vw) * 0.013999999999999999), 16px)) 0;
    padding: clamp(10px, calc(var(--so-bw-cw, 100vw) * 0.012), 14px) clamp(12px, calc(var(--so-bw-cw, 100vw) * 0.013999999999999999), 16px) 8px;
}
#so-booking-widget .so-bw-tz-hint {
    margin: 2px 0 6px;
}
#so-booking-widget .so-bw-slot-group-title {
    margin-bottom: 2px;
}
#so-booking-widget .so-bw-required {
    margin-left: 2px;
}
#so-booking-widget .so-bw-powered {
    padding: 10px 24px 14px;
}
#so-booking-widget .so-bw-summary-section-title {
    padding: 10px var(--so-bw-summary-pad-x);
}
#so-booking-widget .so-bw-policies {
    padding: 14px 16px;
}
#so-booking-widget .so-bw-policies--inline {
    margin: 0 0 16px;
}
#so-booking-widget .so-bw-policies--review {
    margin: 16px 0 0;
}
#so-booking-widget .so-bw-policies-title {
    margin-bottom: 12px;
}
#so-booking-widget .so-bw-policies-group {
    margin-bottom: 12px;
}
#so-booking-widget .so-bw-policies-group:last-child {
    margin-bottom: 0;
}
#so-booking-widget .so-bw-policies-group-title {
    margin-bottom: 4px;
}
#so-booking-widget .so-bw-policies-items {
    margin: 0;
    padding: 0;
}
#so-booking-widget .so-bw-policies-item {
    margin-bottom: 4px;
}
#so-booking-widget .so-bw-policies-item:last-child {
    margin-bottom: 0;
}
#so-booking-widget .so-bw-summary-row {
    padding: var(--so-bw-summary-pad-y) var(--so-bw-summary-pad-x);
}
#so-booking-widget .so-bw-summary-row--total {
    padding: 14px var(--so-bw-summary-pad-x);
}
#so-booking-widget .so-bw-summary-tz {
    margin-left: 2px;
}
#so-booking-widget .so-bw-policy-panel-cell {
    padding: 8px 12px;
}
#so-booking-widget .so-bw-policy-panel-field-info {
    padding: 8px 12px;
}
#so-booking-widget .so-bw-policy-panel-value {
    margin: 2px 0 0;
}
#so-booking-widget .so-bw-policy-panel-input {
    padding: 8px 12px;
}
#so-booking-widget .so-bw-policy-panel-total {
    padding: 10px 12px;
}
#so-booking-widget .so-bw-availability-banner svg {
    margin-top: 2px;
}
#so-booking-widget .so-bw-approval-banner svg {
    margin-top: 2px;
}
#so-booking-widget .so-bw-approval-banner p {
    margin: 4px 0 0;
}
#so-booking-widget .so-bw-recurring-header {
    padding: 14px 16px;
}
#so-booking-widget .so-bw-recurring-subtitle {
    margin: 2px 0 0;
}
#so-booking-widget .so-bw-recurring-freq {
    padding: 12px 14px;
}
#so-booking-widget .so-bw-recurring-day {
    padding: 0 6px;
}
#so-booking-widget .so-bw-recurring-end-count {
    padding: 6px 8px;
}
#so-booking-widget .so-bw-recurring-end-date {
    padding: 6px 10px;
}
#so-booking-widget .so-bw-recurring-preview-head {
    margin-bottom: 8px;
}
#so-booking-widget .so-bw-recurring-skipped-title {
    margin: 0 0 4px;
}
#so-booking-widget .so-bw-confirmation {
    padding: 32px 24px;
}
#so-booking-widget .so-bw-confirmation-icon {
    margin-bottom: 20px;
}
#so-booking-widget .so-bw-confirmation-title {
    margin-bottom: 8px;
}
#so-booking-widget .so-bw-confirmation-details {
    padding: 4px 0;
    margin-bottom: 24px;
}
#so-booking-widget .so-bw-confirmation-detail {
    padding: 10px 20px;
}
#so-booking-widget .so-bw-rail-head {
    margin-bottom: 20px;
}
#so-booking-widget .so-bw-rail-sub {
    margin-top: 6px;
}
#so-booking-widget .so-bw-rail-row {
    padding: 10px 0;
}
#so-booking-widget .so-bw-rail-total {
    padding-top: 16px;
    margin-top: 10px;
}
#so-booking-widget .so-bw-card-grid--scroll {
    padding-right: 6px;
}
#so-booking-widget .so-bw-stepbar {
    margin: 4px 0 16px;
    padding-bottom: 2px;
}

/* was @media (max-width: 480px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    #so-booking-widget:where([data-cw~="480"]) .so-bw-card-extra-row{
        padding: 8px 10px;
    }
    #so-booking-widget:where([data-cw~="480"]) .so-bw-confirmation{
        padding: 24px 16px;
    }
    #so-booking-widget:where([data-cw~="480"]) .so-bw-summary-row{
        padding: 10px 16px;
    }
    #so-booking-widget:where([data-cw~="480"]) .so-bw-summary-row--total{
        padding: 14px 16px;
    }
    #so-booking-widget:where([data-cw~="480"]) .so-bw-summary-section-title{
        padding: 10px 16px;
    }
    #so-booking-widget:where([data-cw~="480"]) .so-bw-policies{
        padding: 12px 14px;
    }


/* was @media (max-width:680px) — now gated on the CONTAINER width
   via data-cw (see booking-widget.js _wireContainerWidth). :where()
   keeps specificity identical to the media query it replaces. */

    #so-booking-widget:where([data-cw~="680"]) .so-bw-rail-head{
        margin-bottom: 0;
    }
    #so-booking-widget:where([data-cw~="680"]) .so-bw-rail-row{
        padding: 4px 0;
    }
    #so-booking-widget:where([data-cw~="680"]) .so-bw-rail-total{
        padding-top: 0;
        margin-top: 0;
    }


/* ==========================================================================
   12. Template Variations
   ==========================================================================
   Per-template visual treatments keyed off the wrapper's data-template
   attribute, set by the shortcode from the Studio's saved value. Each
   block tweaks the key personality knobs (header style, corner
   radii, density, surface colors) of the default "minimal" base so
   that picking a card in the Studio actually changes how the public
   widget looks. The functional structure is unchanged — just the
   chrome.
*/

/* --- Minimal: polished neutral-canvas identity -------------------- */
/* The default styling rendered before this block was very plain, so
   the six templates that map to baseLayout=minimal (Modern Minimal,
   Spa Mint, Zen Stone, Tutor Amber, SaaS Electric, Design Coral)
   landed on the public page looking generic. This block gives the
   minimal layout its own personality: a subtle floating shadow on
   the container, a primary-tinted top border, refined card lift on
   hover, and a soft accent on the selected card. The colour token
   `--so-bw-primary` is already set per template by the shortcode,
   so each minimal-base template renders with its own brand hue. */
#so-booking-widget[data-template="minimal"] .so-bw-container {
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04), 0 8px 24px rgba(0, 0, 0, 0.04);
    border: 1px solid rgba(0, 0, 0, 0.06);
    /* The thin band at the top picks up the saved primary so each
       minimal-base template (Spa Mint, Tutor Amber, Design Coral, …)
       carries a faint colour signature without overwhelming the
       neutral canvas the layout is meant to project. */
    border-top: 3px solid var(--so-bw-primary);
}
#so-booking-widget[data-template="minimal"] .so-bw-header {
    padding-bottom: 18px;
}
#so-booking-widget[data-template="minimal"] .so-bw-card {
    transition: border-color 180ms ease, transform 180ms ease, box-shadow 180ms ease;
}
#so-booking-widget[data-template="minimal"] .so-bw-card:hover:not(.so-bw-card--selected) {
    transform: translateY(-1px);
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
    border-color: var(--so-bw-primary-light);
}

/* --- Gradient: vivid header, larger card emphasis ----------------- */
#so-booking-widget[data-template="gradient"] .so-bw-container {
    background: linear-gradient(135deg, var(--so-bw-primary) 0%, var(--so-bw-primary-hover) 100%) padding-box, #fff;
    background-clip: padding-box;
    color: #fff;
    --so-bw-header-border: rgba(255, 255, 255, 0.18);
}
#so-booking-widget[data-template="gradient"] .so-bw-header {
    background: transparent;
    border-bottom: 1px solid rgba(255, 255, 255, 0.2);
    color: #fff;
}
#so-booking-widget[data-template="gradient"] .so-bw-header .so-bw-step-label,
#so-booking-widget[data-template="gradient"] .so-bw-header .so-bw-step-count {
    color: rgba(255, 255, 255, 0.95);
}
/* Gradient identity: white card-on-gradient inner step. Border radii
   now match Modern Minimal across container / card / button so the
   5 gradient-base templates (Bold Gradient, Workshop Orange, Resort
   Aqua, Digital Magenta, Agency Purple) feel like their minimal
   counterpart with the gradient hero header. The inner step keeps
   its 16px radius - that's the gradient's lifted-card identity, not
   a corner that the customer sees against the body bg. Mobile
   collapses step margin to 0 (handled in section 12b). */
#so-booking-widget[data-template="gradient"] .so-bw-container {
    --so-bw-step-bg: #fff;
    /* Inner step box renders as a sharp rectangle (no curve) so
       the body reads as a clean shape against the gradient hero
       background. Was 16px rounded - the user wanted shape, not
       curve. Step margin still creates the visible gradient
       border around the white box. */
    --so-bw-step-radius: 0;
    --so-bw-step-margin: 16px;
    --so-bw-card-radius: var(--so-bw-radius-lg);
    --so-bw-btn-radius: var(--so-bw-radius);
}
#so-booking-widget[data-template="gradient"] .so-bw-step {
    color: var(--so-bw-text);
}
#so-booking-widget[data-template="gradient"] .so-bw-card {
    transition: border-color 200ms, transform 150ms;
}
#so-booking-widget[data-template="gradient"] .so-bw-card.is-selected {
    border-color: var(--so-bw-primary);
    transform: translateY(-2px);
}

/* --- Classic: sharper edges, formal form-style fields ------------- */
#so-booking-widget[data-template="classic"] .so-bw-container {
    --so-bw-radius: var(--so-bw-r-4);
    --so-bw-radius-sm: var(--so-bw-r-2);
    --so-bw-radius-lg: var(--so-bw-r-6);
    /* The container itself reads from --so-bw-radius-xl, not the
       smaller tokens above. Without this override the outer corners
       stayed at 16px while the classic header (border-radius: 0)
       and bottom nav rendered square — the 16px rounded container
       then clipped the red header and nav at all four corners,
       producing the visible mismatch the user reported. */
    --so-bw-radius-xl: var(--so-bw-r-4);
    border-radius: var(--so-bw-r-4);
    border: 1px solid #d1d5db;
    box-shadow: none;
}
#so-booking-widget[data-template="classic"] .so-bw-header {
    background: var(--so-bw-primary);
    color: #fff;
    border-radius: 0;
}
#so-booking-widget[data-template="classic"] .so-bw-header .so-bw-step-label,
#so-booking-widget[data-template="classic"] .so-bw-header .so-bw-step-count {
    color: rgba(255, 255, 255, 0.95);
    font-weight: 600;
}
/* Colored-header templates (classic / bubble / gradient / dark)
   paint the header in a strong colour. Any visible progress-bar
   contrast at small fill widths (e.g. 20% on Step 1 of 5) reads
   as a tab below the active step's title because the bar's filled
   portion contrasts with the unfilled portion against the colored
   header. The "Step N of M" text in the same row already conveys
   progress, so we hide the bar entirely on these templates -
   the colored header now reads as one solid uniform band with no
   secondary indicator strip below. Light-header templates (minimal
   / studio / compact) keep the visible progress bar on their
   gray bg-secondary header where it doesn't create a tab artifact. */
#so-booking-widget[data-template="classic"] .so-bw-progress,
#so-booking-widget[data-template="gradient"] .so-bw-progress,
#so-booking-widget[data-template="dark"] .so-bw-progress,
#so-booking-widget[data-template="bubble"] .so-bw-progress {
    display: none;
}
/* And remove the 12px gap the step-indicator reserved for the now-
   hidden progress bar so the colored header doesn't have extra
   empty space below the title. Bubble removed from this list since
   it now uses a light header where the progress bar reads cleanly
   (same as minimal / studio / compact). */
#so-booking-widget[data-template="classic"] .so-bw-step-indicator,
#so-booking-widget[data-template="gradient"] .so-bw-step-indicator,
#so-booking-widget[data-template="dark"] .so-bw-step-indicator {
    margin-bottom: 0;
}
#so-booking-widget[data-template="classic"] .so-bw-card {
    border-color: #d1d5db;
}
/* Classic identity: sharp 2px corners, uppercase header + buttons.
   All tokens — no raw padding/font overrides. */
#so-booking-widget[data-template="classic"] .so-bw-container {
    --so-bw-btn-radius: var(--so-bw-r-2);
    --so-bw-btn-text-transform: uppercase;
    --so-bw-btn-letter-spacing: 0.04em;
    --so-bw-btn-font-size: 13px;
    --so-bw-card-radius: var(--so-bw-r-2);
    --so-bw-header-text-transform: uppercase;
    --so-bw-header-letter-spacing: 0.05em;
    /* Transparent header divider on coloured headers - the rose /
       red / blue / dark band flows clean into the step content
       without a 1px gray hairline cutting across it. */
    --so-bw-header-border: transparent;
}

/* --- Dark: full dark surface -------------------------------------
   Comprehensive token override: every shared token that defaults to
   a light-theme value gets a dark equivalent here. Components that
   bind to these tokens (cards, time slots, inputs, calendar nav,
   summary, bottom nav, checkboxes, card meta, empty states,
   confirmation text, default icon tiles, borders) automatically
   inherit dark surfaces and contrast-appropriate text via the token
   cascade -- one block, every dependent rule. Light-theme templates
   continue to use the :root defaults since their --so-bw-text is set
   to a dark colour by the inline shortcode and white card-bg reads
   correctly. */
#so-booking-widget[data-template="dark"] .so-bw-container {
    --so-bw-card-bg: #1e293b;
    --so-bw-card-bg-hover: #273449;
    --so-bw-text-secondary: #cbd5e1;
    --so-bw-text-tertiary: #94a3b8;
    --so-bw-bg-tertiary: #334155;
    --so-bw-border: #334155;
    /* Dark header and dark body are both #0f172a — keep a subtle
       lightened divider so the step content visually separates
       from the header without the harsh #e5e7eb default. */
    --so-bw-header-border: rgba(255, 255, 255, 0.06);
}
#so-booking-widget[data-template="dark"] .so-bw-container,
#so-booking-widget[data-template="dark"] .so-bw-step,
#so-booking-widget[data-template="dark"] .so-bw-header {
    background: #0f172a;
    color: #f9fafb;
    border-color: #1e293b;
}
#so-booking-widget[data-template="dark"] .so-bw-card {
    /* background now driven by --so-bw-card-bg token override above */
    border-color: #334155;
    color: #f9fafb;
}
#so-booking-widget[data-template="dark"] .so-bw-card-title { color: #f9fafb; }
#so-booking-widget[data-template="dark"] .so-bw-card-meta { color: #94a3b8; }
#so-booking-widget[data-template="dark"] .so-bw-step-desc,
#so-booking-widget[data-template="dark"] .so-bw-step-label,
#so-booking-widget[data-template="dark"] .so-bw-step-count {
    color: #cbd5e1;
}
#so-booking-widget[data-template="dark"] .so-bw-calendar-day {
    color: #e2e8f0;
}

/* --- Bubble: very rounded, chat-style softness --------------------
   Restyled to take its header + body cues from Design Coral (minimal
   layout): light header (gray bg-secondary, dark text), primary-
   tinted top border on the container, and body content sitting on
   the clean tinted body without a coloured pill for the step
   description. The bubble identity is preserved through the heavier
   24px corners, soft floating shadow, and pill-shaped buttons --
   the chat-style softness comes from shape, not from a coloured
   header. Affects Bubble/Chat, Fitness Red, Boutique Rose, Nail
   Bar, Cafe Coffee, Art Violet (the 6 templates that map to
   data-template="bubble"). */
#so-booking-widget[data-template="bubble"] .so-bw-container {
    /* Border radii now match Modern Minimal across container /
       card / button so bubble-base templates feel like their
       minimal counterpart with each template's own brand colour
       and tinted body. Container = 16px (--so-bw-radius-xl
       default), cards = 12px (--so-bw-radius-lg), buttons = 8px
       (--so-bw-radius). Identity preserved via the soft floating
       shadow and tinted body bg. */
    --so-bw-radius-xl: var(--so-bw-r-16);
    --so-bw-card-radius: var(--so-bw-radius-lg);
    --so-bw-btn-radius: var(--so-bw-radius);
    --so-bw-header-border: var(--so-bw-border);
    border-radius: var(--so-bw-r-16);
    box-shadow: 0 12px 40px rgba(0, 0, 0, 0.08);
    border: 1px solid rgba(0, 0, 0, 0.06);
}
#so-booking-widget[data-template="bubble"] .so-bw-header {
    /* Header top corners match container's 16px - clean rounded
       edge, content has the same breathing room as minimal. */
    border-radius: var(--so-bw-r-16) var(--so-bw-r-16) 0 0;
}
#so-booking-widget[data-template="bubble"] .so-bw-card:hover:not(.so-bw-card--selected) {
    /* Was primary-light, which collided with .so-bw-card-icon--avatar's
       own primary-light background — on hover the "Any Available"
       icon tile blended into the card and looked like the icon had
       disappeared. card-bg-hover is a near-white shade that gives
       the avatar tile contrast against the hovered card, and the
       primary border still signals interactivity. */
    background: var(--so-bw-card-bg-hover);
    border-color: var(--so-bw-primary);
}
/* Bubble identity tokens already set on the .so-bw-container rule
   above (16px corners aligned with Modern Minimal). This duplicate
   block previously redeclared --so-bw-btn-radius: 999px and
   --so-bw-card-radius: 20px which competed with the unified
   minimal-aligned values; now empty so the cascade resolves to
   the canonical block. */
#so-booking-widget[data-template="bubble"] .so-bw-step-desc {
    /* Step description on bubble templates now reads as plain
       secondary text matching the minimal/Design Coral reference
       layout. Earlier iterations used a primary-light pill or a
       white-with-primary-border pill, both of which read as
       extra chrome on top of the body's tinted bg. Plain text is
       cleaner and lets the cards below carry the visual weight. */
    color: var(--so-bw-text-secondary);
}

/* --- Compact: tighter spacing, smaller type ---------------------
   All structural deltas expressed as design-system tokens — the
   base .so-bw-header / .so-bw-step / .so-bw-card / .so-bw-btn
   rules read these and adapt automatically. Mobile section 12b
   bumps button pad-y to 10px while keeping the dense font-size
   as the layout's identity signal. */
#so-booking-widget[data-template="compact"] .so-bw-container {
    font-size: 13px;
    --so-bw-btn-pad-y: 7px;
    --so-bw-btn-pad-x: 14px;
    --so-bw-btn-font-size: 12px;
    --so-bw-header-pad-y-top: 18px;
    --so-bw-header-pad-x: 28px;
    --so-bw-header-pad-y-bottom: 12px;
    --so-bw-step-pad: 14px 16px;
    --so-bw-step-gap: 8px;
    --so-bw-card-pad: 10px 12px;
    --so-bw-card-title-size: 13px;
    --so-bw-card-meta-size: 11px;
}

/* --- Studio: magazine-style, generous spacing, bold typography ----
   Used by 5 of the 30 templates (Studio Booking, Luxe Gold, Office
   Charcoal, Restaurant Burgundy, Photo Mono). Was previously two
   thin rules — beefed up so the "showcase imagery + bold typography"
   tagline actually shows through. */
#so-booking-widget[data-template="studio"] .so-bw-container {
    box-shadow: 0 6px 24px rgba(0, 0, 0, 0.06);
    border: 1px solid #f1f5f9;
}
/* Studio identity: gradient header bg + 6px card corners. Tokens. */
#so-booking-widget[data-template="studio"] .so-bw-container {
    --so-bw-header-pad-y-top: 22px;
    --so-bw-header-pad-x: 32px;
    --so-bw-header-letter-spacing: -0.01em;
    --so-bw-card-radius: var(--so-bw-r-6);
}
#so-booking-widget[data-template="studio"] .so-bw-header {
    background: linear-gradient(0deg, #fafafa, #fff);
    border-bottom: 1px solid #f1f5f9;
}
#so-booking-widget[data-template="studio"] .so-bw-card {
    transition: border-color 200ms, box-shadow 200ms;
}
#so-booking-widget[data-template="studio"] .so-bw-card:hover {
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
    border-color: var(--so-bw-primary);
}
#so-booking-widget[data-template="studio"] .so-bw-card-title {
    font-weight: 700;
    font-size: 15px;
    letter-spacing: -0.01em;
}

/* Studio DOM variant: horizontal-scroll cards with vertical card
   layout. Replicates the admin Studio preview's "horizontal staff
   row" idiom -- cards become 160px-wide flex items in an overflow-x
   scroller, with the avatar tile on top and name centered below.
   Bio meta hidden in this compact-card mode (the editorial layout
   prefers visual identity over text density). The selected check
   icon repositions to the top-right corner of the card. */
#so-booking-widget[data-template="studio"] .so-bw-card-grid {
    display: flex;
    grid-template-columns: none;
    overflow-x: auto;
    gap: 12px;
    padding: 0 0 8px;
    scrollbar-width: thin;
    scrollbar-color: var(--so-bw-border) transparent;
}
#so-booking-widget[data-template="studio"] .so-bw-card-grid::-webkit-scrollbar {
    height: 6px;
}
#so-booking-widget[data-template="studio"] .so-bw-card-grid::-webkit-scrollbar-thumb {
    background: var(--so-bw-border);
    border-radius: var(--so-bw-r-3);
}
#so-booking-widget[data-template="studio"] .so-bw-card {
    flex: 0 0 160px;
    flex-direction: column;
    align-items: center;
    text-align: center;
    padding: 14px 12px;
    gap: 10px;
}
#so-booking-widget[data-template="studio"] .so-bw-card-icon {
    width: 64px;
    height: 64px;
    border-radius: var(--so-bw-r-14);
    min-width: 64px;
    min-height: 64px;
    max-width: 64px;
    max-height: 64px;
}
#so-booking-widget[data-template="studio"] .so-bw-card-body {
    width: 100%;
    text-align: center;
    flex: 0 0 auto;
    min-width: 0;
}
#so-booking-widget[data-template="studio"] .so-bw-card-meta {
    display: none;
}
#so-booking-widget[data-template="studio"] .so-bw-card-check,
#so-booking-widget[data-template="studio"] .so-bw-card-checkbox {
    position: absolute;
    top: 8px;
    right: 8px;
    width: 24px;
    height: 24px;
    align-self: auto;
}

/* ── DOM signatures for the remaining 9 base layouts ─────────────
   Every template now has a visibly distinct grid / card / hover
   treatment in addition to its colour palette + corner radius +
   surface tokens. */

/* MINIMAL: lift on hover with primary-tinted shadow. Already has
   the 3px primary top border on the container. Add a subtle
   scale/translate on the card so the click-target lifts. */
#so-booking-widget[data-template="minimal"] .so-bw-card {
    transition: transform 200ms ease, box-shadow 200ms ease, border-color 200ms ease;
}
#so-booking-widget[data-template="minimal"] .so-bw-card:hover:not(.so-bw-card--selected) {
    transform: translateY(-2px);
    box-shadow: 0 8px 16px -4px rgba(0, 0, 0, 0.06);
}

/* GRADIENT: cards scale slightly on hover with primary-tinted
   shadow that picks up the gradient brand colour. */
#so-booking-widget[data-template="gradient"] .so-bw-card:hover:not(.so-bw-card--selected) {
    transform: scale(1.02);
    box-shadow: 0 6px 16px -4px rgba(var(--so-bw-primary-rgb), 0.25);
}

/* CLASSIC: vertical 1-col list. Cards become full-width table-row
   style with shared 1px borders, no rounded corners, primary-light
   background + primary left rail on the selected row. Reads like
   a clinical / formal contact form. */
#so-booking-widget[data-template="classic"] .so-bw-card-grid {
    grid-template-columns: 1fr;
    gap: 0;
}
#so-booking-widget[data-template="classic"] .so-bw-card {
    border-radius: 0;
    border-left: none;
    border-right: none;
    border-top: none;
    border-bottom: 1px solid #e5e7eb;
    box-shadow: none;
    transition: background 150ms ease, border-color 150ms ease;
}
#so-booking-widget[data-template="classic"] .so-bw-card:first-child {
    border-top: 1px solid #e5e7eb;
}
#so-booking-widget[data-template="classic"] .so-bw-card--selected {
    background: var(--so-bw-primary-light);
    border-left: 3px solid var(--so-bw-primary);
    padding-left: 13px; /* compensate for the 3px border so content doesn't shift */
}

/* DARK: tighter card grid with subtle primary glow on hover.
   Cards already dark-on-dark with primary border in the dark
   colour block; this adds the interaction signal. */
#so-booking-widget[data-template="dark"] .so-bw-card-grid {
    gap: 10px;
}
#so-booking-widget[data-template="dark"] .so-bw-card:hover:not(.so-bw-card--selected) {
    box-shadow: 0 0 16px rgba(var(--so-bw-primary-rgb), 0.25);
    border-color: var(--so-bw-primary);
}

/* BUBBLE: chat-bubble shape — asymmetric corner radius mimics a
   speech bubble (sharp bottom-left tail). The card-icon stays
   circular so the avatar still reads as a person. */
#so-booking-widget[data-template="bubble"] .so-bw-card {
    border-radius: var(--so-bw-r-18) var(--so-bw-r-18) var(--so-bw-r-18) var(--so-bw-r-4);
}
#so-booking-widget[data-template="bubble"] .so-bw-card:nth-child(even) {
    border-radius: var(--so-bw-r-18) var(--so-bw-r-18) var(--so-bw-r-4) var(--so-bw-r-18); /* mirrored tail for alternating cards */
}

/* COMPACT: 3-col grid at desktop, 2-col at tablet, 1-col mobile.
   Combined with compact's already-dense padding, this fits more
   choices in view at once - the layout's identity. */
#so-booking-widget[data-template="compact"] .so-bw-card-grid {
    grid-template-columns: repeat(3, 1fr);
}
/* was @media (max-width:768px) — now gated on the CONTAINER width
   via data-cw (see booking-widget.js _wireContainerWidth). :where()
   keeps specificity identical to the media query it replaces. */

    #so-booking-widget:where([data-cw~="768"])[data-template="compact"] .so-bw-card-grid{
        grid-template-columns: repeat(2, 1fr);
    }

/* was @media (max-width: 480px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    #so-booking-widget:where([data-cw~="480"])[data-template="compact"] .so-bw-card-grid{
        grid-template-columns: 1fr;
    }


/* ── Calendar + time-slot DOM signatures per base layout ────────────
   Same visual-distinction approach as cards: each template gets
   unique calendar-day shapes, calendar-nav buttons, and time-slot
   chrome so the date/time step looks visibly different across
   templates -- not just a colour swap. */

/* MINIMAL: round day pills with subtle hover lift; oval time slots
   .so-bw-slot-row shares the same border-radius treatment as
   .so-bw-time-slot — both render slot buttons for this template. */
#so-booking-widget[data-template="minimal"] .so-bw-calendar-day {
    border-radius: 50%;
}
#so-booking-widget[data-template="minimal"] .so-bw-time-slot,
#so-booking-widget[data-template="minimal"] .so-bw-slot-row {
    border-radius: 999px;
}
#so-booking-widget[data-template="minimal"] .so-bw-time-slot:hover,
#so-booking-widget[data-template="minimal"] .so-bw-slot-row:hover {
    transform: translateY(-1px);
}

/* GRADIENT: pill-shaped time slots with gradient hover */
#so-booking-widget[data-template="gradient"] .so-bw-time-slot,
#so-booking-widget[data-template="gradient"] .so-bw-slot-row {
    border-radius: 999px;
    border-width: 2px;
}
#so-booking-widget[data-template="gradient"] .so-bw-time-slot--selected,
#so-booking-widget[data-template="gradient"] .so-bw-slot-row--selected {
    background: linear-gradient(135deg, var(--so-bw-primary) 0%, var(--so-bw-primary-hover) 100%);
}
#so-booking-widget[data-template="gradient"] .so-bw-calendar-day--selected {
    background: linear-gradient(135deg, var(--so-bw-primary) 0%, var(--so-bw-primary-hover) 100%) !important;
}

/* CLASSIC: sharp 0-radius squares for both calendar days and time
   slots, formal form-style with double-border on selected */
#so-booking-widget[data-template="classic"] .so-bw-calendar-day {
    border-radius: 0;
    border: 1px solid transparent;
}
#so-booking-widget[data-template="classic"] .so-bw-calendar-day--available:hover {
    border-color: var(--so-bw-primary);
}
#so-booking-widget[data-template="classic"] .so-bw-time-slot,
#so-booking-widget[data-template="classic"] .so-bw-slot-row {
    border-radius: 0;
    border-width: 1px;
}
#so-booking-widget[data-template="classic"] .so-bw-time-slot--selected,
#so-booking-widget[data-template="classic"] .so-bw-slot-row--selected {
    box-shadow: inset 0 0 0 2px var(--so-bw-primary);
}
#so-booking-widget[data-template="classic"] .so-bw-calendar-nav {
    border-radius: 0;
}

/* DARK: full dark calendar + time-slot styling so the date/time
   step on Salon Noir / Dev Neon / Dark Mode keeps the dark
   aesthetic established by the staff step. Selected states use
   the primary as background with dark text - works for the
   gold (#facc15), cyan (#22d3ee), and lavender (#a78bfa)
   primaries since all three have luminance >0.5 -> dark text
   reads cleanly on the saturated fill. */
#so-booking-widget[data-template="dark"] .so-bw-calendar-month {
    color: #f9fafb;
}
#so-booking-widget[data-template="dark"] .so-bw-calendar-weekdays span {
    color: #94a3b8;
}
#so-booking-widget[data-template="dark"] .so-bw-calendar-day {
    border-radius: var(--so-bw-r-4);
    color: #f9fafb;
}
#so-booking-widget[data-template="dark"] .so-bw-calendar-day--available:hover {
    background: rgba(var(--so-bw-primary-rgb), 0.18);
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}
#so-booking-widget[data-template="dark"] .so-bw-calendar-day--disabled {
    color: #475569;
    opacity: 0.5;
}
#so-booking-widget[data-template="dark"] .so-bw-calendar-day--selected {
    background: var(--so-bw-primary) !important;
    color: #0a0a0f !important;
    box-shadow: 0 0 16px rgba(var(--so-bw-primary-rgb), 0.7);
    font-weight: 700;
}
#so-booking-widget[data-template="dark"] .so-bw-calendar-day--today::after {
    background: var(--so-bw-primary);
}
#so-booking-widget[data-template="dark"] .so-bw-calendar-nav {
    background: #1e293b;
    border-color: #334155;
    color: #cbd5e1;
}
#so-booking-widget[data-template="dark"] .so-bw-calendar-nav:hover:not(:disabled) {
    background: #273449;
    border-color: var(--so-bw-primary);
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}
#so-booking-widget[data-template="dark"] .so-bw-timeslots-title {
    color: #f9fafb;
}
#so-booking-widget[data-template="dark"] .so-bw-time-slot,
#so-booking-widget[data-template="dark"] .so-bw-slot-row {
    border-radius: var(--so-bw-r-4);
    border-color: #334155;
    color: #f9fafb;
}
#so-booking-widget[data-template="dark"] .so-bw-time-slot:hover,
#so-booking-widget[data-template="dark"] .so-bw-slot-row:hover {
    background: rgba(var(--so-bw-primary-rgb), 0.12) !important;
    border-color: var(--so-bw-primary);
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
    box-shadow: 0 0 16px rgba(var(--so-bw-primary-rgb), 0.4);
}
#so-booking-widget[data-template="dark"] .so-bw-time-slot--selected,
#so-booking-widget[data-template="dark"] .so-bw-slot-row--selected {
    background: var(--so-bw-primary) !important;
    border-color: var(--so-bw-primary);
    color: #0a0a0f !important;
    box-shadow: 0 0 20px rgba(var(--so-bw-primary-rgb), 0.6);
    font-weight: 700;
}
#so-booking-widget[data-template="dark"] .so-bw-empty {
    color: #94a3b8;
}
#so-booking-widget[data-template="dark"] .so-bw-unavailable {
    background: rgba(239, 68, 68, 0.15);
    border-color: rgba(239, 68, 68, 0.4);
    color: #fca5a5;
}
#so-booking-widget[data-template="dark"] .so-bw-unavailable strong {
    color: #fecaca;
}
#so-booking-widget[data-template="dark"] .so-bw-unavailable-hint {
    color: #fca5a5 !important;
}

/* BUBBLE: very rounded everything (chat-app feel). Time slots are
   pills, calendar days are circles, calendar nav buttons circular */
#so-booking-widget[data-template="bubble"] .so-bw-calendar-day {
    border-radius: 50%;
}
#so-booking-widget[data-template="bubble"] .so-bw-calendar-nav {
    border-radius: 50%;
}
#so-booking-widget[data-template="bubble"] .so-bw-time-slot,
#so-booking-widget[data-template="bubble"] .so-bw-slot-row {
    border-radius: 999px;
}

/* COMPACT: tight 4-col time-slot grid for density signal */
#so-booking-widget[data-template="compact"] .so-bw-time-grid {
    grid-template-columns: repeat(4, 1fr);
    gap: 6px;
}
/* was @media (max-width:768px) — now gated on the CONTAINER width
   via data-cw (see booking-widget.js _wireContainerWidth). :where()
   keeps specificity identical to the media query it replaces. */

    #so-booking-widget:where([data-cw~="768"])[data-template="compact"] .so-bw-time-grid{
        grid-template-columns: repeat(3, 1fr);
    }

#so-booking-widget[data-template="compact"] .so-bw-time-slot,
#so-booking-widget[data-template="compact"] .so-bw-slot-row {
    padding: 6px 4px;
    font-size: 11px;
    border-radius: var(--so-bw-r-4);
}
/* Compact slot-list: 3 columns to honour the dense identity */
#so-booking-widget[data-template="compact"] .so-bw-slot-list {
    grid-template-columns: repeat(3, 1fr);
    gap: 6px;
}
#so-booking-widget[data-template="compact"] .so-bw-calendar-day {
    font-size: 11px;
    border-radius: var(--so-bw-r-4);
}

/* STUDIO: 3-col time-slot grid with bold square slots, calendar
   days as small rounded squares */
#so-booking-widget[data-template="studio"] .so-bw-time-grid {
    grid-template-columns: repeat(3, 1fr);
    gap: 8px;
}
#so-booking-widget[data-template="studio"] .so-bw-time-slot,
#so-booking-widget[data-template="studio"] .so-bw-slot-row {
    border-radius: var(--so-bw-r-12);
    border-width: 1.5px;
    font-weight: 600;
    padding: 12px 8px;
}
#so-booking-widget[data-template="studio"] .so-bw-time-slot--selected,
#so-booking-widget[data-template="studio"] .so-bw-slot-row--selected {
    background: #111;
    border-color: #111;
}
#so-booking-widget[data-template="studio"] .so-bw-calendar-day {
    border-radius: var(--so-bw-r-10);
    font-weight: 600;
}
#so-booking-widget[data-template="studio"] .so-bw-calendar-day--selected {
    background: #111 !important;
}

/* ── Per-template-id distinctive signatures ───────────────────────
   These rules key off [data-template-id="<id>"] so individual
   themed templates can have visually unique signatures beyond
   their base-layout treatment. Useful for differentiating
   templates that share a layout (e.g. cafe-coffee vs art-violet
   both use `bubble`, but their data-template-id values let each
   have a unique decoration). The shortcode emits both
   data-template (layout) and data-template-id (template name),
   so layout-level rules still apply alongside these.
*/

/* SALON NOIR: gold-on-near-black with luxe inset border accents */

/* CAFE COFFEE: warm coffee-bean accent ring on cards */

/* FITNESS RED: high-energy diagonal stripe header */

/* ART VIOLET: softer corners + violet underline accent */

/* BOUTIQUE ROSE: scalloped corner via clip-path on cards */

/* NAIL BAR: orange wave accent on header */

/* DEV NEON: terminal monospace + pixel grid background */

/* OFFICE CHARCOAL: editorial top-rule + serif drop-cap on step desc */

/* PHOTO MONO: pure-black accent ring on selected, no colour */

/* RESTAURANT BURGUNDY: gold inset accent on cards */

/* LUXE GOLD: rich gold accents, cream cards, strong decoration */

/* ZEN STONE: very minimal, soft warm gray accents */

/* CLINIC BLUE: medical-form clean white with blue subtle */

/* ACADEMY NAVY: institutional bold serif headers */

/* ──────────────────────────────────────────────────────────────────
   PREVIEW-MIRROR LAYOUTS — each base layout below renders the public
   page mirroring its admin Widget Studio preview HTML structure.
   Templates inherit their layout's preview-mirror block + their own
   brand colour through the inline shortcode CSS-variable cascade.
   The 11 base layouts cover all 38 themed templates.
   ────────────────────────────────────────────────────────────────── */

/* GRADIENT preview-mirror: gradient hero header (linear-gradient
   135deg) + 3-step indicator pills + bordered cards + gradient
   continue bar. Applies to Bold Gradient, Workshop Orange, Resort
   Aqua, Digital Magenta, Agency Purple. */
#so-booking-widget[data-template="gradient"] .so-bw-container {
    background: #ffffff !important;
    border: 1px solid #e5e7eb !important;
    border-radius: var(--so-bw-r-12) !important;
    box-shadow: none !important;
    overflow: hidden !important;
}
#so-booking-widget[data-template="gradient"] .so-bw-header {
    background: linear-gradient(135deg, var(--so-bw-primary) 0%, var(--so-bw-primary-hover) 100%) !important;
    color: #ffffff !important;
    border-bottom: none !important;
    border-radius: 0 !important;
    padding: 24px !important;
    margin: 0 !important;
}
#so-booking-widget[data-template="gradient"] .so-bw-step-indicator {
    flex-direction: column !important;
    align-items: flex-start !important;
    gap: 4px !important;
    margin-bottom: 0 !important;
}
#so-booking-widget[data-template="gradient"] .so-bw-step-label {
    color: #ffffff !important;
    font-size: 20px !important;
    font-weight: 800 !important;
    text-align: left !important;
}
#so-booking-widget[data-template="gradient"] .so-bw-step-count {
    color: rgba(255, 255, 255, 0.85) !important;
    font-size: 12px !important;
    text-align: left !important;
}
#so-booking-widget[data-template="gradient"] .so-bw-progress {
    display: none !important;
}
#so-booking-widget[data-template="gradient"] .so-bw-step {
    background: #ffffff !important;
    margin: 0 !important;
    border-radius: 0 !important;
    padding: 20px !important;
}
#so-booking-widget[data-template="gradient"] .so-bw-card-grid {
    grid-template-columns: 1fr !important;
    gap: 8px !important;
}
#so-booking-widget[data-template="gradient"] .so-bw-card {
    flex-direction: row !important;
    align-items: center !important;
    /* The only template with a card border thicker than the 1px default, so
       it has to restate the width the selected/hover overlay pulls itself out
       by. Left at 1px the overlay would sit a pixel inside the border box and
       the corner arc it exists to close would come back at half width. */
    --so-bw-card-border-width: 2px;
    border: 2px solid #f3f4f6 !important;
    border-radius: var(--so-bw-r-14) !important;
    padding: 14px !important;
    background: #ffffff !important;
    gap: 12px !important;
    transform: none !important;
}
#so-booking-widget[data-template="gradient"] .so-bw-card:hover:not(.so-bw-card--selected) {
    border-color: var(--so-bw-primary) !important;
    transform: none !important;
}
#so-booking-widget[data-template="gradient"] .so-bw-card--selected {
    background: var(--so-bw-primary-light) !important;
    border-color: var(--so-bw-primary) !important;
}
#so-booking-widget[data-template="gradient"] .so-bw-card-icon {
    width: 40px !important;
    height: 40px !important;
    min-width: 40px !important;
    min-height: 40px !important;
    max-width: 40px !important;
    max-height: 40px !important;
    border-radius: var(--so-bw-r-10) !important;
    background: linear-gradient(135deg, var(--so-bw-primary) 0%, var(--so-bw-primary-hover) 100%) !important;
    /* Service and location icon SVGs inherit their per-entity color
       (service-colored or primary-colored) via currentColor — on a
       colored gradient tile those strokes can be nearly invisible.
       Staff icons are always white via their inline span style. Pin
       all icon glyphs to white so every entity type reads at the
       same weight on the gradient surface. */
    color: #ffffff !important;
}
#so-booking-widget[data-template="gradient"] .so-bw-card-title {
    font-size: 13px !important;
    font-weight: 700 !important;
}
#so-booking-widget[data-template="gradient"] .so-bw-card-meta {
    font-size: 11px !important;
    color: #6b7280 !important;
}
#so-booking-widget[data-template="gradient"] .so-bw-card-check {
    width: 18px !important;
    height: 18px !important;
    border-radius: 50% !important;
}
#so-booking-widget[data-template="gradient"] .so-bw-btn--primary,
#so-booking-widget[data-template="gradient"] .so-bw-btn--secondary {
    background: linear-gradient(135deg, var(--so-bw-primary) 0%, var(--so-bw-primary-hover) 100%) !important;
    color: #ffffff !important;
    border: none !important;
    border-radius: var(--so-bw-r-12) !important;
    font-weight: 700 !important;
    padding: 10px 18px !important;
    font-size: 14px !important;
}

/* CLASSIC preview-mirror: gray background, solid primary header,
   form-style stacked fields with UPPERCASE labels above each input,
   sharp 2px-4px corners, formal "SUBMIT" button. Applies to
   Classic Form, Clinic Blue, Academy Navy. */
#so-booking-widget[data-template="classic"] .so-bw-container {
    background: #f9fafb !important;
    border: 1px solid #d1d5db !important;
    border-radius: var(--so-bw-r-4) !important;
    box-shadow: none !important;
}
#so-booking-widget[data-template="classic"] .so-bw-header {
    background: var(--so-bw-primary) !important;
    color: #ffffff !important;
    border-bottom: none !important;
    border-radius: 0 !important;
    padding: 18px 24px !important;
    margin: 0 !important;
}
#so-booking-widget[data-template="classic"] .so-bw-step-indicator {
    flex-direction: column !important;
    align-items: flex-start !important;
    gap: 2px !important;
    margin-bottom: 0 !important;
}
#so-booking-widget[data-template="classic"] .so-bw-step-label {
    color: #ffffff !important;
    font-size: 16px !important;
    font-weight: 700 !important;
    text-align: left !important;
    text-transform: none !important;
    letter-spacing: 0 !important;
}
#so-booking-widget[data-template="classic"] .so-bw-step-count {
    color: rgba(255, 255, 255, 0.85) !important;
    font-size: 11px !important;
    text-align: left !important;
}
#so-booking-widget[data-template="classic"] .so-bw-progress {
    display: none !important;
}
#so-booking-widget[data-template="classic"] .so-bw-step {
    padding: 20px !important;
}
#so-booking-widget[data-template="classic"] .so-bw-card-grid {
    grid-template-columns: 1fr !important;
    gap: 0 !important;
}
#so-booking-widget[data-template="classic"] .so-bw-card {
    flex-direction: row !important;
    align-items: center !important;
    background: #ffffff !important;
    border: 1px solid #d1d5db !important;
    border-radius: var(--so-bw-r-2) !important;
    padding: 10px !important;
    margin-bottom: 12px !important;
    gap: 10px !important;
    box-shadow: none !important;
    transform: none !important;
}
#so-booking-widget[data-template="classic"] .so-bw-card-title {
    font-size: 12px !important;
    font-weight: 600 !important;
    text-transform: uppercase !important;
    letter-spacing: 0.04em !important;
}
#so-booking-widget[data-template="classic"] .so-bw-card-meta {
    font-size: 11px !important;
}
#so-booking-widget[data-template="classic"] .so-bw-input,
#so-booking-widget[data-template="classic"] .so-bw-textarea,
#so-booking-widget[data-template="classic"] .so-bw-select {
    border: 1px solid #d1d5db !important;
    border-radius: var(--so-bw-r-2) !important;
    padding: 8px 10px !important;
    font-size: 12px !important;
    background: #ffffff !important;
}
#so-booking-widget[data-template="classic"] .so-bw-label {
    font-size: 11px !important;
    font-weight: 600 !important;
    text-transform: uppercase !important;
    letter-spacing: 0.05em !important;
    color: #374151 !important;
}
#so-booking-widget[data-template="classic"] .so-bw-btn--primary,
#so-booking-widget[data-template="classic"] .so-bw-btn--secondary {
    background: var(--so-bw-primary) !important;
    color: #ffffff !important;
    border-radius: var(--so-bw-r-2) !important;
    text-transform: uppercase !important;
    letter-spacing: 0.04em !important;
    font-weight: 600 !important;
    font-size: 13px !important;
    padding: 10px 18px !important;
}

/* DARK preview-mirror: deep navy canvas, top section with icon +
   title + subtitle, 2-col card grid with primary-fill on selected,
   bottom section with "SELECT TIME SLOT" mini-header + 3-col tight
   slot grid + solid primary "Confirm Booking" CTA. Applies to
   Dark Mode, Salon Noir, Dev Neon. */
#so-booking-widget[data-template="dark"] .so-bw-container {
    background: #0f172a !important;
    border: 1px solid #1e293b !important;
    border-radius: var(--so-bw-r-12) !important;
    overflow: hidden !important;
    box-shadow: none !important;
}
#so-booking-widget[data-template="dark"] .so-bw-header {
    background: #0f172a !important;
    border-bottom: 1px solid #1e293b !important;
    padding: 20px !important;
    margin: 0 !important;
    border-radius: 0 !important;
}
#so-booking-widget[data-template="dark"] .so-bw-step-label {
    color: #ffffff !important;
    font-size: 15px !important;
    font-weight: 700 !important;
}
#so-booking-widget[data-template="dark"] .so-bw-step-count {
    color: #64748b !important;
    font-size: 11px !important;
}
#so-booking-widget[data-template="dark"] .so-bw-step {
    background: #0f172a !important;
    padding: 20px !important;
}
#so-booking-widget[data-template="dark"] .so-bw-card-grid {
    grid-template-columns: repeat(2, 1fr) !important;
    gap: 8px !important;
}
#so-booking-widget[data-template="dark"] .so-bw-card {
    flex-direction: column !important;
    align-items: flex-start !important;
    background: #1e293b !important;
    border: 1px solid #334155 !important;
    border-radius: var(--so-bw-r-8) !important;
    padding: 12px !important;
    color: #e2e8f0 !important;
    gap: 4px !important;
}
#so-booking-widget[data-template="dark"] .so-bw-card--selected {
    background: var(--so-bw-primary) !important;
    border-color: var(--so-bw-primary) !important;
    color: #ffffff !important;
}
#so-booking-widget[data-template="dark"] .so-bw-card--selected .so-bw-card-title,
#so-booking-widget[data-template="dark"] .so-bw-card--selected .so-bw-card-meta {
    color: #ffffff !important;
}
#so-booking-widget[data-template="dark"] .so-bw-card-icon {
    display: none !important;
}
#so-booking-widget[data-template="dark"] .so-bw-card-title {
    font-size: 11px !important;
    font-weight: 700 !important;
    color: #e2e8f0 !important;
}
#so-booking-widget[data-template="dark"] .so-bw-card-meta {
    font-size: 10px !important;
    color: #64748b !important;
}
#so-booking-widget[data-template="dark"] .so-bw-timeslots-title {
    color: #94a3b8 !important;
    font-size: 11px !important;
    font-weight: 600 !important;
    text-transform: uppercase !important;
    letter-spacing: 0.05em !important;
}
#so-booking-widget[data-template="dark"] .so-bw-time-grid {
    grid-template-columns: repeat(3, 1fr) !important;
    gap: 6px !important;
}
#so-booking-widget[data-template="dark"] .so-bw-time-slot {
    background: #1e293b !important;
    color: #94a3b8 !important;
    border: 1px solid #334155 !important;
    border-radius: var(--so-bw-r-8) !important;
    padding: 8px !important;
    font-size: 11px !important;
}
#so-booking-widget[data-template="dark"] .so-bw-time-slot--selected {
    background: var(--so-bw-primary) !important;
    color: #ffffff !important;
    border-color: var(--so-bw-primary) !important;
}
#so-booking-widget[data-template="dark"] .so-bw-nav {
    background: #0f172a !important;
    border-top: 1px solid #1e293b !important;
}
#so-booking-widget[data-template="dark"] .so-bw-btn--primary,
#so-booking-widget[data-template="dark"] .so-bw-btn--secondary {
    background: var(--so-bw-primary) !important;
    color: #ffffff !important;
    border: none !important;
    border-radius: var(--so-bw-r-8) !important;
    font-weight: 700 !important;
    font-size: 13px !important;
    padding: 12px 18px !important;
}

/* BUBBLE preview-mirror: chat-style softness. Light gray canvas
   `#f0f2f5`, primary chat-header with bot avatar + title + status,
   white cards with chat-bubble corner radii (alternating), pill
   buttons. Applies to Bubble/Chat, Fitness Red, Boutique Rose,
   Nail Bar, Cafe Coffee, Art Violet. */
#so-booking-widget[data-template="bubble"] .so-bw-container {
    background: #f0f2f5 !important;
    border: 1px solid #e4e6eb !important;
    border-radius: var(--so-bw-r-12) !important;
    overflow: hidden !important;
    box-shadow: none !important;
}
#so-booking-widget[data-template="bubble"] .so-bw-header {
    background: var(--so-bw-primary) !important;
    color: #ffffff !important;
    border-bottom: none !important;
    /* Match the container's 12px corner radius — the previous
       border-radius: 0 left a hard-corner header inside a soft-
       corner container, which read as a misalignment artefact. */
    border-radius: var(--so-bw-r-12) var(--so-bw-r-12) 0 0 !important;
    padding: 14px 20px !important;
    margin: 0 !important;
}
#so-booking-widget[data-template="bubble"] .so-bw-step-label {
    color: #ffffff !important;
    font-size: 14px !important;
    font-weight: 700 !important;
}
#so-booking-widget[data-template="bubble"] .so-bw-step-count {
    color: rgba(255, 255, 255, 0.85) !important;
    font-size: 10px !important;
}
#so-booking-widget[data-template="bubble"] .so-bw-step {
    background: #f0f2f5 !important;
    padding: 16px !important;
}
#so-booking-widget[data-template="bubble"] .so-bw-step-desc {
    background: #ffffff !important;
    border-radius: var(--so-bw-r-12) var(--so-bw-r-12) var(--so-bw-r-12) var(--so-bw-r-2) !important;
    padding: 10px 14px !important;
    font-size: 12px !important;
    font-weight: 600 !important;
    color: #111827 !important;
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1) !important;
    display: inline-block !important;
    max-width: 320px !important;
}
#so-booking-widget[data-template="bubble"] .so-bw-card-grid {
    grid-template-columns: 1fr !important;
    gap: 10px !important;
}
#so-booking-widget[data-template="bubble"] .so-bw-card {
    background: #ffffff !important;
    border: none !important;
    border-radius: var(--so-bw-r-12) var(--so-bw-r-12) var(--so-bw-r-12) var(--so-bw-r-2) !important;
    padding: 10px 14px !important;
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1) !important;
}
/* Bubble template — keep the chat-bubble asymmetry through
   border-radius alone. The previous styling forced every even
   card to primary-fill + white text + 80% width + right-aligned,
   which mid-flow looked like the card was already SELECTED.
   Customers picking a service / staff would interpret the brown
   pill as "this is what's chosen" and skip clicking. The flipped
   border-radius below preserves the chat-bubble look without the
   misleading selection cue. */
#so-booking-widget[data-template="bubble"] .so-bw-card:nth-child(even) {
    border-radius: var(--so-bw-r-12) var(--so-bw-r-12) var(--so-bw-r-2) var(--so-bw-r-12) !important;
}
#so-booking-widget[data-template="bubble"] .so-bw-card-icon {
    width: 28px !important;
    height: 28px !important;
    min-width: 28px !important;
    min-height: 28px !important;
    max-width: 28px !important;
    max-height: 28px !important;
}
#so-booking-widget[data-template="bubble"] .so-bw-card-title {
    font-size: 12px !important;
    font-weight: 600 !important;
    color: #111827 !important;
}
#so-booking-widget[data-template="bubble"] .so-bw-card-meta {
    font-size: 11px !important;
}
#so-booking-widget[data-template="bubble"] .so-bw-nav {
    background: #ffffff !important;
    border-top: 1px solid #e4e6eb !important;
    padding: 12px !important;
}
#so-booking-widget[data-template="bubble"] .so-bw-btn--primary,
#so-booking-widget[data-template="bubble"] .so-bw-btn--secondary {
    background: var(--so-bw-primary) !important;
    color: #ffffff !important;
    border: none !important;
    border-radius: 999px !important;
    padding: 9px 16px !important;
    font-size: 12px !important;
    font-weight: 600 !important;
}

/* COMPACT preview-mirror: narrow widget feel, primary mini-header
   "Quick Book", form-style stacked uppercase mini-labels, 2-col
   tight time grid, single Book Now CTA. Applies to Compact Sidebar,
   Consultant Slate. */
#so-booking-widget[data-template="compact"] .so-bw-container {
    background: #ffffff !important;
    border: 1px solid #e5e7eb !important;
    box-shadow: none !important;
    overflow: hidden !important;
    font-size: 13px !important;
}
#so-booking-widget[data-template="compact"] .so-bw-header {
    background: var(--so-bw-primary) !important;
    color: #ffffff !important;
    border-bottom: none !important;
    border-radius: var(--so-bw-r-8) !important;
    margin: 16px 16px 0 !important;
    padding: 14px !important;
    text-align: center !important;
}
#so-booking-widget[data-template="compact"] .so-bw-step-label {
    color: #ffffff !important;
    font-size: 14px !important;
    font-weight: 700 !important;
    text-align: center !important;
}
#so-booking-widget[data-template="compact"] .so-bw-step-count {
    color: rgba(255, 255, 255, 0.85) !important;
    font-size: 10px !important;
    text-align: center !important;
}
#so-booking-widget[data-template="compact"] .so-bw-step {
    padding: 16px !important;
}
#so-booking-widget[data-template="compact"] .so-bw-step-desc {
    font-size: 10px !important;
    color: #6b7280 !important;
    margin-bottom: 5px !important;
    text-transform: uppercase !important;
    letter-spacing: 0.05em !important;
    font-weight: 600 !important;
}
#so-booking-widget[data-template="compact"] .so-bw-card-grid {
    grid-template-columns: 1fr !important;
    gap: 4px !important;
}
#so-booking-widget[data-template="compact"] .so-bw-card {
    flex-direction: row !important;
    align-items: center !important;
    border: 1px solid #e5e7eb !important;
    border-radius: var(--so-bw-r-6) !important;
    padding: 8px 10px !important;
    background: #ffffff !important;
    gap: 8px !important;
    box-shadow: none !important;
}
#so-booking-widget[data-template="compact"] .so-bw-card-icon {
    width: 24px !important;
    height: 24px !important;
    min-width: 24px !important;
    min-height: 24px !important;
    max-width: 24px !important;
    max-height: 24px !important;
}
#so-booking-widget[data-template="compact"] .so-bw-time-grid {
    grid-template-columns: 1fr 1fr !important;
    gap: 4px !important;
}
#so-booking-widget[data-template="compact"] .so-bw-time-slot {
    padding: 6px !important;
    font-size: 11px !important;
    border-radius: var(--so-bw-r-6) !important;
}

/* STUDIO preview-mirror: bold typography, fluid padding for
   responsiveness. All padding/font-size values use clamp() so they
   scale down on narrow viewports without needing per-breakpoint
   overrides - prevents content overflow that the previous fixed
   28px padding caused at <600px widths. Applies to Studio Booking,
   Luxe Gold, Office Charcoal, Restaurant Burgundy, Photo Mono. */
#so-booking-widget[data-template="studio"] .so-bw-container {
    background: #ffffff !important;
    box-shadow: 0 6px 24px rgba(0, 0, 0, 0.06) !important;
    border: 1px solid #f1f5f9 !important;
    overflow: hidden !important;
    /* Wider container so studio's editorial layout has room to
       breathe without forcing card content onto multiple lines.
       Up to 1200px on extra-wide displays, falls back to 100% of
       viewport on narrower widths. */
    max-width: min(1200px, 100%) !important;
    width: 100% !important;
    margin-left: auto !important;
    margin-right: auto !important;
}
#so-booking-widget[data-template="studio"] .so-bw-header {
    background: linear-gradient(0deg, #fafafa, #fff) !important;
    border-bottom: 1px solid #f1f5f9 !important;
    padding: clamp(16px, calc(var(--so-bw-cw, 100vw) * 0.04), 28px) !important;
    margin: 0 !important;
    border-radius: 0 !important;
}
#so-booking-widget[data-template="studio"] .so-bw-step-indicator {
    flex-direction: column !important;
    align-items: flex-start !important;
    gap: 4px !important;
    margin-bottom: 0 !important;
    width: 100% !important;
}
#so-booking-widget[data-template="studio"] .so-bw-step-label {
    font-size: clamp(15px, calc(var(--so-bw-cw, 100vw) * 0.04), 22px) !important;
    font-weight: 700 !important;
    color: #111111 !important;
    letter-spacing: -0.01em !important;
    /* "the container gives plenty of room for any normal step label" was the
       original justification for a single-line ellipsis here. It held only
       for English at the widest breakpoint: the label is clamp()-sized up to
       22px, and at 320px a translated step name lost its tail. Wraps now. */
    white-space: normal !important;
    overflow: visible !important;
    overflow-wrap: anywhere !important;
    max-width: 100% !important;
}
#so-booking-widget[data-template="studio"] .so-bw-step-count {
    font-size: clamp(11px, calc(var(--so-bw-cw, 100vw) * 0.025), 12px) !important;
    color: #6b7280 !important;
}
#so-booking-widget[data-template="studio"] .so-bw-step {
    padding: clamp(16px, calc(var(--so-bw-cw, 100vw) * 0.04), 28px) !important;
}
#so-booking-widget[data-template="studio"] .so-bw-card-grid {
    grid-template-columns: 1fr !important;
    gap: 8px !important;
}
#so-booking-widget[data-template="studio"] .so-bw-card {
    flex-direction: row !important;
    align-items: center !important;
    border: 1px solid #f1f5f9 !important;
    border-radius: var(--so-bw-r-6) !important;
    padding: clamp(10px, calc(var(--so-bw-cw, 100vw) * 0.025), 14px) !important;
    background: #ffffff !important;
    gap: clamp(10px, calc(var(--so-bw-cw, 100vw) * 0.025), 14px) !important;
    transition: border-color 200ms, box-shadow 200ms !important;
    min-width: 0 !important;
    max-width: 100% !important;
}
#so-booking-widget[data-template="studio"] .so-bw-card:hover:not(.so-bw-card--selected) {
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06) !important;
    border-color: var(--so-bw-primary) !important;
}
/* Matches the :hover rule directly above, which already tints the border with
   var(--so-bw-primary) — hovering a card showed the accent, then selecting it
   snapped to black. */
#so-booking-widget[data-template="studio"] .so-bw-card--selected {
    border-color: var(--so-bw-primary) !important;
    background: #fafafa !important;
}
#so-booking-widget[data-template="studio"] .so-bw-card-icon {
    width: clamp(40px, calc(var(--so-bw-cw, 100vw) * 0.09), 48px) !important;
    height: clamp(40px, calc(var(--so-bw-cw, 100vw) * 0.09), 48px) !important;
    min-width: clamp(40px, calc(var(--so-bw-cw, 100vw) * 0.09), 48px) !important;
    min-height: clamp(40px, calc(var(--so-bw-cw, 100vw) * 0.09), 48px) !important;
    max-width: clamp(40px, calc(var(--so-bw-cw, 100vw) * 0.09), 48px) !important;
    max-height: clamp(40px, calc(var(--so-bw-cw, 100vw) * 0.09), 48px) !important;
}
#so-booking-widget[data-template="studio"] .so-bw-card-body {
    min-width: 0 !important;
    flex: 1 1 0 !important;
    /* min-width: 0 is what lets this cell shrink; the overflow: hidden that
       accompanied it only clipped the text that shrinking was meant to wrap. */
    overflow: visible !important;
}
#so-booking-widget[data-template="studio"] .so-bw-card-title {
    font-size: clamp(13px, calc(var(--so-bw-cw, 100vw) * 0.03), 15px) !important;
    font-weight: 700 !important;
    letter-spacing: -0.01em !important;
    /* Service names are customer-facing identifiers, not decoration — a
       truncated one is indistinguishable from its siblings. Wraps. */
    white-space: normal !important;
    overflow: visible !important;
    overflow-wrap: anywhere !important;
    word-break: break-word !important;
}
#so-booking-widget[data-template="studio"] .so-bw-card-meta {
    font-size: clamp(11px, calc(var(--so-bw-cw, 100vw) * 0.025), 12px) !important;
    /* "Keeps cards a uniform compact height" was the trade this made, and it
       is the wrong one: the grid is a single column of auto-height rows, so
       uniformity buys nothing and cost every description its tail. */
    white-space: normal !important;
    overflow: visible !important;
    overflow-wrap: anywhere !important;
    -webkit-line-clamp: none !important;
}
#so-booking-widget[data-template="studio"] .so-bw-time-grid {
    grid-template-columns: repeat(auto-fit, minmax(80px, 1fr)) !important;
    gap: 8px !important;
}
#so-booking-widget[data-template="studio"] .so-bw-time-slot {
    border-radius: var(--so-bw-r-12) !important;
    border-width: 1.5px !important;
    font-weight: 600 !important;
    padding: clamp(8px, calc(var(--so-bw-cw, 100vw) * 0.02), 12px) clamp(6px, calc(var(--so-bw-cw, 100vw) * 0.02), 8px) !important;
    font-size: clamp(11px, calc(var(--so-bw-cw, 100vw) * 0.025), 13px) !important;
    min-width: 0 !important;
}
/* Same hard-coded-accent fix as the buttons below — the selected slot is
   var(--so-bw-primary) on minimal / dark / gradient / classic. */
#so-booking-widget[data-template="studio"] .so-bw-time-slot--selected {
    background: var(--so-bw-primary) !important;
    border-color: var(--so-bw-primary) !important;
    color: #ffffff !important;
}
#so-booking-widget[data-template="studio"] .so-bw-nav {
    padding: clamp(12px, calc(var(--so-bw-cw, 100vw) * 0.03), 18px) clamp(16px, calc(var(--so-bw-cw, 100vw) * 0.04), 28px) !important;
    flex-wrap: wrap !important;
    gap: 8px !important;
}
/* Accent colour comes from --so-bw-primary, like every other base layout
   (minimal / classic / dark / bubble / gradient all read the same var).
   Studio alone hard-coded #111111 !important, which outranks the
   `#so-booking-widget{--so-bw-primary:…}` block BookingShortcode emits — so
   the Studio "Button Color" picker was saved and then silently ignored, and
   all four studio-layout templates (studio-booking #8a4a6e, service-grid
   #3a6ef0, centre-stage #bb6a3a, kiosk #1ad6c8) rendered identical black
   buttons instead of their own palettes. */
#so-booking-widget[data-template="studio"] .so-bw-btn--primary,
#so-booking-widget[data-template="studio"] .so-bw-btn--secondary {
    background: var(--so-bw-primary) !important;
    color: #ffffff !important;
    border: 1px solid var(--so-bw-primary) !important;
    border-radius: 999px !important;
    padding: clamp(10px, calc(var(--so-bw-cw, 100vw) * 0.025), 14px) clamp(16px, calc(var(--so-bw-cw, 100vw) * 0.04), 28px) !important;
    font-size: clamp(12px, calc(var(--so-bw-cw, 100vw) * 0.027999999999999997), 14px) !important;
    font-weight: 700 !important;
    max-width: 100% !important;
}

/* MINIMAL preview-mirror: matches the admin Studio's `case 'minimal'`
   preview HTML exactly. Hero header (primary bg, centered title +
   subtitle), vertical 1-col card list (each row: small avatar +
   title/meta + primary "Book" pill on right), compact inline date
   picker (uppercase mini-header, 7-col tight grid), 3-col compact
   pill time slots. Applies to all 6 minimal-base templates -
   Modern Minimal, Spa Mint, Tutor Amber, Zen Stone, SaaS Electric,
   Design Coral - each with their own brand colour via the inline
   CSS variable cascade. */

/* Container: clean white, 12px corners, 1px gray border, no shadow */
#so-booking-widget[data-template="minimal"] .so-bw-container {
    background: #ffffff !important;
    border: 1px solid #e5e7eb !important;
    border-radius: var(--so-bw-r-12) !important;
    box-shadow: none !important;
    /* 3px primary top accent — the minimal template's signature
       brand cue. The previous explicit border-top: 1px override
       erased it; let the shorthand `border` rule above set the
       sides and keep the 3px primary top from the section-12
       per-template block. */
    border-top: 3px solid var(--so-bw-primary) !important;
}

/* Hero header: primary bg, centered, white text, floating with
   16px outer margin like a card on the white canvas */
#so-booking-widget[data-template="minimal"] .so-bw-header {
    background: var(--so-bw-primary) !important;
    color: #ffffff !important;
    border-bottom: none !important;
    border-radius: var(--so-bw-r-8) !important;
    margin: 16px 16px 0 !important;
    padding: 20px 24px !important;
    text-align: center !important;
}
#so-booking-widget[data-template="minimal"] .so-bw-step-indicator {
    flex-direction: column !important;
    gap: 6px !important;
    margin-bottom: 0 !important;
}
#so-booking-widget[data-template="minimal"] .so-bw-step-label {
    color: #ffffff !important;
    font-size: 20px !important;
    font-weight: 700 !important;
    text-align: center !important;
}
#so-booking-widget[data-template="minimal"] .so-bw-step-count {
    color: rgba(255, 255, 255, 0.85) !important;
    font-size: 12px !important;
    font-weight: 500 !important;
    text-align: center !important;
}
#so-booking-widget[data-template="minimal"] .so-bw-progress {
    display: none !important;
}

/* Step body: tighter padding, white surface */
#so-booking-widget[data-template="minimal"] .so-bw-step {
    padding: 16px !important;
}
#so-booking-widget[data-template="minimal"] .so-bw-step-desc {
    font-size: 12px !important;
    color: #6b7280 !important;
    margin-bottom: 12px !important;
    text-align: center !important;
}

/* Cards: vertical 1-col list, row-flex with 32px small avatar */
#so-booking-widget[data-template="minimal"] .so-bw-card-grid {
    grid-template-columns: 1fr !important;
    gap: 8px !important;
}
#so-booking-widget[data-template="minimal"] .so-bw-card {
    flex-direction: row !important;
    align-items: center !important;
    border: 1px solid #e5e7eb !important;
    border-radius: var(--so-bw-r-8) !important;
    padding: 12px !important;
    background: #ffffff !important;
    box-shadow: none !important;
    gap: 12px !important;
    transform: none !important;
}
#so-booking-widget[data-template="minimal"] .so-bw-card:hover:not(.so-bw-card--selected) {
    border-color: var(--so-bw-primary) !important;
    transform: none !important;
    box-shadow: 0 1px 3px rgba(var(--so-bw-primary-rgb), 0.1) !important;
}
#so-booking-widget[data-template="minimal"] .so-bw-card--selected {
    border-color: var(--so-bw-primary) !important;
    background: var(--so-bw-primary-light) !important;
}
#so-booking-widget[data-template="minimal"] .so-bw-card-icon {
    width: 32px !important;
    height: 32px !important;
    min-width: 32px !important;
    min-height: 32px !important;
    max-width: 32px !important;
    max-height: 32px !important;
    border-radius: var(--so-bw-r-8) !important;
}
#so-booking-widget[data-template="minimal"] .so-bw-card-icon--avatar > svg,
#so-booking-widget[data-template="minimal"] .so-bw-card-icon--avatar > span > svg {
    width: 18px !important;
    height: 18px !important;
}
#so-booking-widget[data-template="minimal"] .so-bw-card-icon--avatar > span {
    font-size: 12px !important;
}
#so-booking-widget[data-template="minimal"] .so-bw-card-title {
    font-size: 13px !important;
    font-weight: 600 !important;
    margin-bottom: 2px !important;
}
#so-booking-widget[data-template="minimal"] .so-bw-card-meta {
    font-size: 11px !important;
    color: #6b7280 !important;
}

/* Card-check → small primary pill mirroring the preview's "Book"
   button anchored right of the card. When hidden (not selected),
   use display:none instead of visibility:hidden so the pill doesn't
   consume horizontal space in the card-top row. This prevents a
   blank ~60px gap on the right side of every unselected card that
   narrowed the available body text width on mobile. */
#so-booking-widget[data-template="minimal"] .so-bw-card-check {
    background: var(--so-bw-primary) !important;
    /* Hidden state: zero footprint */
    display: none !important;
    width: auto !important;
    height: auto !important;
    border-radius: var(--so-bw-r-8) !important;
    padding: 5px 10px !important;
    font-size: 10px !important;
    font-weight: 600 !important;
    color: #ffffff !important;
    margin-left: auto !important;
}
#so-booking-widget[data-template="minimal"] .so-bw-card-check--visible {
    display: inline-flex !important;
    visibility: visible !important;
    opacity: 1 !important;
}

/* Service group title: small uppercase primary accent */
#so-booking-widget[data-template="minimal"] .so-bw-group-title {
    font-size: 11px !important;
    color: var(--so-bw-primary-ink, var(--so-bw-primary)) !important;
    text-transform: uppercase !important;
    letter-spacing: 0.05em !important;
    border-bottom: 1px solid var(--so-bw-primary-light) !important;
}

/* Calendar: compact bordered inline picker matching preview */
#so-booking-widget[data-template="minimal"] .so-bw-calendar {
    border: 1px solid #e5e7eb !important;
    border-radius: var(--so-bw-r-8) !important;
    padding: 14px !important;
    margin-bottom: 16px !important;
}
#so-booking-widget[data-template="minimal"] .so-bw-calendar-header {
    margin-bottom: 8px !important;
}
#so-booking-widget[data-template="minimal"] .so-bw-calendar-month {
    font-size: 11px !important;
    color: #6b7280 !important;
    font-weight: 600 !important;
    text-transform: uppercase !important;
    letter-spacing: 0.04em !important;
}
#so-booking-widget[data-template="minimal"] .so-bw-calendar-nav {
    width: 24px !important;
    height: 24px !important;
    border-radius: var(--so-bw-r-4) !important;
}
#so-booking-widget[data-template="minimal"] .so-bw-calendar-weekdays span {
    font-size: 9px !important;
    color: #9ca3af !important;
    padding: 2px 0 !important;
}
#so-booking-widget[data-template="minimal"] .so-bw-calendar-grid {
    gap: 4px !important;
}
#so-booking-widget[data-template="minimal"] .so-bw-calendar-day {
    font-size: 10px !important;
    padding: 4px 2px !important;
    border-radius: var(--so-bw-r-4) !important;
    color: #374151 !important;
}
#so-booking-widget[data-template="minimal"] .so-bw-calendar-day--selected {
    background: var(--so-bw-primary) !important;
    color: #ffffff !important;
    font-weight: 700 !important;
}

/* Time slots: compact 3-col pill grid */
#so-booking-widget[data-template="minimal"] .so-bw-timeslots-title {
    font-size: 11px !important;
    color: #6b7280 !important;
    text-transform: uppercase !important;
    letter-spacing: 0.04em !important;
    font-weight: 600 !important;
}
#so-booking-widget[data-template="minimal"] .so-bw-time-grid {
    grid-template-columns: repeat(3, 1fr) !important;
    gap: 6px !important;
}
#so-booking-widget[data-template="minimal"] .so-bw-time-slot {
    font-size: 11px !important;
    padding: 8px 6px !important;
    border-radius: var(--so-bw-r-6) !important;
    background: #ffffff !important;
    border: 1px solid #e5e7eb !important;
}
#so-booking-widget[data-template="minimal"] .so-bw-time-slot:hover {
    border-color: var(--so-bw-primary) !important;
    background: var(--so-bw-primary-light) !important;
}
#so-booking-widget[data-template="minimal"] .so-bw-time-slot--selected {
    background: var(--so-bw-primary) !important;
    color: #ffffff !important;
    border-color: var(--so-bw-primary) !important;
}

/* Bottom nav: subtle white strip with primary Continue */
#so-booking-widget[data-template="minimal"] .so-bw-nav {
    background: #ffffff !important;
    border-top: 1px solid #f3f4f6 !important;
    padding: 12px 16px !important;
}
#so-booking-widget[data-template="minimal"] .so-bw-btn--primary,
#so-booking-widget[data-template="minimal"] .so-bw-btn--secondary {
    background: var(--so-bw-primary) !important;
    color: #ffffff !important;
    border: 1px solid var(--so-bw-primary) !important;
    border-radius: var(--so-bw-r-8) !important;
    font-size: 13px !important;
    padding: 8px 16px !important;
    font-weight: 600 !important;
}
#so-booking-widget[data-template="minimal"] .so-bw-btn--primary:hover:not(:disabled),
#so-booking-widget[data-template="minimal"] .so-bw-btn--secondary:hover:not(:disabled) {
    background: var(--so-bw-primary-hover) !important;
    border-color: var(--so-bw-primary-hover) !important;
}

/* SAAS ELECTRIC: tech grid pattern background */

/* DESIGN CORAL: soft coral gradient body */

/* SPA MINT: soft mint gradient body */

/* WORKSHOP ORANGE: industrial accent stripe */

/* AGENCY PURPLE: creative gradient body */

/* RESORT AQUA: tropical gradient with white inner step */

/* DIGITAL MAGENTA: vibrant pink-to-purple gradient */

/* CONSULTANT SLATE: subtle vertical sidebar accent */

/* ==========================================================================
   12b. Mobile responsive overrides — per template
   ==========================================================================
   The earlier @media (max-width: 480px) block at section 22 catches the
   base layout, but per-template rules above set their own padding /
   font-size on `.so-bw-header`, `.so-bw-btn`, `.so-bw-card-title` etc.
   With equal specificity those template rules WIN on mobile due to
   source order — defeating the section-22 mobile fixes for compact /
   studio / bubble buttons. This block sits AFTER every per-template
   override so equal-specificity selectors win on later source order
   and per-template mobile behaviour is finally honoured.

   When extending: each rule here intentionally targets a property that
   a per-template rule sets — never add general mobile rules here, put
   those in section 22 instead.
*/
/* was @media (max-width: 480px) — unwrapped onto the CONTAINER width.
   A viewport media query cannot see a 420px floating panel or a 560px popup
   dialog on a 1440px desktop, so every rule below was silently skipped in a
   shell: nav buttons kept 24px of padding sized for a 700px card. :where()
   contributes no specificity, so each rule scores exactly what it did inside
   the media block, and the block stays where it was so source order — which
   several of these rules depend on to beat per-template desktop blocks — is
   unchanged. */
/* Mobile button tokens — applied AFTER per-template desktop
       overrides so they win the cascade on equal specificity.
       Every template inherits these mobile values via the design-
       system token chain — no per-template button rules needed
       here. Adjusting one knob (e.g. pad-x) automatically threads
       through all 30 templates. */
    #so-booking-widget:where([data-cw~="480"]) {
        --so-bw-btn-pad-x: 16px;
        --so-bw-btn-font-size: 14px;
    }
    /* Per-template mobile token overrides — set on .so-bw-container
       (specificity 1,2,0) which beats the section-22 widget-root
       tokens (specificity 1,0,0) where templates need template-
       specific mobile values. Since this @media block sits AFTER
       all per-template desktop blocks, equal-specificity rules win
       on later source order. */

    /* Compact: bump pad-y so the button stays tappable; tighten
       header further than the universal mobile collapse. */
    #so-booking-widget:where([data-cw~="480"])[data-template="compact"] .so-bw-container {
        --so-bw-btn-pad-y: 10px;
        --so-bw-btn-pad-x: 14px;
        --so-bw-header-pad-y-top: 14px;
        --so-bw-header-pad-x: 18px;
        --so-bw-header-pad-y-bottom: 10px;
    }
    /* Compact + minimal float their header as a pill outside the
       container with margin: 16px 16px 0. On 320px viewports that
       leaves only ~248px for a 20px bold step label and risks a
       two-line wrap inside the centered pill. Reduce to 8px to
       restore breathing room. */
    #so-booking-widget:where([data-cw~="480"])[data-template="compact"] .so-bw-header,
    #so-booking-widget:where([data-cw~="480"])[data-template="minimal"] .so-bw-header {
        margin: 8px 8px 0 !important;
    }

    /* Studio: collapse the magazine-style header padding to base
       mobile so the trailing step count doesn't crowd the corner. */
    #so-booking-widget:where([data-cw~="480"])[data-template="studio"] .so-bw-container {
        --so-bw-header-pad-y-top: 16px;
        --so-bw-header-pad-x: 18px;
        --so-bw-header-pad-y-bottom: 12px;
    }

    /* Dark templates (Dark Mode / Salon Noir / Dev Neon): the
       preview-mirror block forces .so-bw-card-grid to repeat(2,1fr)
       with !important. At 320px that gives ~130px per card which
       truncates titles/meta. Force back to single-column on mobile
       — same specificity as the preview-mirror rule but later in
       source order so it wins. */
    #so-booking-widget:where([data-cw~="480"])[data-template="dark"] .so-bw-card-grid {
        grid-template-columns: 1fr !important;
    }


/* Sticky nav overlap guard — the .so-bw-nav at the bottom of every
   step is position: sticky / bottom: 0 and ~70px tall. Without
   extra step padding the last card / slot / form row sits under
   the nav permanently.
   Specificity (1,2,0) via the [data-template] attribute selector
   matches per-template preview-mirror blocks at the same level. Since
   this block comes AFTER those rules in source order, same-specificity
   + !important wins by later source order on all templates that have
   a data-template attribute. The plain (.so-bw-container) .so-bw-step
   selector covers templates where data-template is absent. */
#so-booking-widget[data-template] .so-bw-step {
    padding-bottom: clamp(72px, calc(var(--so-bw-cw, 100vw) * 0.1), 96px) !important;
}
#so-booking-widget .so-bw-step {
    padding-bottom: clamp(72px, calc(var(--so-bw-cw, 100vw) * 0.1), 96px);
}

/* ─────────────────────────────────────────────────────────────────
   PER-LAYOUT CALENDAR STYLING
   ─────────────────────────────────────────────────────────────────
   The booking widget ships 7 base layouts (minimal / gradient /
   classic / dark / bubble / compact / studio). Earlier rules above
   only varied a couple of properties per layout — day shape and
   selected colour — so on the public site the calendar looked
   nearly identical regardless of which template the admin picked.
   This block gives each layout its OWN calendar identity: header
   typography + month label treatment, weekday strip, day-cell
   shape and hover, today marker, and selected state. Source order
   matters — these come AFTER the earlier per-template rules so
   they win conflicts; specificity is the same id-based pattern
   so no !important required.
   ───────────────────────────────────────────────────────────────── */

/* ── MINIMAL ─────────────────────────────────────────────────────
   Airy, borderless, pill-shaped days. Centred uppercase month
   label with an under-bar accent. Conveys "premium minimalism". */
#so-booking-widget[data-template="minimal"] .so-bw-calendar {
    background: var(--so-bw-card-bg);
    border-radius: var(--so-bw-r-14);
    padding: clamp(8px, calc(var(--so-bw-cw, 100vw) * 0.01), 14px);
}
#so-booking-widget[data-template="minimal"] .so-bw-calendar-month {
    font-size: 11px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.18em;
    color: var(--so-bw-text-secondary);
    border-bottom: 2px solid var(--so-bw-primary);
    padding: 2px 12px 6px;
    display: inline-block;
    flex: 0 1 auto;
    margin: 0 auto;
}
#so-booking-widget[data-template="minimal"] .so-bw-calendar-weekdays,
#so-booking-widget[data-template="minimal"] .so-bw-calendar-grid {
    border: none;
    border-radius: 0;
    overflow: visible;
}
#so-booking-widget[data-template="minimal"] .so-bw-calendar-weekdays span {
    background: transparent;
    border: none;
    text-transform: uppercase;
    letter-spacing: 0.1em;
    font-size: 10px;
    color: var(--so-bw-text-tertiary);
    padding: 6px 0 8px;
}
#so-booking-widget[data-template="minimal"] .so-bw-calendar-grid {
    gap: 2px;
}
#so-booking-widget[data-template="minimal"] .so-bw-calendar-day {
    border: none;
    background: transparent;
    border-radius: 999px;
}
#so-booking-widget[data-template="minimal"] .so-bw-calendar-day--available:hover {
    background: var(--so-bw-primary-light);
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
    transform: scale(0.94);
}
#so-booking-widget[data-template="minimal"] .so-bw-calendar-day--selected,
#so-booking-widget[data-template="minimal"] .so-bw-calendar-day--selected:hover {
    box-shadow: 0 4px 14px rgba(99, 102, 241, 0.30);
    transform: scale(1.05);
}
#so-booking-widget[data-template="minimal"] .so-bw-calendar-day--today::after {
    width: 3px;
    height: 3px;
    bottom: 4px;
}
#so-booking-widget[data-template="minimal"] .so-bw-calendar-nav {
    border: 1px solid var(--so-bw-border);
    border-radius: 50%;
}

/* ── GRADIENT ────────────────────────────────────────────────────
   Multi-stop gradient on selected day, soft pastel hover wash,
   gradient month-label background. Conveys "vibrant, fresh". */
#so-booking-widget[data-template="gradient"] .so-bw-calendar {
    background: linear-gradient(135deg, rgba(99, 102, 241, 0.04), rgba(168, 85, 247, 0.06));
    border: 1px solid rgba(168, 85, 247, 0.15);
    border-radius: var(--so-bw-r-16);
    padding: clamp(10px, calc(var(--so-bw-cw, 100vw) * 0.012), 16px);
}
#so-booking-widget[data-template="gradient"] .so-bw-calendar-month {
    background: linear-gradient(135deg, var(--so-bw-primary), #a855f7);
    -webkit-background-clip: text;
    background-clip: text;
    -webkit-text-fill-color: transparent;
    font-weight: 800;
    font-size: clamp(14px, calc(var(--so-bw-cw, 100vw) * 0.013000000000000001), 17px);
    letter-spacing: -0.02em;
}
#so-booking-widget[data-template="gradient"] .so-bw-calendar-weekdays {
    background: rgba(255,255,255,0.5);
    border-color: rgba(168, 85, 247, 0.18);
}
#so-booking-widget[data-template="gradient"] .so-bw-calendar-weekdays span {
    background: transparent;
    color: #a855f7;
    font-weight: 700;
    border-right-color: rgba(168, 85, 247, 0.12);
}
#so-booking-widget[data-template="gradient"] .so-bw-calendar-grid {
    border-color: rgba(168, 85, 247, 0.18);
}
#so-booking-widget[data-template="gradient"] .so-bw-calendar-day {
    background: rgba(255,255,255,0.65);
    border-right-color: rgba(168, 85, 247, 0.10);
    border-bottom-color: rgba(168, 85, 247, 0.10);
}
#so-booking-widget[data-template="gradient"] .so-bw-calendar-day--available:hover {
    background: linear-gradient(135deg, rgba(99, 102, 241, 0.12), rgba(168, 85, 247, 0.16));
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}
#so-booking-widget[data-template="gradient"] .so-bw-calendar-day--selected,
#so-booking-widget[data-template="gradient"] .so-bw-calendar-day--selected:hover {
    background: linear-gradient(135deg, var(--so-bw-primary), #a855f7) !important;
    box-shadow: 0 4px 14px rgba(168, 85, 247, 0.45);
}
#so-booking-widget[data-template="gradient"] .so-bw-calendar-nav {
    background: rgba(255,255,255,0.7);
    border: 1px solid rgba(168, 85, 247, 0.22);
}
#so-booking-widget[data-template="gradient"] .so-bw-calendar-nav:hover:not(:disabled) {
    background: linear-gradient(135deg, var(--so-bw-primary), #a855f7);
    color: #fff;
    border-color: transparent;
}

/* ── CLASSIC ─────────────────────────────────────────────────────
   Sharp 0-radius rectangles, thick weekday header bar, serif-ish
   bold month label. Conveys "newspaper / formal calendar". */
#so-booking-widget[data-template="classic"] .so-bw-calendar {
    border: 2px solid var(--so-bw-text);
    border-radius: 0;
    background: var(--so-bw-card-bg);
}
#so-booking-widget[data-template="classic"] .so-bw-calendar-header {
    padding: 10px 12px;
    border-bottom: 2px solid var(--so-bw-text);
    margin-bottom: 0;
    background: var(--so-bw-text);
    color: #fff;
}
#so-booking-widget[data-template="classic"] .so-bw-calendar-month {
    color: #fff;
    font-weight: 800;
    font-size: clamp(14px, calc(var(--so-bw-cw, 100vw) * 0.013000000000000001), 16px);
    text-transform: uppercase;
    letter-spacing: 0.06em;
    font-family: Georgia, 'Times New Roman', serif;
}
#so-booking-widget[data-template="classic"] .so-bw-calendar-weekdays {
    border: none;
    border-bottom: 2px solid var(--so-bw-text);
    border-radius: 0;
}
#so-booking-widget[data-template="classic"] .so-bw-calendar-weekdays span {
    background: var(--so-bw-bg-secondary);
    color: var(--so-bw-text);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    font-size: 10px;
    border-right: 1px solid var(--so-bw-text);
    padding: 8px 0;
}
#so-booking-widget[data-template="classic"] .so-bw-calendar-grid {
    border: none;
    border-radius: 0;
}
#so-booking-widget[data-template="classic"] .so-bw-calendar-day {
    border-right: 1px solid var(--so-bw-text);
    border-bottom: 1px solid var(--so-bw-text);
    border-radius: 0;
    font-family: Georgia, 'Times New Roman', serif;
    font-weight: 600;
}
#so-booking-widget[data-template="classic"] .so-bw-calendar-day--today {
    background: var(--so-bw-bg-secondary);
}
#so-booking-widget[data-template="classic"] .so-bw-calendar-day--today::after {
    display: none;
}
#so-booking-widget[data-template="classic"] .so-bw-calendar-day--selected,
#so-booking-widget[data-template="classic"] .so-bw-calendar-day--selected:hover {
    background: var(--so-bw-text) !important;
    color: #fff !important;
    box-shadow: inset 0 0 0 3px var(--so-bw-card-bg), inset 0 0 0 4px var(--so-bw-text);
}

/* ── DARK ────────────────────────────────────────────────────────
   Dark-glass canvas with neon accent ring on selected day, glowing
   today dot. Conveys "after-hours / nightclub booking". */
#so-booking-widget[data-template="dark"] .so-bw-calendar {
    background: rgba(255,255,255,0.03);
    border: 1px solid rgba(255,255,255,0.10);
    border-radius: var(--so-bw-r-14);
    padding: clamp(8px, calc(var(--so-bw-cw, 100vw) * 0.01), 14px);
    backdrop-filter: blur(10px);
    -webkit-backdrop-filter: blur(10px);
}
#so-booking-widget[data-template="dark"] .so-bw-calendar-month {
    color: #fff;
    text-shadow: 0 0 18px rgba(99, 102, 241, 0.5);
    font-weight: 700;
}
#so-booking-widget[data-template="dark"] .so-bw-calendar-day--selected,
#so-booking-widget[data-template="dark"] .so-bw-calendar-day--selected:hover {
    box-shadow: 0 0 0 2px rgba(255,255,255,0.15), 0 0 20px var(--so-bw-primary);
}
#so-booking-widget[data-template="dark"] .so-bw-calendar-day--today::after {
    background: var(--so-bw-primary);
    box-shadow: 0 0 8px var(--so-bw-primary), 0 0 16px var(--so-bw-primary);
    width: 5px;
    height: 5px;
}

/* ── BUBBLE ──────────────────────────────────────────────────────
   Big rounded circular days, bouncy scale on hover/selected,
   playful candy-pop palette. Conveys "casual / friendly". */
#so-booking-widget[data-template="bubble"] .so-bw-calendar {
    background: var(--so-bw-card-bg);
    border-radius: var(--so-bw-r-24);
    padding: clamp(10px, calc(var(--so-bw-cw, 100vw) * 0.012), 18px);
    box-shadow: inset 0 0 0 1px var(--so-bw-border);
}
#so-booking-widget[data-template="bubble"] .so-bw-calendar-month {
    background: var(--so-bw-primary-light);
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
    border-radius: 999px;
    padding: 4px 14px;
    flex: 0 1 auto;
    font-size: clamp(13px, calc(var(--so-bw-cw, 100vw) * 0.011000000000000001), 15px);
    font-weight: 700;
}
#so-booking-widget[data-template="bubble"] .so-bw-calendar-weekdays,
#so-booking-widget[data-template="bubble"] .so-bw-calendar-grid {
    border: none;
    border-radius: 0;
    background: transparent;
}
#so-booking-widget[data-template="bubble"] .so-bw-calendar-weekdays span {
    background: transparent;
    border: none;
    color: var(--so-bw-text-tertiary);
    font-size: 10px;
    text-transform: uppercase;
}
#so-booking-widget[data-template="bubble"] .so-bw-calendar-grid {
    gap: 4px;
}
#so-booking-widget[data-template="bubble"] .so-bw-calendar-day {
    border: none;
    background: transparent;
    border-radius: 50%;
    transition: all 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
}
#so-booking-widget[data-template="bubble"] .so-bw-calendar-day--available:hover {
    background: var(--so-bw-primary-light);
    transform: scale(1.12);
}
#so-booking-widget[data-template="bubble"] .so-bw-calendar-day--selected,
#so-booking-widget[data-template="bubble"] .so-bw-calendar-day--selected:hover {
    transform: scale(1.16);
    box-shadow: 0 8px 20px rgba(99, 102, 241, 0.40);
}
#so-booking-widget[data-template="bubble"] .so-bw-calendar-nav {
    background: var(--so-bw-primary-light);
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
    border: none;
    width: 32px;
    height: 32px;
}
#so-booking-widget[data-template="bubble"] .so-bw-calendar-nav:hover:not(:disabled) {
    background: var(--so-bw-primary);
    color: #fff;
    transform: scale(1.1);
}

/* ── COMPACT ─────────────────────────────────────────────────────
   Tightly packed mini-grid, slimmer days, minimal chrome. Conveys
   "dense, efficient, dashboard-style". */
#so-booking-widget[data-template="compact"] .so-bw-calendar {
    background: var(--so-bw-card-bg);
    border: 1px solid var(--so-bw-border);
    border-radius: var(--so-bw-r-8);
    padding: 6px;
}
#so-booking-widget[data-template="compact"] .so-bw-calendar-header {
    padding: 4px 6px;
    margin-bottom: 4px;
}
#so-booking-widget[data-template="compact"] .so-bw-calendar-month {
    font-size: 12px;
    font-weight: 600;
    text-align: left;
    flex: 1;
}
#so-booking-widget[data-template="compact"] .so-bw-calendar-nav {
    width: 22px;
    height: 22px;
    border-radius: var(--so-bw-r-4);
}
#so-booking-widget[data-template="compact"] .so-bw-calendar-nav svg {
    width: 12px;
    height: 12px;
}
#so-booking-widget[data-template="compact"] .so-bw-calendar-weekdays,
#so-booking-widget[data-template="compact"] .so-bw-calendar-grid {
    border: none;
    border-radius: 0;
}
#so-booking-widget[data-template="compact"] .so-bw-calendar-weekdays span {
    background: transparent;
    border: none;
    font-size: 9px;
    padding: 3px 0;
}
#so-booking-widget[data-template="compact"] .so-bw-calendar-day {
    border: none;
    background: transparent;
    border-radius: var(--so-bw-r-4);
    font-size: 11px;
    min-height: 28px;
}
#so-booking-widget[data-template="compact"] .so-bw-calendar-day--available:hover {
    background: var(--so-bw-primary-light);
}
#so-booking-widget[data-template="compact"] .so-bw-calendar-day--selected,
#so-booking-widget[data-template="compact"] .so-bw-calendar-day--selected:hover {
    border-radius: var(--so-bw-r-4);
}

/* ── STUDIO ──────────────────────────────────────────────────────
   Mirrors the calendar shown in the Widget Studio admin preview for
   the Studio Booking template (WidgetPage.vue → renderPreviewByLayout
   case 'studio'): rounded date cards (14px radius) with bold sans-
   serif numbers, individual 1.5px borders per cell (not table-style
   continuous), black `#111` solid selected state with white text,
   and a clean left-aligned month label paired with rounded 8px-radius
   nav buttons. The weekday strip is muted (no fill, transparent) so
   the date cards themselves carry the visual weight. */
#so-booking-widget[data-template="studio"] .so-bw-calendar {
    background: var(--so-bw-card-bg);
    border: none;
    border-radius: 0;
    padding: 0;
    font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
}
#so-booking-widget[data-template="studio"] .so-bw-calendar::before {
    content: none;
}
#so-booking-widget[data-template="studio"] .so-bw-calendar-header {
    margin-bottom: 14px;
}
#so-booking-widget[data-template="studio"] .so-bw-calendar-month {
    font-size: clamp(15px, calc(var(--so-bw-cw, 100vw) * 0.013000000000000001), 16px);
    font-weight: 700;
    text-transform: none;
    letter-spacing: -0.01em;
    text-align: left;
    color: #111;
    flex: 1;
    font-family: inherit;
}
#so-booking-widget[data-template="studio"] .so-bw-calendar-weekdays {
    border: none;
    border-radius: 0;
    background: transparent;
    margin-bottom: 6px;
}
#so-booking-widget[data-template="studio"] .so-bw-calendar-weekdays span {
    background: transparent;
    color: #9ca3af;
    border: none;
    text-transform: none;
    letter-spacing: 0;
    font-size: 11px;
    font-weight: 500;
    padding: 4px 0;
}
#so-booking-widget[data-template="studio"] .so-bw-calendar-grid {
    border: none;
    border-radius: 0;
    gap: 4px;
    overflow: visible;
}
#so-booking-widget[data-template="studio"] .so-bw-calendar-day {
    border: 1.5px solid #e5e7eb;
    border-radius: var(--so-bw-r-14);
    background: #fff;
    color: #111;
    font-weight: 700;
    font-size: clamp(13px, calc(var(--so-bw-cw, 100vw) * 0.01), 15px);
    transition: border-color 0.15s ease, background 0.15s ease, color 0.15s ease;
}
/* Reset the table-style "strip last row/column" rules — every cell
   in the studio variant is its own bordered card. */
#so-booking-widget[data-template="studio"] .so-bw-calendar-day:nth-child(7n),
#so-booking-widget[data-template="studio"] .so-bw-calendar-day:nth-last-child(-n+7) {
    border: 1.5px solid #e5e7eb;
}
#so-booking-widget[data-template="studio"] .so-bw-calendar-day--other-month,
#so-booking-widget[data-template="studio"] .so-bw-calendar-day--disabled {
    background: #fff;
    border-color: #f3f4f6;
    color: #d1d5db;
}
#so-booking-widget[data-template="studio"] .so-bw-calendar-day--available:hover {
    border-color: #111;
    background: #fff;
    color: #111;
    box-shadow: none;
}
#so-booking-widget[data-template="studio"] .so-bw-calendar-day--today {
    border-color: #d1d5db;
    color: #111;
    font-weight: 700;
}
#so-booking-widget[data-template="studio"] .so-bw-calendar-day--today::after {
    background: #111;
    width: 4px;
    height: 4px;
    bottom: 6px;
}
#so-booking-widget[data-template="studio"] .so-bw-calendar-day--selected,
#so-booking-widget[data-template="studio"] .so-bw-calendar-day--selected:hover {
    background: #111 !important;
    color: #fff !important;
    border: 1.5px solid #111 !important;
    border-radius: var(--so-bw-r-14);
    box-shadow: none;
    z-index: 1;
}
#so-booking-widget[data-template="studio"] .so-bw-calendar-day--selected::after {
    display: none;
}
#so-booking-widget[data-template="studio"] .so-bw-calendar-nav {
    width: 30px;
    height: 30px;
    border: 1.5px solid #e5e7eb;
    border-radius: var(--so-bw-r-8);
    color: #374151;
    background: #fff;
}
#so-booking-widget[data-template="studio"] .so-bw-calendar-nav:hover:not(:disabled) {
    border-color: #111;
    background: #fff;
    color: #111;
}
#so-booking-widget[data-template="studio"] .so-bw-calendar-nav:disabled {
    opacity: 0.4;
}

/* ==========================================================================
   26. Service-card layout guard — MUST be last in source order
   ==========================================================================
   Service cards (step 2) use flex-direction: column so the selectable
   top row (icon + title + check) sits above the "+ Add extras" full-width
   strip. Every preview-mirror block earlier in this file sets
   `.so-bw-card { flex-direction: row !important }` at specificity (1,2,0).
   Those rules match the service card element (which has class "so-bw-card
   so-bw-card--service") and override the vertical layout, causing the
   "+ Add extras" toggle to drift INLINE next to the title instead of
   living below it as a full-width bottom strip — and on narrow mobile
   viewports the card body text was also squished because the toggle
   occupied right-side horizontal space.

   FIX: repeat the service-card column-layout rule AFTER all preview-mirror
   blocks at specificity (1,2,0). Same specificity + later source order wins
   against every per-template flex-direction: row !important rule above.
   The [data-template] attribute selector is the specificity booster; the
   plain .so-bw-card--service rule below covers plain widgets with no
   data-template attribute.
   ========================================================================== */
#so-booking-widget[data-template] .so-bw-card--service {
    flex-direction: column !important;
    align-items: stretch !important;
    padding: 0 !important;
    gap: 0 !important;
}

/* ==========================================================================
   27. Card-check display guard
   ==========================================================================
   The base rule uses visibility: hidden (not display: none) on unselected
   card-checks so the card layout doesn't reflow when a card is selected.
   For the minimal template the card-check was overridden to `width: auto;
   padding: 5px 10px` — a much wider pill than the default 28px circle. The
   invisible wide pill still consumed ~60px of horizontal space in the card-
   top row, squishing the card-body text on mobile.
   FIX: the minimal card-check rule (added earlier) sets display:none on the
   hidden state. This guard ensures all other templates' checks continue to
   use visibility:hidden (no layout reflow on selection). No change needed
   for the base rule — it already uses visibility:hidden correctly. This
   comment block documents why only the minimal template uses display:none.
   ========================================================================== */

/* ==========================================================================
   STANDOUT LAYOUTS — ported from the Booking Widget Studio design file.
   Additive skins keyed on [data-template="…"]; each only applies when its
   layout is the active template, so the original 7 layouts are unaffected.
   Token-based (mirrors the dark skin) — restyles surfaces, accent and
   typography to evoke each design without restructuring the Alpine flow.
   Live IDs that map here: dev-neon→terminal, salon-noir→swipe,
   saas-electric→typeform, digital-magenta→stories, art-violet→glass,
   luxe-gold→boarding-pass, consultant-slate→timeline, photo-mono→receipt.
   ========================================================================== */

/* --- Terminal: command line, mono, green-on-black ----------------------- */
#so-booking-widget[data-template="terminal"] .so-bw-container {
    --so-bw-primary: #22c55e;
    --so-bw-primary-hover: #16a34a;
    --so-bw-card-bg: #11151a;
    --so-bw-card-bg-hover: #16201a;
    --so-bw-bg-secondary: #11151a;
    --so-bw-text-secondary: #9fce9f;
    --so-bw-text-tertiary: #5a6e5a;
    --so-bw-bg-tertiary: #16201a;
    --so-bw-border: #1e2a1e;
    --so-bw-header-border: rgba(61, 240, 122, 0.18);
    --so-bw-font: 'Space Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
}
#so-booking-widget[data-template="terminal"] .so-bw-container,
#so-booking-widget[data-template="terminal"] .so-bw-step,
#so-booking-widget[data-template="terminal"] .so-bw-header {
    background: #0c0f12;
    color: #c8f7c5;
    border-color: #1e2a1e;
}
#so-booking-widget[data-template="terminal"] .so-bw-card { border-color: #1e2a1e; color: #c8f7c5; }
#so-booking-widget[data-template="terminal"] .so-bw-card-title { color: #e6ffe6; }
#so-booking-widget[data-template="terminal"] .so-bw-card-meta,
#so-booking-widget[data-template="terminal"] .so-bw-step-desc,
#so-booking-widget[data-template="terminal"] .so-bw-step-label,
#so-booking-widget[data-template="terminal"] .so-bw-step-count { color: #8fb58f; }
#so-booking-widget[data-template="terminal"] .so-bw-calendar-day { color: #c8f7c5; }

/* --- Swipe Deck: dark plum, rounded, pink accent ------------------------ */
#so-booking-widget[data-template="swipe"] .so-bw-container {
    --so-bw-primary: #ff4d6d;
    --so-bw-primary-hover: #e63357;
    --so-bw-card-bg: #2a2230;
    --so-bw-card-bg-hover: #37303e;
    --so-bw-bg-secondary: #2a2230;
    --so-bw-text-secondary: #c9bfd0;
    --so-bw-text-tertiary: #9a90a6;
    --so-bw-bg-tertiary: #37303e;
    --so-bw-border: rgba(255, 255, 255, 0.14);
    --so-bw-header-border: rgba(255, 255, 255, 0.1);
    --so-bw-radius-xl: var(--so-bw-r-24);
    --so-bw-btn-radius: 999px;
}
#so-booking-widget[data-template="swipe"] .so-bw-container,
#so-booking-widget[data-template="swipe"] .so-bw-step,
#so-booking-widget[data-template="swipe"] .so-bw-header {
    background: #1a1420;
    color: #ffffff;
    border-color: rgba(255, 255, 255, 0.14);
}
/* Light cards on dark body → dark card text (header/step text stays light). */
#so-booking-widget[data-template="swipe"] .so-bw-card { border-color: rgba(255, 255, 255, 0.14); color: #1c1726 !important; }
#so-booking-widget[data-template="swipe"] .so-bw-card-title { color: #1a1420 !important; }
#so-booking-widget[data-template="swipe"] .so-bw-card-meta { color: #5b5566 !important; }
#so-booking-widget[data-template="swipe"] .so-bw-step-desc,
#so-booking-widget[data-template="swipe"] .so-bw-step-label,
#so-booking-widget[data-template="swipe"] .so-bw-step-count { color: #c9bfd0; }
#so-booking-widget[data-template="swipe"] .so-bw-calendar-day { color: #f1eaf4; }

/* --- Typeform: dark navy, yellow accent (dark button text) -------------- */
#so-booking-widget[data-template="typeform"] .so-bw-container {
    --so-bw-primary: #ffd23f;
    --so-bw-primary-hover: #eac01f;
    --so-bw-card-bg: rgba(255, 255, 255, 0.06);
    --so-bw-card-bg-hover: rgba(255, 255, 255, 0.1);
    /* Secondary surface (policy card, recurring header, etc.) MUST be dark
       on this navy template — without this it fell back to the light-theme
       default (#f9fafb), so the light --so-bw-text headings ("Service
       policies", "Recurring Booking") rendered light-on-light = invisible. */
    --so-bw-bg-secondary: rgba(255, 255, 255, 0.06);
    --so-bw-text-secondary: #b6c6dc;
    --so-bw-text-tertiary: #8aa0c0;
    --so-bw-bg-tertiary: rgba(255, 255, 255, 0.08);
    --so-bw-border: rgba(255, 255, 255, 0.16);
    --so-bw-header-border: rgba(255, 255, 255, 0.12);
}
#so-booking-widget[data-template="typeform"] .so-bw-container,
#so-booking-widget[data-template="typeform"] .so-bw-step,
#so-booking-widget[data-template="typeform"] .so-bw-header {
    background: linear-gradient(160deg, #102a4e, #0b1f3a);
    color: #ffffff;
    border-color: rgba(255, 255, 255, 0.16);
}
/* Cards are LIGHT (per-template card bg) on the dark navy body, so their text
   must be DARK to be legible — NOT the white used for the header/step text. */
#so-booking-widget[data-template="typeform"] .so-bw-card { border-color: rgba(255, 255, 255, 0.16); color: #1c2740 !important; }
#so-booking-widget[data-template="typeform"] .so-bw-card-title { color: #16233e !important; }
#so-booking-widget[data-template="typeform"] .so-bw-card-meta { color: #5a6b85 !important; }
#so-booking-widget[data-template="typeform"] .so-bw-step-desc,
#so-booking-widget[data-template="typeform"] .so-bw-step-label,
#so-booking-widget[data-template="typeform"] .so-bw-step-count { color: #b6c6dc; }
#so-booking-widget[data-template="typeform"] .so-bw-calendar-day { color: #e6eefc; }
#so-booking-widget[data-template="typeform"] .so-bw-btn--primary { color: #0b1f3a; }

/* --- Stories: dark purple radial, pink accent -------------------------- */
#so-booking-widget[data-template="stories"] .so-bw-container {
    --so-bw-primary: #ff3d77;
    --so-bw-primary-hover: #e01e5a;
    --so-bw-card-bg: rgba(255, 255, 255, 0.06);
    --so-bw-card-bg-hover: rgba(255, 255, 255, 0.1);
    --so-bw-bg-secondary: rgba(255, 255, 255, 0.06);
    --so-bw-text-secondary: #c9b9d8;
    --so-bw-text-tertiary: #a294b8;
    --so-bw-bg-tertiary: rgba(255, 255, 255, 0.08);
    --so-bw-border: rgba(255, 255, 255, 0.16);
    --so-bw-header-border: rgba(255, 255, 255, 0.12);
}
#so-booking-widget[data-template="stories"] .so-bw-container,
#so-booking-widget[data-template="stories"] .so-bw-step,
#so-booking-widget[data-template="stories"] .so-bw-header {
    background: radial-gradient(600px 600px at 50% 0%, #33153f, #120a1e);
    color: #ffffff;
    border-color: rgba(255, 255, 255, 0.16);
}
/* Light cards on dark body → dark card text (header/step text stays light). */
#so-booking-widget[data-template="stories"] .so-bw-card { border-color: rgba(255, 255, 255, 0.16); color: #281534 !important; }
#so-booking-widget[data-template="stories"] .so-bw-card-title { color: #2a1336 !important; }
#so-booking-widget[data-template="stories"] .so-bw-card-meta { color: #6b5b75 !important; }
#so-booking-widget[data-template="stories"] .so-bw-step-desc,
#so-booking-widget[data-template="stories"] .so-bw-step-label,
#so-booking-widget[data-template="stories"] .so-bw-step-count { color: #c9b9d8; }
#so-booking-widget[data-template="stories"] .so-bw-calendar-day { color: #f3e9f7; }

/* --- Glass: gradient backdrop + frosted translucent cards -------------- */
#so-booking-widget[data-template="glass"] .so-bw-container {
    --so-bw-primary: #7c5cff;
    --so-bw-primary-hover: #6a47f0;
    --so-bw-card-bg: rgba(255, 255, 255, 0.14);
    --so-bw-card-bg-hover: rgba(255, 255, 255, 0.22);
    --so-bw-bg-secondary: rgba(255, 255, 255, 0.08);
    --so-bw-text-secondary: rgba(255, 255, 255, 0.75);
    --so-bw-text-tertiary: rgba(255, 255, 255, 0.6);
    --so-bw-bg-tertiary: rgba(255, 255, 255, 0.16);
    --so-bw-border: rgba(255, 255, 255, 0.25);
    --so-bw-header-border: rgba(255, 255, 255, 0.2);
}
#so-booking-widget[data-template="glass"] .so-bw-container,
#so-booking-widget[data-template="glass"] .so-bw-step,
#so-booking-widget[data-template="glass"] .so-bw-header {
    background: linear-gradient(135deg, #6a3ac0, #3a2eae 55%, #e0185c);
    color: #ffffff;
    border-color: rgba(255, 255, 255, 0.25);
}
#so-booking-widget[data-template="glass"] .so-bw-card {
    border-color: rgba(255, 255, 255, 0.3);
    /* The frosted card reads LIGHT (translucent white over the gradient), so
       its text must be dark to stay legible — white was invisible. */
    color: #271a4d !important;
    -webkit-backdrop-filter: blur(16px);
    backdrop-filter: blur(16px);
}
#so-booking-widget[data-template="glass"] .so-bw-card-title { color: #1f1442 !important; }
#so-booking-widget[data-template="glass"] .so-bw-card-meta { color: #574b7c !important; }
#so-booking-widget[data-template="glass"] .so-bw-card-meta,
#so-booking-widget[data-template="glass"] .so-bw-step-desc,
#so-booking-widget[data-template="glass"] .so-bw-step-label,
#so-booking-widget[data-template="glass"] .so-bw-step-count { color: rgba(255, 255, 255, 0.75); }
#so-booking-widget[data-template="glass"] .so-bw-calendar-day { color: #ffffff; }

/* --- Boarding Pass: light, blue accent, dashed ticket edges ------------ */
#so-booking-widget[data-template="boarding-pass"] .so-bw-container {
    --so-bw-primary: #2b7de0;
    --so-bw-primary-hover: #1a52a0;
    --so-bw-border: #dce3ec;
    --so-bw-header-border: #dce3ec;
}
#so-booking-widget[data-template="boarding-pass"] .so-bw-container {
    border: 1px dashed #b9c6d8;
}
#so-booking-widget[data-template="boarding-pass"] .so-bw-card { border-style: dashed; }

/* Timeline is no longer a legacy skin — it is architecture M (see the
   design-system section below) and takes its accent from the template
   palette like every other architecture, so the hardcoded deep-green
   override that used to live here has been removed. */

/* --- Receipt: white paper, monospace, dashed rules --------------------- */
#so-booking-widget[data-template="receipt"] .so-bw-container {
    --so-bw-primary: #1a1a1a;
    --so-bw-primary-hover: #000000;
    --so-bw-border: #cfcfcf;
    --so-bw-header-border: #cfcfcf;
    --so-bw-font: 'Space Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
}
#so-booking-widget[data-template="receipt"] .so-bw-card { border-style: dashed; border-color: #cfcfcf; }
#so-booking-widget[data-template="receipt"] .so-bw-header { border-bottom: 1px dashed #bbbbbb; }

/* --- Wheel Pickers: light, iOS blue, extra-rounded --------------------- */
#so-booking-widget[data-template="wheel"] .so-bw-container {
    --so-bw-primary: #0a84ff;
    --so-bw-primary-hover: #0060d0;
    --so-bw-radius-xl: var(--so-bw-r-22);
    --so-bw-card-radius: var(--so-bw-r-14);
    --so-bw-btn-radius: var(--so-bw-r-12);
}

/* --- Diagonal Split: teal brand accent + angled gradient header -------- */
#so-booking-widget[data-template="diagonal"] .so-bw-container {
    --so-bw-primary: #0e7c86;
    --so-bw-primary-hover: #0a555c;
}
#so-booking-widget[data-template="diagonal"] .so-bw-header {
    background: linear-gradient(150deg, #0e7c86, #063c42);
    color: #ffffff;
    clip-path: polygon(0 0, 100% 0, 100% 80%, 0 100%);
    padding-bottom: 20px;
}
#so-booking-widget[data-template="diagonal"] .so-bw-step-label,
#so-booking-widget[data-template="diagonal"] .so-bw-step-count { color: #ffffff; }

/* ==========================================================================
   PER-TEMPLATE STRUCTURE — pull the live booking page closer to the design
   file's *layout* (not just its colours): the card-style templates render
   as a single narrow card with a one-column service list + big title, like
   the file's Swipe Deck / Stories / Glass / Typeform mockups, instead of
   the default wide two-column grid. DOM is unchanged; this only restyles
   width + grid + type so the public page matches what the gallery shows.
   ========================================================================== */

/* Swipe Deck — narrow dark card, single-column list, stacked-deck shells. */
#so-booking-widget[data-template="swipe"] .so-bw-container {
    max-width: 400px;
    margin-left: auto;
    margin-right: auto;
    border-radius: var(--so-bw-r-24);
    position: relative;
}
#so-booking-widget[data-template="swipe"] .so-bw-container::before,
#so-booking-widget[data-template="swipe"] .so-bw-container::after {
    content: "";
    position: absolute;
    top: 10px;
    bottom: 10px;
    left: 6px;
    right: 6px;
    border-radius: var(--so-bw-r-24);
    z-index: -1;
}
#so-booking-widget[data-template="swipe"] .so-bw-container::before { transform: rotate(-3.5deg); background: #2a2230; }
#so-booking-widget[data-template="swipe"] .so-bw-container::after  { transform: rotate(3deg);  background: #37303e; }
#so-booking-widget[data-template="swipe"] .so-bw-card-grid { grid-template-columns: 1fr !important; }
#so-booking-widget[data-template="swipe"] .so-bw-step-label { font-size: 22px; font-weight: 800; letter-spacing: -0.02em; }

/* Stories — narrow full-bleed dark card, single-column list, big title. */
#so-booking-widget[data-template="stories"] .so-bw-container {
    max-width: 380px;
    margin-left: auto;
    margin-right: auto;
    border-radius: var(--so-bw-r-26);
}
#so-booking-widget[data-template="stories"] .so-bw-card-grid { grid-template-columns: 1fr !important; }
#so-booking-widget[data-template="stories"] .so-bw-step-label { font-size: 22px; font-weight: 800; letter-spacing: -0.02em; }

/* Glass — narrow frosted card centred on the gradient, single column. */
#so-booking-widget[data-template="glass"] .so-bw-container {
    max-width: 420px;
    margin-left: auto;
    margin-right: auto;
    border-radius: var(--so-bw-r-24);
}
#so-booking-widget[data-template="glass"] .so-bw-card-grid { grid-template-columns: 1fr !important; }

/* Typeform — keep full width but one focused question column. */
#so-booking-widget[data-template="typeform"] .so-bw-card-grid { grid-template-columns: 1fr !important; }

/* Boarding Pass / Wheel — single-column list reads like the file's stub /
   drum column rather than a wide grid. */
#so-booking-widget[data-template="boarding-pass"] .so-bw-card-grid,
#so-booking-widget[data-template="wheel"] .so-bw-card-grid { grid-template-columns: 1fr !important; }

/* ==========================================================================
   BASE-LAYOUT STRUCTURE — make the 7 original layouts genuinely different
   shapes (not just colour): distinct container widths, service-grid columns
   and corner styles, so swapping Modern Minimal ↔ Compact ↔ Studio ↔ Bubble
   ↔ Classic visibly changes the *layout*, not only the palette.
   ========================================================================== */

/* Compact — narrow sidebar widget. */
#so-booking-widget[data-template="compact"] .so-bw-container {
    max-width: 360px;
    margin-left: auto;
    margin-right: auto;
}
#so-booking-widget[data-template="compact"] .so-bw-card-grid { grid-template-columns: 1fr !important; }

/* Bubble — soft, rounded, medium width. */
#so-booking-widget[data-template="bubble"] .so-bw-container {
    max-width: 480px;
    margin-left: auto;
    margin-right: auto;
    border-radius: var(--so-bw-r-24);
}
#so-booking-widget[data-template="bubble"] .so-bw-card { border-radius: var(--so-bw-r-18); }
#so-booking-widget[data-template="bubble"] .so-bw-card-grid { grid-template-columns: 1fr !important; }

/* Studio — wide showcase, two-column service grid. */
#so-booking-widget[data-template="studio"] .so-bw-container {
    max-width: 700px;
    margin-left: auto;
    margin-right: auto;
}
/* was @media (min-width: 560px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    #so-booking-widget:where([data-cwmin~="560"])[data-template="studio"] .so-bw-card-grid{ grid-template-columns: 1fr 1fr !important; }


/* Classic — form-style, sharp corners, single column. */
#so-booking-widget[data-template="classic"] .so-bw-container {
    max-width: 540px;
    margin-left: auto;
    margin-right: auto;
    border-radius: var(--so-bw-r-4);
}
#so-booking-widget[data-template="classic"] .so-bw-card { border-radius: var(--so-bw-r-4); }
#so-booking-widget[data-template="classic"] .so-bw-card-grid { grid-template-columns: 1fr !important; }

/* Minimal — clean, narrow-ish single column. */
#so-booking-widget[data-template="minimal"] .so-bw-container {
    max-width: 540px;
    margin-left: auto;
    margin-right: auto;
}
#so-booking-widget[data-template="minimal"] .so-bw-card-grid { grid-template-columns: 1fr !important; }

/* Gradient — medium width, rounded. */
#so-booking-widget[data-template="gradient"] .so-bw-container {
    max-width: 580px;
    margin-left: auto;
    margin-right: auto;
}

/* Dark — medium width to match the others' centred frame. */
#so-booking-widget[data-template="dark"] .so-bw-container {
    max-width: 560px;
    margin-left: auto;
    margin-right: auto;
}

/* ==========================================================================
   TWO-PANE SUMMARY RAIL — reproduces the design file's BW_1 summary-rail
   templates (Modern Minimal / Dark Mode / Spa Mint / Studio Booking) on the
   REAL booking widget: a coloured left rail with the live booking summary +
   running total, and the step flow in the right pane. So when the admin
   saves one of these templates, the appointment page both LOOKS and
   FUNCTIONS like it. Off by default; minimal / dark / studio opt in.
   ========================================================================== */
#so-booking-widget .so-bw-rail { display: none; }

#so-booking-widget[data-template="minimal"] .so-bw-container,
#so-booking-widget[data-template="dark"] .so-bw-container,
#so-booking-widget[data-template="studio"] .so-bw-container {
    position: relative;
    max-width: 800px;
    padding-left: 256px;
    overflow: hidden;
}
#so-booking-widget[data-template="minimal"] .so-bw-rail,
#so-booking-widget[data-template="dark"] .so-bw-rail,
#so-booking-widget[data-template="studio"] .so-bw-rail {
    display: flex;
    flex-direction: column;
    position: absolute;
    left: 0; top: 0; bottom: 0;
    width: 256px;
    padding: 26px 22px;
    color: #fff;
    /* Rail = the template's ACCENT colour fading into a darkened shade
       of itself, so each template's brand rail matches the design file
       (terracotta for Modern Minimal, red for Dark Mode, plum for
       Studio, …) instead of a single hardcoded gradient. color-mix is
       supported in all evergreen browsers; the flat var() is the
       fallback for the rare engine without it. */
    background: var(--so-bw-primary, #6366f1);
    background: linear-gradient(165deg, var(--so-bw-primary, #6366f1), color-mix(in srgb, var(--so-bw-primary, #6366f1) 32%, #0a0a14));
    z-index: 2;
}

.so-bw-rail-head { margin-bottom: 20px; }
.so-bw-rail-title { font-size: 19px; font-weight: 800; line-height: 1.12; letter-spacing: -0.01em; }
.so-bw-rail-sub { font-size: 12px; opacity: 0.72; margin-top: 6px; line-height: 1.4; }
/* ── The facts sit in a panel, not loose on the rail ──────────────────────
   The rows were a bare column on the rail's own background, so the summary and
   the rail were one undifferentiated surface. Boxing the list separates "what
   you have chosen" from the panel it lives on, which is what makes it read as
   a receipt-in-progress rather than as more of the sidebar.

   Border and background come from the tokens, so each architecture's own
   surface and hairline are used rather than a literal that would be wrong on
   half of them. Dusk opts out just below: its rail is a dark strip docked
   along the BOTTOM, where a boxed column of rows is not what is rendered. */
.so-bw-rail-list {
    flex: 1 1 auto;
    display: flex;
    flex-direction: column;
    padding: 4px 12px;
    border: 1px solid var(--so-bw-border);
    border-radius: var(--so-bw-ds-radius-card, 12px);
    background: var(--so-bw-card-bg);
}
#so-booking-widget[data-template="dusk"] .so-bw-rail-list {
    padding: 0;
    border: 0;
    border-radius: 0;
    background: none;
}
.so-bw-rail-row { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; padding: 10px 0; border-bottom: 1px solid rgba(255, 255, 255, 0.13); }
.so-bw-rail-k { font-size: 11px; letter-spacing: 0.04em; text-transform: uppercase; opacity: 0.62; flex-shrink: 0; }
/* ── A glyph per row ──────────────────────────────────────────────────────
   The summary reads as a list of facts, and a fact is quicker to find by its
   shape than by reading its label. The glyphs come from `summaryFactIcon()` —
   the same helper the newer `.so-bw-sumpanel` uses for the same four facts, so
   the two summary components draw from one set rather than diverging.

   Rendered INSIDE `.so-bw-rail-k` rather than as a third child of the row. The
   row is a `space-between` flex pair — label left, value right — on eleven
   architectures, each of which has tuned that relationship; a third flex child
   would have spread them into three columns everywhere. Inside the label it
   costs the row nothing. */
.so-bw-rail-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 14px;
    height: 14px;
    margin-right: 7px;
    vertical-align: -2px;
    opacity: 0.8;
}
.so-bw-rail-icon svg { width: 14px; height: 14px; }
.so-bw-rail-v { font-size: 12.5px; font-weight: 700; text-align: right; word-break: break-word; }
/* `width: 100%` and a gap floor are both load-bearing. This is a flex item in a
   column rail, so without an explicit width it shrink-wraps to its content —
   and `space-between` then has no free space to distribute, which butts the
   label straight against the amount. It rendered as "TOTALFree" and
   "TOTAL₹2,400.00" on Accordion and Agenda at 380px. The gap is the floor for
   the case where the row IS wide enough but the two strings still nearly meet. */
.so-bw-rail-total { display: flex; align-items: center; justify-content: space-between; width: 100%; gap: 12px; padding-top: 16px; margin-top: 10px; border-top: 1px solid rgba(255, 255, 255, 0.28); }
.so-bw-rail-total span:first-child { font-size: 12px; letter-spacing: 0.04em; text-transform: uppercase; opacity: 0.75; }
.so-bw-rail-total span:last-child { font-size: 22px; font-weight: 800; }

/* Two-pane content: single-column cards in the right pane. */
#so-booking-widget[data-template="minimal"] .so-bw-card-grid,
#so-booking-widget[data-template="dark"] .so-bw-card-grid,
#so-booking-widget[data-template="studio"] .so-bw-card-grid { grid-template-columns: 1fr !important; }

/* Narrow screens: stack — rail becomes a compact summary on top. */
/* was @media (max-width:680px) — now gated on the CONTAINER width
   via data-cw (see booking-widget.js _wireContainerWidth). :where()
   keeps specificity identical to the media query it replaces. */

    #so-booking-widget:where([data-cw~="680"])[data-template="minimal"] .so-bw-container,
    #so-booking-widget:where([data-cw~="680"])[data-template="dark"] .so-bw-container,
    #so-booking-widget:where([data-cw~="680"])[data-template="studio"] .so-bw-container{ padding-left: 0; max-width: 100%; }
    #so-booking-widget:where([data-cw~="680"])[data-template="minimal"] .so-bw-rail,
    #so-booking-widget:where([data-cw~="680"])[data-template="dark"] .so-bw-rail,
    #so-booking-widget:where([data-cw~="680"])[data-template="studio"] .so-bw-rail{
        position: static; width: auto; flex-direction: row; flex-wrap: wrap; align-items: center;
        gap: 6px 16px; border-radius: var(--so-bw-r-12); margin: 0 0 14px; padding: 14px 16px;
    }
    #so-booking-widget:where([data-cw~="680"]) .so-bw-rail-head{ margin-bottom: 0; flex: 1 1 100%; }
    #so-booking-widget:where([data-cw~="680"]) .so-bw-rail-list{ flex-direction: row; flex-wrap: wrap; gap: 0 16px; flex: 1 1 100%; }
    #so-booking-widget:where([data-cw~="680"]) .so-bw-rail-row{ border-bottom: none; padding: 4px 0; }
    #so-booking-widget:where([data-cw~="680"]) .so-bw-rail-total{ padding-top: 0; margin-top: 0; border-top: none; }


/* ==========================================================================
   PER-TEMPLATE NAVIGATION — gives each standout layout its own motion +
   progress treatment so the booking flow *navigates* like the design file,
   not just recolours. Pure presentation: the step engine is unchanged; the
   keyframe re-runs each time a step's display flips none→block (x-show), so
   advancing/going-back animates per template. The matching Enter / tap-to-
   advance behaviour is wired in booking-widget.js (_setupTemplateNav).
   ========================================================================== */
@keyframes soBwSlideIn { from { opacity: 0; transform: translateX(44px); } to { opacity: 1; transform: none; } }
@keyframes soBwFadeUp  { from { opacity: 0; transform: translateY(26px); } to { opacity: 1; transform: none; } }
@keyframes soBwSnap    { from { opacity: 0; transform: scale(0.96); }      to { opacity: 1; transform: none; } }
@keyframes soBwPrint   { from { opacity: 0; transform: translateY(-16px); clip-path: inset(0 0 100% 0); } to { opacity: 1; transform: none; clip-path: inset(0 0 0 0); } }
@keyframes soBwBlurIn  { from { opacity: 0; filter: blur(10px); }          to { opacity: 1; filter: none; } }

#so-booking-widget[data-template="swipe"]    .so-bw-step { animation: soBwSlideIn 0.34s cubic-bezier(0.22, 0.61, 0.36, 1); }
#so-booking-widget[data-template="stories"]  .so-bw-step { animation: soBwSnap 0.3s ease; }
#so-booking-widget[data-template="typeform"] .so-bw-step { animation: soBwFadeUp 0.36s cubic-bezier(0.22, 0.61, 0.36, 1); }
#so-booking-widget[data-template="boarding-pass"] .so-bw-step { animation: soBwFadeUp 0.3s ease; }
#so-booking-widget[data-template="timeline"] .so-bw-step { animation: soBwFadeUp 0.3s ease; }
#so-booking-widget[data-template="glass"]    .so-bw-step { animation: soBwBlurIn 0.4s ease; }
#so-booking-widget[data-template="receipt"]  .so-bw-step { animation: soBwPrint 0.4s ease; }
#so-booking-widget[data-template="terminal"] .so-bw-step { animation: soBwSnap 0.18s ease; }

/* Stories: top story-style segmented progress (≈6 segments) + tappable
   header cue. */
#so-booking-widget[data-template="stories"] .so-bw-header { cursor: pointer; }
#so-booking-widget[data-template="stories"] .so-bw-progress {
    position: relative;
    height: 4px;
    border-radius: var(--so-bw-r-2);
    overflow: hidden;
}
#so-booking-widget[data-template="stories"] .so-bw-progress::after {
    content: "";
    position: absolute;
    inset: 0;
    background: repeating-linear-gradient(90deg,
        transparent 0,
        transparent calc(16.66% - 3px),
        rgba(18, 10, 30, 0.9) calc(16.66% - 3px),
        rgba(18, 10, 30, 0.9) 16.66%);
    pointer-events: none;
}

/* Typeform: one big question per screen + thin top progress + Enter hint. */
#so-booking-widget[data-template="typeform"] .so-bw-progress { height: 4px; border-radius: 0; }
#so-booking-widget[data-template="typeform"] .so-bw-step-label {
    font-size: 22px;
    font-weight: 800;
    letter-spacing: -0.02em;
    line-height: 1.1;
}
#so-booking-widget[data-template="typeform"] .so-bw-step-count::after {
    /* Escaped for the same reason as .so-bw-slot-back-date::before — see the
       note there. Six digits so both spaces after it survive. */
    content: "  \0000B7  press Enter \21B5";
    opacity: 0.7;
}

/* Swipe: deck-style — give the active step a card lift so the slide reads
   as a card being dealt. */
#so-booking-widget[data-template="swipe"] .so-bw-step {
    border-radius: var(--so-bw-r-20);
}

@media (prefers-reduced-motion: reduce) {
    #so-booking-widget[data-template] .so-bw-step { animation: none !important; }
}

/* Two-pane right-pane header — clean (no heavy colour bar) for the light
   summary-rail templates, matching the file's subtle step indicator. The
   dark template keeps its dark header. */
#so-booking-widget[data-template="minimal"] .so-bw-header,
#so-booking-widget[data-template="studio"] .so-bw-header {
    background: #ffffff !important;
    color: #111827 !important;
    border-bottom: 1px solid #eef0f5;
}
#so-booking-widget[data-template="minimal"] .so-bw-step-label,
#so-booking-widget[data-template="studio"] .so-bw-step-label { color: #111827; }
#so-booking-widget[data-template="minimal"] .so-bw-step-count,
#so-booking-widget[data-template="studio"] .so-bw-step-count { color: #6b7280; }

/* ==========================================================================
   REDUCED, CENTRED WIDTH — the flow templates render as a contained, centred
   card rather than spanning the full page. Two-pane templates (minimal /
   dark / studio) are a touch wider to fit their 256px rail + content; the
   single-pane flow templates (gradient / classic / bubble) sit in a tighter
   reading column. Card-style standout templates keep their own widths.

   We still set an explicit max-width + auto margins on the WIDGET ROOT
   (`#so-booking-widget`, specificity 1,1,0 beats the host block theme's
   0-specificity `:where()` content-size clamp) so the chosen width wins
   over the theme's ~645px reading column instead of being squeezed by it.
   ========================================================================== */
/* Width is driven by the Studio "Widget Size" radio via [data-size] on the
   root (small / medium / large / full).

   CAUTION — `--so-bw-maxw` is NOT how the nineteen shipping templates size
   themselves, despite what this comment said for a long time. The only rule
   that consumes it is the one directly below, whose selector list is six
   RETIRED layout keys (minimal / dark / studio / gradient / classic /
   bubble). None of them are in WidgetTemplateCatalog, so for the live
   catalogue this token and its four overrides are inert. The live mechanism
   is section 23, which scales each template's own width. Kept because the
   retired keys still have stylesheet coverage. */
#so-booking-widget { --so-bw-maxw: 820px; }
#so-booking-widget[data-size="small"]  { --so-bw-maxw: 660px; }
#so-booking-widget[data-size="medium"] { --so-bw-maxw: 820px; }
#so-booking-widget[data-size="large"]  { --so-bw-maxw: 1000px; }
#so-booking-widget[data-size="full"]   { --so-bw-maxw: 100%; }

#so-booking-widget[data-template="minimal"],
#so-booking-widget[data-template="dark"],
#so-booking-widget[data-template="studio"],
#so-booking-widget[data-template="gradient"],
#so-booking-widget[data-template="classic"],
#so-booking-widget[data-template="bubble"],
#so-booking-widget[data-template="minimal"] .so-bw-container,
#so-booking-widget[data-template="dark"] .so-bw-container,
#so-booking-widget[data-template="studio"] .so-bw-container,
#so-booking-widget[data-template="gradient"] .so-bw-container,
#so-booking-widget[data-template="classic"] .so-bw-container,
#so-booking-widget[data-template="bubble"] .so-bw-container {
    max-width: var(--so-bw-maxw, 820px);
    width: 100%;
    margin-left: auto;
    margin-right: auto;
}

/* ==========================================================================
   PER-STEP SEARCH BAR — filters the location / service / staff card lists
   (rendered only when a list has > 5 cards). Sits above the (now scrollable)
   grid; the magnifier icon + clear "×" are absolutely positioned inside the
   field. Inherits the active template's primary for the focus ring so it
   tints per template like every other control.
   ========================================================================== */
/* NOTE: padding/margin here are prefixed with #so-booking-widget so they
   beat the widget's `#so-booking-widget *{padding:0;margin:0}` reset
   (specificity 1,0,0) — without the id prefix the input padding collapses
   to 0 and the placeholder text slides under the magnifier icon. */
#so-booking-widget .so-bw-search {
    position: relative;
    display: flex;
    align-items: center;
    margin: 0 0 14px;
}
.so-bw-search-icon {
    position: absolute;
    left: 13px;
    color: var(--so-bw-text-tertiary);
    pointer-events: none;
}
#so-booking-widget .so-bw-search-input {
    width: 100%;
    padding: 9px 34px 9px 38px;
    border: 1px solid var(--so-bw-border);
    border-radius: var(--so-bw-radius);
    font-family: inherit;
    font-size: 14px;
    color: var(--so-bw-text);
    background: var(--so-bw-card-bg);
    outline: none;
    transition: border-color var(--so-bw-transition), box-shadow var(--so-bw-transition);
}
.so-bw-search-input::placeholder { color: var(--so-bw-text-tertiary); }
.so-bw-search-input:focus {
    border-color: var(--so-bw-border-focus);
    box-shadow: 0 0 0 3px var(--so-bw-primary-light);
}
.so-bw-search-clear {
    position: absolute;
    right: 8px;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 22px;
    height: 22px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: transparent;
    color: var(--so-bw-text-tertiary);
    font-size: 19px;
    line-height: 1;
    cursor: pointer;
    transition: background var(--so-bw-transition), color var(--so-bw-transition);
}
.so-bw-search-clear:hover {
    background: var(--so-bw-bg-tertiary);
    color: var(--so-bw-text);
}
/* Dark template: the search field sits on a dark surface, so flip it. */
#so-booking-widget[data-template="dark"] .so-bw-search-input {
    background: rgba(255, 255, 255, 0.06);
    border-color: rgba(255, 255, 255, 0.16);
    color: #f1f5f9;
}
#so-booking-widget[data-template="dark"] .so-bw-search-input::placeholder { color: #94a3b8; }

/* ==========================================================================
   SCROLLABLE CARD LIST — caps a long list's height so the rest of the flow
   (Continue button, summary) stays in view, and the list scrolls within.
   Applied to the location / staff grids and a wrapper around the grouped
   service list. Keeps the widget compact regardless of catalogue length.
   ========================================================================== */
.so-bw-card-grid--scroll,
.so-bw-scroll-wrap--on {
    max-height: 60vh;
    overflow-y: auto;
    overflow-x: hidden;
    /* Breathing room so cards / focus rings don't collide with the bar. */
    padding-right: 6px;
    scrollbar-width: thin;
    scrollbar-color: var(--so-bw-border) transparent;
}
.so-bw-card-grid--scroll::-webkit-scrollbar,
.so-bw-scroll-wrap--on::-webkit-scrollbar { width: 8px; }
.so-bw-card-grid--scroll::-webkit-scrollbar-track,
.so-bw-scroll-wrap--on::-webkit-scrollbar-track { background: transparent; }
.so-bw-card-grid--scroll::-webkit-scrollbar-thumb,
.so-bw-scroll-wrap--on::-webkit-scrollbar-thumb {
    background: var(--so-bw-border);
    border-radius: 999px;
}
.so-bw-card-grid--scroll::-webkit-scrollbar-thumb:hover,
.so-bw-scroll-wrap--on::-webkit-scrollbar-thumb:hover { background: var(--so-bw-text-tertiary); }
#so-booking-widget[data-template="dark"] .so-bw-card-grid--scroll,
#so-booking-widget[data-template="dark"] .so-bw-scroll-wrap--on { scrollbar-color: rgba(255,255,255,0.22) transparent; }
#so-booking-widget[data-template="dark"] .so-bw-card-grid--scroll::-webkit-scrollbar-thumb,
#so-booking-widget[data-template="dark"] .so-bw-scroll-wrap--on::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.22); }

/* ==========================================================================
   COMPACT — the full-width flow / two-pane templates get tighter cards,
   smaller avatar tiles and trimmer step padding so the booking page reads
   as a dense, efficient form (matches the compact reference) rather than a
   roomy single-column list. Token-only so every component inherits; the
   narrow card-style templates (swipe / stories / glass / wheel / …) keep
   their designed roomier sizing.
   ========================================================================== */
#so-booking-widget[data-template="minimal"],
#so-booking-widget[data-template="dark"],
#so-booking-widget[data-template="studio"],
#so-booking-widget[data-template="gradient"],
#so-booking-widget[data-template="classic"],
#so-booking-widget[data-template="bubble"] {
    --so-bw-card-pad: 12px;
    --so-bw-card-gap: 10px;
    --so-bw-card-title-size: 14px;
    --so-bw-card-meta-size: 12px;
    --so-bw-icon-size: 40px;
    --so-bw-icon-svg-size: 23px;
    --so-bw-icon-text-size: 16px;
    --so-bw-step-pad: 18px;
    --so-bw-header-pad-y-top: 18px;
    --so-bw-header-pad-y-bottom: 14px;
}
/* Tighten the step intro copy + group headings to match the denser cards. */
#so-booking-widget[data-template="minimal"] .so-bw-step-desc,
#so-booking-widget[data-template="dark"] .so-bw-step-desc,
#so-booking-widget[data-template="studio"] .so-bw-step-desc,
#so-booking-widget[data-template="gradient"] .so-bw-step-desc,
#so-booking-widget[data-template="classic"] .so-bw-step-desc,
#so-booking-widget[data-template="bubble"] .so-bw-step-desc {
    margin-bottom: 14px;
    font-size: 13px;
}

/* ==========================================================================
   HORIZONTAL NUMBERED STEP BAR — reproduces the design file's signature
   "1 2 3 …" progress indicator: connected numbered dots with labels, the
   current step filled in the template accent, completed steps ticked. Lives
   in the header for every template; it supersedes the old thin progress bar
   (hidden below). Scrolls horizontally on narrow widths rather than wrapping.
   ========================================================================== */
.so-bw-stepbar {
    display: flex;
    align-items: flex-start;
    margin: 4px 0 16px;
    padding-bottom: 2px;
    overflow-x: auto;
    scrollbar-width: none;
}
.so-bw-stepbar::-webkit-scrollbar { display: none; }
.so-bw-stepbar-item {
    position: relative;
    flex: 1 1 0;
    min-width: 54px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 6px;
    text-align: center;
}
/* Connector line from the previous dot to this one, behind the dots.
   ── :first-of-type, NOT :first-child ────────────────────────────────────
   The first step used to draw a connector too — a solid accent line running
   off its left edge into the card's padding, pointing back at a dot that does
   not exist. Measured on the live install: 205px wide at `left: -102.5px`.

   `:not(:first-child)` was correct against the markup as WRITTEN and wrong
   against the markup as RENDERED. Alpine's `x-for` keeps its `<template>`
   element in the DOM and inserts the repeated items after it, so the first
   `.so-bw-stepbar-item` is `:nth-child(2)` and `:not(:first-child)` matches it.

   Type-matching skips the stray sibling: the items are divs, the template is a
   `<template>`, so `:first-of-type` is the first ITEM regardless of what Alpine
   leaves in front of it. Same specificity, so nothing else in the cascade
   shifts — and every template with a step bar loses the stray line, which is
   the whole point; it was never intentional on any of them.

   The `--active` / `--done` colour rules below carry the same correction, or
   the first step would keep drawing a coloured one. */
.so-bw-stepbar-item:not(:first-of-type)::before {
    content: '';
    position: absolute;
    top: 12px;
    left: -50%;
    right: 50%;
    height: 2px;
    background: var(--so-bw-border, #e5e7eb);
    z-index: 0;
}
.so-bw-stepbar-item--active:not(:first-of-type)::before,
.so-bw-stepbar-item--done:not(:first-of-type)::before {
    background: var(--so-bw-primary, #6366f1);
}
.so-bw-stepbar-dot {
    position: relative;
    z-index: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 26px;
    height: 26px;
    border-radius: 50%;
    background: var(--so-bw-card-bg, #fff);
    border: 2px solid var(--so-bw-border, #e5e7eb);
    color: var(--so-bw-text-tertiary, #9ca3af);
    font-size: 12px;
    font-weight: 700;
    flex-shrink: 0;
    transition: background var(--so-bw-transition), border-color var(--so-bw-transition), color var(--so-bw-transition);
}
.so-bw-stepbar-item--active .so-bw-stepbar-dot {
    background: var(--so-bw-primary, #6366f1);
    border-color: var(--so-bw-primary, #6366f1);
    color: #fff;
    box-shadow: 0 0 0 4px var(--so-bw-primary-light, rgba(99, 102, 241, 0.15));
}
.so-bw-stepbar-item--done .so-bw-stepbar-dot {
    background: var(--so-bw-primary, #6366f1);
    border-color: var(--so-bw-primary, #6366f1);
    color: #fff;
}
.so-bw-stepbar-label {
    font-size: 11px;
    font-weight: 600;
    line-height: 1.25;
    color: var(--so-bw-text-tertiary, #9ca3af);
    /* Eight step names in one row, every one of them translated. `nowrap`
       made the widest label set the minimum width of the whole step bar, so
       one long translation ("Bestätigung") pushed the bar past the header on
       a phone. Labels wrap under their own dot instead; the item is a centred
       flex column, so a two-line label stays centred on it. */
    white-space: normal;
    overflow-wrap: anywhere;
    min-width: 0;
    max-width: 100%;
}
.so-bw-stepbar-item--active .so-bw-stepbar-label { color: var(--so-bw-primary, #6366f1); }
.so-bw-stepbar-item--done .so-bw-stepbar-label { color: var(--so-bw-text-secondary, #6b7280); }

/* The numbered step bar replaces the old slim progress bar everywhere. */
.so-bw-progress { display: none !important; }

/* On dark templates the pane is dark — lift the future-step dot off it. */
#so-booking-widget[data-template="dark"] .so-bw-stepbar-dot,
#so-booking-widget[data-template="studio"] .so-bw-stepbar-dot {
    background: rgba(255, 255, 255, 0.08);
    border-color: rgba(255, 255, 255, 0.22);
    color: rgba(255, 255, 255, 0.6);
}
#so-booking-widget[data-template="dark"] .so-bw-stepbar-item:not(:first-of-type)::before,
#so-booking-widget[data-template="studio"] .so-bw-stepbar-item:not(:first-of-type)::before {
    background: rgba(255, 255, 255, 0.18);
}
#so-booking-widget[data-template="dark"] .so-bw-stepbar-item--active:not(:first-of-type)::before,
#so-booking-widget[data-template="dark"] .so-bw-stepbar-item--done:not(:first-of-type)::before,
#so-booking-widget[data-template="studio"] .so-bw-stepbar-item--active:not(:first-of-type)::before,
#so-booking-widget[data-template="studio"] .so-bw-stepbar-item--done:not(:first-of-type)::before {
    background: var(--so-bw-primary, #6366f1);
}

/* ==========================================================================
   COMPACT FIXED-HEIGHT CARD — on desktop the widget becomes a self-contained
   card: the header (numbered step bar) and the footer (Back / Continue) stay
   PINNED while ONLY the body — the service / staff / location lists, the
   calendar, the forms — scrolls. So a long catalogue never grows the page,
   the Continue button is always reachable without scrolling, and several
   widgets (appointment + events + tickets) can share one page without it
   becoming endlessly tall. The per-step search bar sticks to the top of the
   scroll region. Mobile keeps the natural full-height flow.
   ========================================================================== */
.so-bw-header,
.so-bw-nav,
.so-bw-error,
.so-bw-member-banner,
.so-bw-hold-timer { flex-shrink: 0; }
#so-booking-widget .so-bw-body { min-height: 0; }

/* was @media (min-width: 768px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    /* Bound the card to a compact height for the standard-flow templates
       (the seven base layouts). The standout card templates — swipe /
       stories / glass / wheel / typeform / boarding-pass / … — keep their
       own designed heights and are intentionally left out. */
    #so-booking-widget:where([data-cwmin~="768"])[data-template="minimal"] .so-bw-container,
    #so-booking-widget:where([data-cwmin~="768"])[data-template="gradient"] .so-bw-container,
    #so-booking-widget:where([data-cwmin~="768"])[data-template="classic"] .so-bw-container,
    #so-booking-widget:where([data-cwmin~="768"])[data-template="dark"] .so-bw-container,
    #so-booking-widget:where([data-cwmin~="768"])[data-template="bubble"] .so-bw-container,
    #so-booking-widget:where([data-cwmin~="768"])[data-template="compact"] .so-bw-container,
    #so-booking-widget:where([data-cwmin~="768"])[data-template="studio"] .so-bw-container{
        display: flex;
        flex-direction: column;
        max-height: min(78vh, 660px);
    }
    /* The single scroll region between the pinned header and footer. */
    #so-booking-widget:where([data-cwmin~="768"]) .so-bw-body{
        flex: 1 1 auto;
        min-height: 0;
        overflow-y: auto;
        overflow-x: hidden;
    }
    /* Standard properties behind the same engine gate as the pair in section
       12 — see the long note there. Setting either of them anywhere in the
       cascade switches Chromium off the -webkit- pseudo-elements for this
       element entirely, so a second unguarded declaration here would undo
       that fix from a completely different part of the file. */
    @supports not selector(::-webkit-scrollbar) {
        #so-booking-widget:where([data-cwmin~="768"]) .so-bw-body{
            scrollbar-width: thin;
            scrollbar-color: var(--so-bw-border) transparent;
        }
    }
    #so-booking-widget:where([data-cwmin~="768"]) .so-bw-body::-webkit-scrollbar{ width: 8px; }
    #so-booking-widget:where([data-cwmin~="768"]) .so-bw-body::-webkit-scrollbar-track{ background: transparent; }
    #so-booking-widget:where([data-cwmin~="768"]) .so-bw-body::-webkit-scrollbar-thumb{ background: var(--so-bw-border); border-radius: 999px; }
    /* Search bar pinned to the top of the scroll region. Opaque pane-bg
       so the cards scroll cleanly underneath it. */
    #so-booking-widget:where([data-cwmin~="768"]) .so-bw-search{
        position: sticky;
        top: 0;
        z-index: 4;
        margin: 0 0 12px;
        padding-bottom: 4px;
        background: var(--so-bw-bg, #fff);
    }
    /* The body is now the ONE scroll container — drop the per-list inner
       max-height caps so there's no nested scrollbar inside it. */
    #so-booking-widget:where([data-cwmin~="768"]) .so-bw-card-grid--scroll,
    #so-booking-widget:where([data-cwmin~="768"]) .so-bw-scroll-wrap--on{
        max-height: none;
        overflow: visible;
        padding-right: 0;
    }
    #so-booking-widget:where([data-cwmin~="768"])[data-template="dark"] .so-bw-body::-webkit-scrollbar-thumb,
    #so-booking-widget:where([data-cwmin~="768"])[data-template="studio"] .so-bw-body::-webkit-scrollbar-thumb{ background: rgba(255, 255, 255, 0.22); }


/* ==========================================================================
   Per-template distinct visual identities (CSS-only, layered on base layout)
   Each rule keys off [data-template-id]. The base/layout rules force card,
   icon, container, header and button styling with !important, so every
   property here that the base also sets MUST use !important and set the
   shape DIRECTLY (border-radius on .so-bw-card / .so-bw-container /
   .so-bw-btn--primary) — the --so-bw-*-radius variables are ignored by the
   layout. Templates sharing a base layout are made clearly distinguishable
   by radius, treatment, icon-tile shape and header. Accent
   (var(--so-bw-primary)) is preserved; only color-mix tints of it are used.
   ========================================================================== */

/* ── minimal group: modern-minimal / spa-mint / zen-studio / popup-launcher
      / inline-embed / bottom-sheet / gauge-stack ───────────────────────── */

/* modern-minimal — hairline-flat, sharp 8px, plain bottom-hairline header */

/* spa-mint — soft, calm, big 22px radius, borderless mint tint, soft shadow,
   round icon tiles, no header line */

/* zen-studio — earthy serif, hard-edged 2px, borderless tinted fill, square
   icon tiles, filled header band */

/* popup-launcher — modal, soft-shadow-no-border 14px, rounded icon, plain
   header, lifted container */

/* inline-embed — borderless-transparent container, outlined-1px transparent
   cards, sharp 4px, square icon, hairline header */

/* bottom-sheet — sheet, rounded-top container, filled-tint pill-ish 18px
   cards, large round icon, filled header band */

/* gauge-stack — bold numeric mono, hard-edged 0px, outlined-thick 2px,
   square icon, accent top-bar header */

/* ── classic group: classic-form / clinic-blue / calendly-classic / checkout ─ */

/* classic-form — traditional serif, sharp 2px outlined-1px, square icon,
   filled secondary header band */

/* clinic-blue — crisp medical, 8px soft, outlined-1px tinted, rounded icon,
   accent bottom-rule header */

/* calendly-classic — scheduler, 14px soft-shadow-no-border, round icon,
   pill buttons, plain header */

/* checkout — e-commerce, hard-edged 0px, filled-tint card, square icon,
   dark filled header band */

/* ── studio group: studio-booking / service-grid / centre-stage / kiosk ──── */

/* studio-booking — editorial serif, hard-edged 0px borderless bottom-rule
   rows, square icon, double-rule header */

/* ==========================================================================
   STRUCTURAL PROGRESS / CHROME DIFFERENTIATION — PER BASE LAYOUT
   ==========================================================================
   Each non-rail base layout gets a structurally distinct progress indicator
   and header/frame chrome so selecting a template visibly changes the *design*,
   not just the colour. Appended last so source-order wins over earlier rules
   at equal specificity (1,2,0 via [data-template] attribute selector).

   Rules:
   1. All padding/margin on elements inside #so-booking-widget must be prefixed
      with #so-booking-widget to beat the `* {padding:0;margin:0}` reset at
      specificity (1,0,0).
   2. Every property that an earlier preview-mirror or per-template block sets
      with !important must also use !important here.
   3. NO display:none on cards, slots, buttons, or the .so-bw-nav footer.
   4. Tokens only — no hard-coded brand colours; use var(--so-bw-primary) /
      var(--so-bw-bg) / var(--so-bw-text) / color-mix tints.
   ========================================================================== */

/* ── TYPEFORM (typeform, stepper-wizard)
   Chrome: HIDE the numbered stepbar; slim filled progress bar edge-to-edge
   at the very top of the container (outside the header); airy centered header;
   generous card padding. One focused question per screen. ─────────────────── */
#so-booking-widget[data-template="typeform"] .so-bw-stepbar {
    display: none !important;
}
/* Re-enable the progress bar (globally hidden by the stepbar block) as a
   prominent slim filled bar pinned to the top edge of the container. */
#so-booking-widget[data-template="typeform"] .so-bw-progress {
    display: block !important;
    position: absolute !important;
    top: 0 !important;
    left: 0 !important;
    right: 0 !important;
    height: 4px !important;
    border-radius: 0 !important;
    background: rgba(255, 255, 255, 0.18) !important;
    overflow: visible !important;
    z-index: 10 !important;
}
#so-booking-widget[data-template="typeform"] .so-bw-progress-bar {
    height: 4px !important;
    border-radius: 0 !important;
    background: var(--so-bw-primary) !important;
    box-shadow: 0 0 8px rgba(var(--so-bw-primary-rgb, 232, 232, 58), 0.55) !important;
}
/* Container: position relative so the absolute progress bar sits flush top */
#so-booking-widget[data-template="typeform"] .so-bw-container {
    position: relative !important;
    overflow: hidden !important;
}
/* Header: centered, minimal, airy — matches typeform.com aesthetic */
#so-booking-widget[data-template="typeform"] .so-bw-header {
    text-align: center !important;
    padding-top: 36px !important; /* push below the 4px progress bar */
    border-bottom: none !important;
    background: transparent !important;
}
#so-booking-widget[data-template="typeform"] .so-bw-step-indicator {
    flex-direction: column !important;
    align-items: center !important;
    gap: 8px !important;
    margin-bottom: 0 !important;
}
#so-booking-widget[data-template="typeform"] .so-bw-step-label {
    font-size: clamp(22px, calc(var(--so-bw-cw, 100vw) * 0.04), 28px) !important;
    font-weight: 800 !important;
    letter-spacing: -0.02em !important;
    line-height: 1.1 !important;
    text-align: center !important;
}
#so-booking-widget[data-template="typeform"] .so-bw-step-count {
    opacity: 0.65 !important;
    font-size: 13px !important;
}
/* Cards: generous padding, breathing room */
#so-booking-widget[data-template="typeform"] .so-bw-card {
    padding: 18px !important;
}
/* was @media (max-width: 480px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    #so-booking-widget:where([data-cw~="480"])[data-template="typeform"] .so-bw-header{
        padding-top: 28px !important;
    }
    #so-booking-widget:where([data-cw~="480"])[data-template="typeform"] .so-bw-step-label{
        font-size: 20px !important;
    }


/* ── GLASS (glass-modal)
   Chrome: frosted floating card — translucent container background,
   backdrop-filter blur, big 24px radius, large multi-layer shadow;
   slim top progress bar; hide stepbar labels (keep dots tiny, decorative).
   Frosted card feel: content floats above a vivid gradient backdrop. ──────── */
#so-booking-widget[data-template="glass"] .so-bw-container {
    background: rgba(255, 255, 255, 0.12) !important;
    backdrop-filter: blur(14px) !important;
    -webkit-backdrop-filter: blur(14px) !important;
    border-radius: var(--so-bw-r-24) !important;
    border: 1px solid rgba(255, 255, 255, 0.28) !important;
    box-shadow:
        0 8px 32px rgba(0, 0, 0, 0.32),
        0 2px 8px rgba(0, 0, 0, 0.18),
        inset 0 1px 0 rgba(255, 255, 255, 0.35) !important;
    overflow: hidden !important;
}
/* Slim top progress bar — re-enabled, absolute at top */
#so-booking-widget[data-template="glass"] .so-bw-progress {
    display: block !important;
    position: absolute !important;
    top: 0 !important;
    left: 0 !important;
    right: 0 !important;
    height: 3px !important;
    border-radius: 0 !important;
    background: rgba(255, 255, 255, 0.20) !important;
    overflow: visible !important;
    z-index: 10 !important;
}
#so-booking-widget[data-template="glass"] .so-bw-progress-bar {
    height: 3px !important;
    border-radius: 0 !important;
    background: rgba(255, 255, 255, 0.85) !important;
    box-shadow: 0 0 10px rgba(255, 255, 255, 0.6) !important;
}
/* Stepbar: keep dots tiny and decorative, HIDE the text labels */
#so-booking-widget[data-template="glass"] .so-bw-stepbar {
    gap: 0 !important;
    margin: 6px 0 12px !important;
    justify-content: center !important;
}
#so-booking-widget[data-template="glass"] .so-bw-stepbar-item {
    flex: 0 0 auto !important;
    min-width: 0 !important;
    gap: 0 !important;
}
#so-booking-widget[data-template="glass"] .so-bw-stepbar-label {
    display: none !important;
}
#so-booking-widget[data-template="glass"] .so-bw-stepbar-dot {
    width: 8px !important;
    height: 8px !important;
    font-size: 0 !important;
    background: rgba(255, 255, 255, 0.25) !important;
    border: 1px solid rgba(255, 255, 255, 0.40) !important;
    border-radius: 50% !important;
    color: transparent !important;
}
#so-booking-widget[data-template="glass"] .so-bw-stepbar-item--active .so-bw-stepbar-dot {
    width: 20px !important;
    height: 8px !important;
    border-radius: var(--so-bw-r-4) !important;
    background: rgba(255, 255, 255, 0.85) !important;
    border-color: transparent !important;
    box-shadow: none !important;
    color: transparent !important;
    font-size: 0 !important;
}
#so-booking-widget[data-template="glass"] .so-bw-stepbar-item--done .so-bw-stepbar-dot {
    background: rgba(255, 255, 255, 0.55) !important;
    border-color: transparent !important;
    color: transparent !important;
    font-size: 0 !important;
}
#so-booking-widget[data-template="glass"] .so-bw-stepbar-item:not(:first-of-type)::before {
    background: rgba(255, 255, 255, 0.18) !important;
    top: 4px !important;
    height: 1px !important;
}
#so-booking-widget[data-template="glass"] .so-bw-stepbar-item--active:not(:first-of-type)::before,
#so-booking-widget[data-template="glass"] .so-bw-stepbar-item--done:not(:first-of-type)::before {
    background: rgba(255, 255, 255, 0.50) !important;
}
/* Frosted header */
#so-booking-widget[data-template="glass"] .so-bw-header {
    background: rgba(255, 255, 255, 0.10) !important;
    border-bottom: 1px solid rgba(255, 255, 255, 0.20) !important;
    border-radius: var(--so-bw-r-24) var(--so-bw-r-24) 0 0 !important;
    padding-top: 20px !important;
    backdrop-filter: blur(8px) !important;
    -webkit-backdrop-filter: blur(8px) !important;
}
/* Nav bar: frosted bottom */
#so-booking-widget[data-template="glass"] .so-bw-nav {
    background: rgba(255, 255, 255, 0.10) !important;
    border-top: 1px solid rgba(255, 255, 255, 0.20) !important;
    backdrop-filter: blur(8px) !important;
    -webkit-backdrop-filter: blur(8px) !important;
}
/* was @media (max-width: 480px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    #so-booking-widget:where([data-cw~="480"])[data-template="glass"] .so-bw-container{
        border-radius: var(--so-bw-r-18) !important;
    }


/* ── BOARDING-PASS (boarding-pass)
   Chrome: ticket-stub feel — monospace font on header, dashed perforation
   divider under header; stepbar becomes a compact "sequence" numbers-only
   strip (hide labels). Container has slight scalloped edge via radial-gradient
   mask-image notches on left/right midpoints. ─────────────────────────────── */
#so-booking-widget[data-template="boarding-pass"] .so-bw-stepbar-label {
    display: none !important;
}
#so-booking-widget[data-template="boarding-pass"] .so-bw-stepbar {
    margin: 4px 0 10px !important;
    gap: 0 !important;
    justify-content: flex-start !important;
}
#so-booking-widget[data-template="boarding-pass"] .so-bw-stepbar-item {
    flex: 0 0 auto !important;
    min-width: 32px !important;
    gap: 0 !important;
}
#so-booking-widget[data-template="boarding-pass"] .so-bw-stepbar-dot {
    width: 22px !important;
    height: 22px !important;
    font-size: 11px !important;
    border-radius: var(--so-bw-r-4) !important; /* rectangular, not round — ticket number box */
    border-width: 1px !important;
    font-family: 'Courier New', Courier, monospace !important;
    font-weight: 700 !important;
}
#so-booking-widget[data-template="boarding-pass"] .so-bw-stepbar-item:not(:first-of-type)::before {
    height: 1px !important;
    top: 11px !important;
    border-top: 1px dashed var(--so-bw-border, #dce3ec) !important;
    background: transparent !important;
}
#so-booking-widget[data-template="boarding-pass"] .so-bw-stepbar-item--active:not(:first-of-type)::before,
#so-booking-widget[data-template="boarding-pass"] .so-bw-stepbar-item--done:not(:first-of-type)::before {
    border-color: var(--so-bw-primary, #2b7de0) !important;
    background: transparent !important;
}
/* Monospace header with dashed perforation bottom */
#so-booking-widget[data-template="boarding-pass"] .so-bw-header {
    font-family: 'Courier New', Courier, monospace !important;
    border-bottom: 2px dashed var(--so-bw-border, #b9c6d8) !important;
    background: var(--so-bw-bg-secondary, #f9fafb) !important;
    letter-spacing: 0.02em !important;
}
#so-booking-widget[data-template="boarding-pass"] .so-bw-step-label {
    font-family: 'Courier New', Courier, monospace !important;
    letter-spacing: 0.04em !important;
    text-transform: uppercase !important;
}
/* Scalloped notch at midpoint via radial-gradient mask on container sides */
#so-booking-widget[data-template="boarding-pass"] .so-bw-container {
    border: 1px dashed var(--so-bw-border, #b9c6d8) !important;
    border-radius: var(--so-bw-r-6) !important;
    overflow: hidden !important;
    /* Scalloped edge: cut two half-circles out of left and right at midpoint */
    -webkit-mask-image: radial-gradient(circle at 0% 52%, transparent 14px, white 15px),
                        radial-gradient(circle at 100% 52%, transparent 14px, white 15px);
    mask-image: radial-gradient(circle at 0% 52%, transparent 14px, white 15px),
                radial-gradient(circle at 100% 52%, transparent 14px, white 15px);
    -webkit-mask-composite: destination-in;
    mask-composite: intersect;
}
/* Re-enable progress as hidden (stepbar is the indicator here) */
#so-booking-widget[data-template="boarding-pass"] .so-bw-progress {
    display: none !important;
}
/* was @media (max-width: 480px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    #so-booking-widget:where([data-cw~="480"])[data-template="boarding-pass"] .so-bw-stepbar-item{
        min-width: 26px !important;
    }
    #so-booking-widget:where([data-cw~="480"])[data-template="boarding-pass"] .so-bw-stepbar-dot{
        width: 18px !important;
        height: 18px !important;
        font-size: 10px !important;
    }


/* ── STORIES (stories)
   Chrome: Instagram-stories segmented progress — restyle stepbar-items into
   thin full-width segments (active/done = filled, pending = faint); dark
   immersive header; hide text labels AND dot numbers. ─────────────────────── */
#so-booking-widget[data-template="stories"] .so-bw-stepbar {
    align-items: center !important;
    gap: 4px !important;
    margin: 0 0 0 !important;
    padding: 0 !important;
    height: 5px !important;
    overflow: visible !important;
}
#so-booking-widget[data-template="stories"] .so-bw-stepbar-item {
    flex: 1 1 0 !important;
    min-width: 0 !important;
    height: 5px !important;
    gap: 0 !important;
    flex-direction: row !important;
    align-items: stretch !important;
    position: relative !important;
}
/* The connecting line becomes the segment background track */
#so-booking-widget[data-template="stories"] .so-bw-stepbar-item:not(:first-of-type)::before {
    display: none !important;
}
/* The dot becomes a full-width segment bar */
#so-booking-widget[data-template="stories"] .so-bw-stepbar-dot {
    width: 100% !important;
    height: 5px !important;
    border-radius: var(--so-bw-r-2) !important;
    background: rgba(255, 255, 255, 0.25) !important;
    border: none !important;
    color: transparent !important;
    font-size: 0 !important;
    box-shadow: none !important;
    flex-shrink: 0 !important;
    position: relative !important;
    z-index: 1 !important;
}
#so-booking-widget[data-template="stories"] .so-bw-stepbar-item--done .so-bw-stepbar-dot {
    background: rgba(255, 255, 255, 0.85) !important;
    border: none !important;
    color: transparent !important;
    font-size: 0 !important;
}
#so-booking-widget[data-template="stories"] .so-bw-stepbar-item--active .so-bw-stepbar-dot {
    background: #ffffff !important;
    border: none !important;
    color: transparent !important;
    font-size: 0 !important;
    box-shadow: none !important;
}
/* Filling animation on active segment using pseudo-element */
#so-booking-widget[data-template="stories"] .so-bw-stepbar-item--active .so-bw-stepbar-dot::after {
    content: '';
    position: absolute;
    left: 0; top: 0; bottom: 0;
    width: 60%;
    background: #ffffff;
    border-radius: var(--so-bw-r-2);
    animation: so-bw-stories-fill 3s linear;
}
@keyframes so-bw-stories-fill {
    from { width: 0%; }
    to { width: 100%; }
}
#so-booking-widget[data-template="stories"] .so-bw-stepbar-label {
    display: none !important;
}
/* Header: immersive dark, flush top */
#so-booking-widget[data-template="stories"] .so-bw-header {
    background: transparent !important;
    border-bottom: none !important;
    padding-bottom: 8px !important;
}
/* Progress bar hidden (stepbar IS the progress) */
#so-booking-widget[data-template="stories"] .so-bw-progress {
    display: none !important;
}
/* Step label: bold white, big */
#so-booking-widget[data-template="stories"] .so-bw-step-label {
    font-size: clamp(20px, calc(var(--so-bw-cw, 100vw) * 0.05), 26px) !important;
    font-weight: 800 !important;
    color: #ffffff !important;
}
#so-booking-widget[data-template="stories"] .so-bw-step-count {
    color: rgba(255, 255, 255, 0.72) !important;
}

/* ── SWIPE (swipe-deck)
   Chrome: DOT progress — stepbar dots shrunk to small circles, labels hidden,
   active dot wider/pill; card-deck feel via the stacked ::before/::after.
   The deck shadows are already on the container; here we focus on the
   progress dots in the header. ─────────────────────────────────────────────── */
#so-booking-widget[data-template="swipe"] .so-bw-stepbar-label {
    display: none !important;
}
#so-booking-widget[data-template="swipe"] .so-bw-stepbar {
    align-items: center !important;
    gap: 6px !important;
    margin: 8px 0 12px !important;
    justify-content: center !important;
}
#so-booking-widget[data-template="swipe"] .so-bw-stepbar-item {
    flex: 0 0 auto !important;
    min-width: 0 !important;
    gap: 0 !important;
}
#so-booking-widget[data-template="swipe"] .so-bw-stepbar-item:not(:first-of-type)::before {
    display: none !important; /* no connector lines between dot-style indicators */
}
#so-booking-widget[data-template="swipe"] .so-bw-stepbar-dot {
    width: 8px !important;
    height: 8px !important;
    border-radius: 50% !important;
    background: rgba(255, 255, 255, 0.22) !important;
    border: none !important;
    color: transparent !important;
    font-size: 0 !important;
    box-shadow: none !important;
    transition: width 0.25s ease, background 0.25s ease !important;
}
#so-booking-widget[data-template="swipe"] .so-bw-stepbar-item--done .so-bw-stepbar-dot {
    background: rgba(255, 255, 255, 0.55) !important;
    border: none !important;
    color: transparent !important;
    font-size: 0 !important;
}
#so-booking-widget[data-template="swipe"] .so-bw-stepbar-item--active .so-bw-stepbar-dot {
    width: 22px !important;
    height: 8px !important;
    border-radius: var(--so-bw-r-4) !important;
    background: var(--so-bw-primary, #ff4d6d) !important;
    border: none !important;
    color: transparent !important;
    font-size: 0 !important;
    box-shadow: 0 0 8px rgba(var(--so-bw-primary-rgb, 255, 77, 109), 0.55) !important;
}
/* Progress bar hidden (dot stepbar is the indicator) */
#so-booking-widget[data-template="swipe"] .so-bw-progress {
    display: none !important;
}
/* Header: dark, no border, tight */
#so-booking-widget[data-template="swipe"] .so-bw-header {
    background: #1a1420 !important;
    border-bottom: none !important;
}
/* The already-present stacked-deck shadow layers (::before/::after) on
   .so-bw-container provide the card-deck effect. Nav: dark strip. */
#so-booking-widget[data-template="swipe"] .so-bw-nav {
    background: #1a1420 !important;
    border-top: 1px solid rgba(255, 255, 255, 0.10) !important;
}

/* ── WHEEL (wheel-pickers)
   Chrome: minimal DOTS progress (like swipe, but lighter palette); rounded
   "roller" container 28px, soft inner shadow; labels hidden. ──────────────── */
#so-booking-widget[data-template="wheel"] .so-bw-stepbar-label {
    display: none !important;
}
#so-booking-widget[data-template="wheel"] .so-bw-stepbar {
    align-items: center !important;
    gap: 6px !important;
    margin: 8px 0 12px !important;
    justify-content: center !important;
}
#so-booking-widget[data-template="wheel"] .so-bw-stepbar-item {
    flex: 0 0 auto !important;
    min-width: 0 !important;
    gap: 0 !important;
}
#so-booking-widget[data-template="wheel"] .so-bw-stepbar-item:not(:first-of-type)::before {
    display: none !important;
}
#so-booking-widget[data-template="wheel"] .so-bw-stepbar-dot {
    width: 7px !important;
    height: 7px !important;
    border-radius: 50% !important;
    background: var(--so-bw-border, #e5e7eb) !important;
    border: none !important;
    color: transparent !important;
    font-size: 0 !important;
    box-shadow: none !important;
}
#so-booking-widget[data-template="wheel"] .so-bw-stepbar-item--done .so-bw-stepbar-dot {
    background: color-mix(in srgb, var(--so-bw-primary, #0a84ff) 55%, #e5e7eb) !important;
    border: none !important;
    color: transparent !important;
    font-size: 0 !important;
}
#so-booking-widget[data-template="wheel"] .so-bw-stepbar-item--active .so-bw-stepbar-dot {
    width: 18px !important;
    height: 7px !important;
    border-radius: var(--so-bw-r-4) !important;
    background: var(--so-bw-primary, #0a84ff) !important;
    border: none !important;
    color: transparent !important;
    font-size: 0 !important;
    box-shadow: none !important;
}
/* Container: extra-round, soft inner shadow "roller drum" feel */
#so-booking-widget[data-template="wheel"] .so-bw-container {
    border-radius: var(--so-bw-r-28) !important;
    box-shadow:
        0 4px 20px rgba(0, 0, 0, 0.08),
        inset 0 2px 8px rgba(0, 0, 0, 0.06),
        inset 0 -2px 6px rgba(255, 255, 255, 0.60) !important;
    border: 1px solid var(--so-bw-border, #e5e7eb) !important;
    overflow: hidden !important;
}
/* Header: soft tinted background, rounded top to match container */
#so-booking-widget[data-template="wheel"] .so-bw-header {
    background: color-mix(in srgb, var(--so-bw-primary, #0a84ff) 5%, #f7f7f9) !important;
    border-bottom: 1px solid var(--so-bw-border, #e5e7eb) !important;
    border-radius: var(--so-bw-r-28) var(--so-bw-r-28) 0 0 !important;
}
/* Progress bar hidden */
#so-booking-widget[data-template="wheel"] .so-bw-progress {
    display: none !important;
}
/* was @media (max-width: 480px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    #so-booking-widget:where([data-cw~="480"])[data-template="wheel"] .so-bw-container{
        border-radius: var(--so-bw-r-20) !important;
    }
    #so-booking-widget:where([data-cw~="480"])[data-template="wheel"] .so-bw-header{
        border-radius: var(--so-bw-r-20) var(--so-bw-r-20) 0 0 !important;
    }


/* ── M · TIMELINE (timeline-plum) — the CHROME lives in §17.10 ───────────
   This block used to hold Timeline's header, step rail and shell, and it
   carried 79 `!important` declarations to do it. Not one of them was about
   the base component rules: they existed to beat
   `#so-booking-widget[data-template] .so-bw-stepbar` (§14803),
   `…[data-template] .so-bw-stepbar-label` (§14133) and
   `…[data-template] .so-bw-container` (§14539) — all (1,2,0), exactly the
   specificity this block scored, and all LATER in the file, so source order
   decided every one of them and the only lever left was `!important`.

   The chrome is now stated ONCE, in §17.10, which sits below all three. Same
   selectors, same specificity, zero `!important`. If you are adding a Timeline
   chrome rule, add it there — putting it back here means re-deriving the
   importance ladder that this move exists to delete.

   The DAY AXIS is unaffected and still lives at "M · TIMELINE … the DAY
   AXIS" further down. ───────────────────────────────────────────────────── */

/* ── DIAGONAL (diagonal-split)
   Chrome: DIAGONAL colour band header — linear-gradient with clip-path angled
   bottom edge cutting a diagonal; white title on teal; pill progress bar below
   the angled header. ─────────────────────────────────────────────────────── */
#so-booking-widget[data-template="diagonal"] .so-bw-header {
    background: linear-gradient(135deg, var(--so-bw-primary, #0e7c86) 0%, color-mix(in srgb, var(--so-bw-primary, #0e7c86) 60%, #000) 100%) !important;
    color: #ffffff !important;
    border-bottom: none !important;
    /* Diagonal bottom edge: top-left/right full height, bottom cuts at ~85% left and full-height right */
    clip-path: polygon(0 0, 100% 0, 100% 100%, 0 88%) !important;
    padding-bottom: 32px !important; /* extra padding so text clears the angled cut */
    padding-top: 20px !important;
    margin-bottom: -1px !important; /* prevent gap between angled bottom and step body */
    position: relative !important;
    z-index: 2 !important;
    border-radius: 0 !important;
}
#so-booking-widget[data-template="diagonal"] .so-bw-step-label {
    color: #ffffff !important;
    font-size: 18px !important;
    font-weight: 800 !important;
    letter-spacing: -0.01em !important;
}
#so-booking-widget[data-template="diagonal"] .so-bw-step-count {
    color: rgba(255, 255, 255, 0.80) !important;
}
/* Progress: pill-style slim bar just below the angled header */
#so-booking-widget[data-template="diagonal"] .so-bw-progress {
    display: block !important;
    height: 5px !important;
    border-radius: 999px !important;
    background: var(--so-bw-bg-tertiary, #f3f4f6) !important;
    overflow: hidden !important;
    margin: 8px 24px 0 !important;
    position: relative !important;
    z-index: 1 !important;
}
#so-booking-widget[data-template="diagonal"] .so-bw-progress-bar {
    height: 5px !important;
    border-radius: 999px !important;
    background: var(--so-bw-primary, #0e7c86) !important;
}
/* Stepbar: hide (pill progress replaces it) */
#so-booking-widget[data-template="diagonal"] .so-bw-stepbar {
    display: none !important;
}
/* Container: sharp top corners, rounded bottom */
#so-booking-widget[data-template="diagonal"] .so-bw-container {
    border-radius: var(--so-bw-r-6) !important;
    overflow: hidden !important;
    border: 1px solid var(--so-bw-border, #e5e7eb) !important;
    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08) !important;
}
#so-booking-widget[data-template="diagonal"] .so-bw-nav {
    background: var(--so-bw-bg-secondary, #f9fafb) !important;
    border-top: 1px solid var(--so-bw-border, #e5e7eb) !important;
}
/* was @media (max-width: 480px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    #so-booking-widget:where([data-cw~="480"])[data-template="diagonal"] .so-bw-header{
        clip-path: polygon(0 0, 100% 0, 100% 100%, 0 90%) !important;
        padding-bottom: 28px !important;
    }
    #so-booking-widget:where([data-cw~="480"])[data-template="diagonal"] .so-bw-progress{
        margin: 6px 16px 0 !important;
    }


/* ── GRADIENT (bold-gradient)
   Chrome: full GRADIENT HERO header banner behind the title; pill-style
   progress bar below the banner; stepbar hidden (progress pill replaces it).
   Distinct from minimal/classic which also have coloured headers but flat. ── */
#so-booking-widget[data-template="gradient"] .so-bw-stepbar {
    display: none !important;
}
#so-booking-widget[data-template="gradient"] .so-bw-progress {
    display: block !important;
    height: 5px !important;
    border-radius: 999px !important;
    /* Progress bar sits INSIDE the step body, not the header */
    background: var(--so-bw-bg-tertiary, #f3f4f6) !important;
    overflow: hidden !important;
    margin: -8px 20px 12px !important;
    position: relative !important;
    z-index: 1 !important;
}
#so-booking-widget[data-template="gradient"] .so-bw-progress-bar {
    height: 5px !important;
    border-radius: 999px !important;
    background: linear-gradient(90deg, var(--so-bw-primary, #3b82f6), color-mix(in srgb, var(--so-bw-primary, #3b82f6) 60%, #fff)) !important;
}
/* Header already has gradient background from preview-mirror block; ensure
   rounded bottom corners match container shape */
#so-booking-widget[data-template="gradient"] .so-bw-header {
    border-radius: var(--so-bw-r-12) var(--so-bw-r-12) 0 0 !important;
}
/* was @media (max-width: 480px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    #so-booking-widget:where([data-cw~="480"])[data-template="gradient"] .so-bw-progress{
        margin: -4px 14px 10px !important;
    }


/* ── COMPACT (compact-sidebar)
   Chrome: dense — slim 3px top progress bar across top; tighter header;
   numbers-only stepbar (hide labels); reduced paddings. ───────────────────── */
#so-booking-widget[data-template="compact"] .so-bw-stepbar-label {
    display: none !important;
}
#so-booking-widget[data-template="compact"] .so-bw-stepbar {
    margin: 4px 0 8px !important;
    gap: 0 !important;
    overflow: hidden !important;
}
#so-booking-widget[data-template="compact"] .so-bw-stepbar-item {
    min-width: 28px !important;
    gap: 4px !important;
}
#so-booking-widget[data-template="compact"] .so-bw-stepbar-dot {
    width: 20px !important;
    height: 20px !important;
    font-size: 10px !important;
    border-radius: var(--so-bw-r-4) !important; /* square for dense feel */
}
/* Slim 3px top progress bar */
#so-booking-widget[data-template="compact"] .so-bw-progress {
    display: block !important;
    position: absolute !important;
    top: 0 !important;
    left: 0 !important;
    right: 0 !important;
    height: 3px !important;
    border-radius: 0 !important;
    background: rgba(0, 0, 0, 0.08) !important;
    overflow: visible !important;
    z-index: 10 !important;
}
#so-booking-widget[data-template="compact"] .so-bw-progress-bar {
    height: 3px !important;
    border-radius: 0 !important;
    background: var(--so-bw-primary) !important;
}
#so-booking-widget[data-template="compact"] .so-bw-container {
    position: relative !important;
    overflow: hidden !important;
}
/* Compact header: tight, no radius, full-width band */
#so-booking-widget[data-template="compact"] .so-bw-header {
    background: var(--so-bw-primary) !important;
    border-radius: 0 !important;
    margin: 0 !important;
    padding: 12px 14px 10px !important;
    border-bottom: none !important;
    text-align: left !important;
}
#so-booking-widget[data-template="compact"] .so-bw-step-indicator {
    flex-direction: row !important;
    align-items: center !important;
    justify-content: space-between !important;
    gap: 8px !important;
    margin-bottom: 6px !important;
}
#so-booking-widget[data-template="compact"] .so-bw-step-label {
    color: #ffffff !important;
    font-size: 13px !important;
    font-weight: 700 !important;
    text-align: left !important;
}
#so-booking-widget[data-template="compact"] .so-bw-step-count {
    color: rgba(255, 255, 255, 0.80) !important;
    font-size: 10px !important;
    text-align: right !important;
}
/* was @media (max-width: 480px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    #so-booking-widget:where([data-cw~="480"])[data-template="compact"] .so-bw-stepbar-dot{
        width: 16px !important;
        height: 16px !important;
        font-size: 9px !important;
    }
    #so-booking-widget:where([data-cw~="480"])[data-template="compact"] .so-bw-stepbar-item{
        min-width: 22px !important;
    }


/* ── BUBBLE (bubble-chat, floating-bubble)
   Chrome: chat aesthetic — dot progress (like swipe but light palette);
   rounded message-bubble header (primary bg, rounded top + asymmetric
   corners echoing chat bubble shape); step label white on primary. ─────────── */
#so-booking-widget[data-template="bubble"] .so-bw-stepbar-label {
    display: none !important;
}
#so-booking-widget[data-template="bubble"] .so-bw-stepbar {
    align-items: center !important;
    gap: 5px !important;
    margin: 6px 0 0 !important;
    justify-content: center !important;
}
#so-booking-widget[data-template="bubble"] .so-bw-stepbar-item {
    flex: 0 0 auto !important;
    min-width: 0 !important;
    gap: 0 !important;
}
#so-booking-widget[data-template="bubble"] .so-bw-stepbar-item:not(:first-of-type)::before {
    display: none !important;
}
#so-booking-widget[data-template="bubble"] .so-bw-stepbar-dot {
    width: 8px !important;
    height: 8px !important;
    border-radius: 50% !important;
    background: rgba(255, 255, 255, 0.35) !important;
    border: none !important;
    color: transparent !important;
    font-size: 0 !important;
    box-shadow: none !important;
}
#so-booking-widget[data-template="bubble"] .so-bw-stepbar-item--done .so-bw-stepbar-dot {
    background: rgba(255, 255, 255, 0.70) !important;
    border: none !important;
    color: transparent !important;
    font-size: 0 !important;
}
#so-booking-widget[data-template="bubble"] .so-bw-stepbar-item--active .so-bw-stepbar-dot {
    width: 20px !important;
    height: 8px !important;
    border-radius: var(--so-bw-r-4) !important;
    background: #ffffff !important;
    border: none !important;
    color: transparent !important;
    font-size: 0 !important;
    box-shadow: 0 0 6px rgba(255, 255, 255, 0.50) !important;
}
/* Progress bar hidden */
#so-booking-widget[data-template="bubble"] .so-bw-progress {
    display: none !important;
}
/* Header: primary coloured message-bubble header, asymmetric rounded bottom
   to evoke a chat bubble sent from "the bot". Already set in preview-mirror
   block but ensure the dot strip sits inside it. */
#so-booking-widget[data-template="bubble"] .so-bw-header {
    background: var(--so-bw-primary) !important;
    border-bottom: none !important;
    border-radius: var(--so-bw-r-12) var(--so-bw-r-12) var(--so-bw-r-12) 0 !important; /* bottom-left flat = chat bubble tail */
    margin: 12px 12px 0 !important;
    padding: 14px 16px 12px !important;
}
#so-booking-widget[data-template="bubble"] .so-bw-step-label {
    color: #ffffff !important;
    font-size: 15px !important;
    font-weight: 700 !important;
}
#so-booking-widget[data-template="bubble"] .so-bw-step-count {
    color: rgba(255, 255, 255, 0.80) !important;
}
/* was @media (max-width: 480px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    #so-booking-widget:where([data-cw~="480"])[data-template="bubble"] .so-bw-header{
        margin: 8px 8px 0 !important;
    }


/* ── RESPONSIVE GUARD — no horizontal overflow at 390px or 1280px ──────────
   All per-layout elements above use flex-wrap / overflow:hidden / max-width
   relative lengths. These guards ensure the container itself never exceeds
   100% at narrow widths. ──────────────────────────────────────────────────── */
/* was @media (max-width: 480px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    #so-booking-widget:where([data-cw~="480"])[data-template="typeform"] .so-bw-container,
    #so-booking-widget:where([data-cw~="480"])[data-template="glass"] .so-bw-container,
    #so-booking-widget:where([data-cw~="480"])[data-template="boarding-pass"] .so-bw-container,
    #so-booking-widget:where([data-cw~="480"])[data-template="stories"] .so-bw-container,
    #so-booking-widget:where([data-cw~="480"])[data-template="swipe"] .so-bw-container,
    #so-booking-widget:where([data-cw~="480"])[data-template="wheel"] .so-bw-container,
    #so-booking-widget:where([data-cw~="480"])[data-template="timeline"] .so-bw-container,
    #so-booking-widget:where([data-cw~="480"])[data-template="diagonal"] .so-bw-container,
    #so-booking-widget:where([data-cw~="480"])[data-template="gradient"] .so-bw-container,
    #so-booking-widget:where([data-cw~="480"])[data-template="compact"] .so-bw-container,
    #so-booking-widget:where([data-cw~="480"])[data-template="bubble"] .so-bw-container{
        max-width: 100% !important;
        width: 100% !important;
    }
    /* Boarding-pass mask-image notches look odd at very small widths; drop them */
    #so-booking-widget:where([data-cw~="480"])[data-template="boarding-pass"] .so-bw-container{
        -webkit-mask-image: none !important;
        mask-image: none !important;
    }


/* service-grid — grid tiles, 12px soft-shadow-no-border, rounded icon,
   hairline header */

/* centre-stage — centered hero, 22px outlined-1px, round icon, filled-tint
   centered header */

/* kiosk — big touch tiles, chunky 14px, outlined-thick 2px filled-tint,
   rounded icon, solid accent header */
/* Light touch-tile cards sit on the dark navy body, so their text must be
   dark (the kiosk theme's light --so-bw-text was invisible on the tiles). */

/* ── dark group: dark-mode / wallet-pass ────────────────────────────────── */

/* dark-mode — neon-on-black, sharp 4px, thin accent border, square icon,
   accent hairline header + glow on selected */

/* wallet-pass — frosted glass wallet, big 22px, translucent border, rounded
   icon, frosted rounded-top header */

/* ── bubble group: bubble-chat / floating-bubble ────────────────────────── */

/* bubble-chat — chat bubbles, asymmetric tail radius, borderless tint,
   round icon, plain header */

/* floating-bubble — full pill cards, lifted container, round icon, plain
   header, airy white */
/* floating-bubble: let the bubble layout's primary-colour header show (removed
   the earlier transparent override that hid it and made the white title text
   invisible — now the green band + white text matches the design thumbnail). */

/* ── typeform group: stepper-wizard / typeform ──────────────────────────── */

/* stepper-wizard — one big question, 8px outlined-thick 2px, rounded icon,
   plain hairline header, large step label */

/* typeform — giant light type, big 24px borderless tint, round icon,
   borderless transparent container + header */
/* Dark cards → white text (overrides the layout's dark card-text rule, which
   stays in effect for stepper-wizard's WHITE cards). */

/* ── remaining distinct templates (each its own layout) ─────────────────── */

/* bold-gradient — vivid glossy, 16px borderless, soft-shadow, rounded icon,
   gradient-tint header */
/* Per-service icon colour. The `[data-template="gradient"]` rule above forces
   every tile to the orange brand gradient, which flattened every service's own
   colour to the same orange. For Bold Gradient specifically, read the per-
   service colour the widget set inline (var(--so-bw-tile-bg)) so each service
   shows its own colour again; tiles without a per-entity colour (location /
   staff) fall back to the brand primary. Scoped to bold-gradient — the other
   gradient templates keep the uniform orange tile. This rule sits after the
   gradient rule at equal specificity, so it wins by source order. */

/* compact-sidebar — dense tabular, sharp 4px outlined-1px, square icon,
   filled-secondary header */

/* swipe-deck — card deck, 18px stacked-shadow borderless, rounded icon,
   plain header */

/* timeline — left-rail nodes, accent left-border, asymmetric radius, round
   icon, hairline header */

/* boarding-pass — ticket stub mono, sharp 4px dashed border, square icon,
   dashed header rule */

/* wheel-pickers — iOS drum, 18px borderless tint, round icon, soft header */

/* glass-modal — frosted translucent, 14px, translucent border, rounded icon,
   frosted header, lifted container */

/* stories — tap-through, 18px outlined-thick 2px tinted, round icon, bold
   accent top-bar header */

/* diagonal-split — angled bold, sharp 4px soft-shadow borderless, square
   icon, diagonal-tint header */

/* ==========================================================================
   THUMBNAIL-MATCHED OVERRIDES — appended last so source-order wins at equal
   specificity. Each block brings the live render in line with the design-file
   thumbnail at assets/template-thumbs/<id>.png.
   All selectors: [data-template-id] (0,2,0) or [data-template] (0,2,0);
   appended later in file beats any earlier rule at same specificity.
   ========================================================================== */

/* ── SWIPE-DECK ─────────────────────────────────────────────────────────────
   Thumbnail: DARK charcoal body (#1a1420), DARK cards (slightly lighter plum
   ~#2a2230), WHITE titles + grey subtitles, circle radio left, dot progress
   top-right. Earlier rules erroneously left card title/meta as dark colours
   matching a LIGHT card. This block corrects cards → dark + text → white. */
#so-booking-widget[data-template="swipe"] .so-bw-card {
    background: #2a2230 !important;
    border-color: rgba(255, 255, 255, 0.14) !important;
    color: #ffffff !important;
}
#so-booking-widget[data-template="swipe"] .so-bw-card-title {
    color: #ffffff !important;
}
#so-booking-widget[data-template="swipe"] .so-bw-card-meta {
    color: #c9bfd0 !important;
}
/* Card hover: slightly lighter than base */
#so-booking-widget[data-template="swipe"] .so-bw-card:hover {
    background: #37303e !important;
}
/* Selected card: accent ring */
#so-booking-widget[data-template="swipe"] .so-bw-card--selected {
    border-color: var(--so-bw-primary, #ff4d6d) !important;
    box-shadow: inset 0 0 0 1px var(--so-bw-primary, #ff4d6d) !important;
}
/* Step/header text stays light on dark body */
#so-booking-widget[data-template="swipe"] .so-bw-step-label {
    color: #ffffff !important;
}
#so-booking-widget[data-template="swipe"] .so-bw-step-count {
    color: #c9bfd0 !important;
}

/* ── STORIES ────────────────────────────────────────────────────────────────
   Thumbnail: DARK purple radial body, DARK purple option cards with WHITE
   titles + light subtitles. Earlier rules set card title/meta to dark colours
   (#281534, #6b5b75). Correct: dark cards + white text. */
#so-booking-widget[data-template="stories"] .so-bw-card {
    background: rgba(50, 20, 65, 0.70) !important;
    border-color: rgba(255, 255, 255, 0.16) !important;
    color: #ffffff !important;
    backdrop-filter: blur(8px) !important;
    -webkit-backdrop-filter: blur(8px) !important;
}
#so-booking-widget[data-template="stories"] .so-bw-card-title {
    color: #ffffff !important;
}
#so-booking-widget[data-template="stories"] .so-bw-card-meta {
    color: #c9b9d8 !important;
}
#so-booking-widget[data-template="stories"] .so-bw-card:hover {
    background: rgba(60, 28, 78, 0.85) !important;
}
#so-booking-widget[data-template="stories"] .so-bw-card--selected {
    border-color: var(--so-bw-primary, #ff3d77) !important;
    background: rgba(70, 30, 90, 0.90) !important;
}

/* ── GLASS-MODAL ─────────────────────────────────────────────────────────────
   Thumbnail: purple→magenta gradient body, frosted cards that are dark enough
   for WHITE text to read (the translucent panel over the vivid gradient appears
   noticeably dark). Earlier rule used rgba(255,255,255,0.14) which renders as
   a near-white card. Increase opacity and switch text to white. */
#so-booking-widget[data-template="glass"] .so-bw-card {
    /* DARK-tinted frosted glass — a white tint stayed near-white over the
       bright magenta end of the gradient (white text vanished). A dark indigo
       tint darkens every part of the gradient enough for white text to read,
       matching the design's frosted cards. */
    background: rgba(22, 14, 48, 0.40) !important;
    border-color: rgba(255, 255, 255, 0.28) !important;
    color: #ffffff !important;
    -webkit-backdrop-filter: blur(16px) !important;
    backdrop-filter: blur(16px) !important;
}
#so-booking-widget[data-template="glass"] .so-bw-card-title {
    color: #ffffff !important;
}
#so-booking-widget[data-template="glass"] .so-bw-card-meta {
    color: rgba(255, 255, 255, 0.75) !important;
}
#so-booking-widget[data-template="glass"] .so-bw-card:hover {
    background: rgba(22, 14, 48, 0.52) !important;
}
#so-booking-widget[data-template="glass"] .so-bw-card--selected {
    background: rgba(255, 255, 255, 0.28) !important;
    border-color: rgba(255, 255, 255, 0.70) !important;
}
/* Continue button: frosted translucent on gradient body */
#so-booking-widget[data-template="glass"] .so-bw-btn--primary {
    background: rgba(255, 255, 255, 0.25) !important;
    border: 1px solid rgba(255, 255, 255, 0.40) !important;
    color: #ffffff !important;
    -webkit-backdrop-filter: blur(8px) !important;
    backdrop-filter: blur(8px) !important;
}
#so-booking-widget[data-template="glass"] .so-bw-btn--primary:hover {
    background: rgba(255, 255, 255, 0.35) !important;
}
/* Step label + count visible on gradient */
#so-booking-widget[data-template="glass"] .so-bw-step-label {
    color: #ffffff !important;
}
#so-booking-widget[data-template="glass"] .so-bw-step-count {
    color: rgba(255, 255, 255, 0.75) !important;
}

/* ── KIOSK ───────────────────────────────────────────────────────────────────
   Thumbnail: DARK navy body (#0e1a30), DARK cards (slightly lighter navy,
   ~#162036) in a 2-COLUMN grid, WHITE titles + grey subtitles, teal accent
   "SELF SERVICE" label, dark "Continue →". Earlier rules used a light tinted
   card (color-mix 10% primary into white) which read as a pale blue-white on
   the dark body — wrong. Fix: dark cards + white text. */
/* Body area for kiosk also needs dark bg to match */
/* 2-column card grid on kiosk (studio base already does 1fr 1fr at 560px+,
   but we force it at all widths > 480px for kiosk to match the thumbnail) */
/* Container: dark navy — NO two-pane rail (studio base enables rail;
   kiosk is a self-contained kiosk terminal, no left summary rail). */
/* Hide the left-rail summary panel entirely for kiosk */
/* Nav footer: dark */
/* Continue button: dark muted */

/* ── DIAGONAL-SPLIT ─────────────────────────────────────────────────────────
   Thumbnail: LEFT vertical teal panel ("BUILDIYO" at top, "Let's get you
   booked." heading, white text on teal background) — takes roughly 40% width.
   RIGHT pane: white cards with dark text, grey "Next" button. The current
   angled header band doesn't match. Use container::before as a fixed left
   panel overlay; step content sits full-width underneath (z-index stacking). */
#so-booking-widget[data-template="diagonal"] .so-bw-container {
    overflow: hidden !important;
    position: relative !important;
    padding-left: 200px !important;
    border-radius: var(--so-bw-r-12) !important;
    border: 1px solid var(--so-bw-border, #e5e7eb) !important;
    box-shadow: 0 4px 20px rgba(0, 0, 0, 0.10) !important;
    background: #ffffff !important;
}
/* Left teal panel: fixed-position inside the container */
#so-booking-widget[data-template="diagonal"] .so-bw-container::before {
    content: "" !important;
    position: absolute !important;
    top: 0 !important;
    left: 0 !important;
    bottom: 0 !important;
    width: 200px !important;
    background: linear-gradient(160deg, #0e7c86 0%, #063c42 100%) !important;
    z-index: 2 !important;
    /* Subtle diagonal right edge */
    clip-path: polygon(0 0, 90% 0, 100% 100%, 0 100%) !important;
}
/* Hide the old angled header treatment */
#so-booking-widget[data-template="diagonal"] .so-bw-header {
    background: transparent !important;
    border-bottom: 1px solid var(--so-bw-border, #e5e7eb) !important;
    clip-path: none !important;
    padding-bottom: 18px !important;
    padding-top: 18px !important;
    margin-bottom: 0 !important;
    color: var(--so-bw-text, #1f2937) !important;
    position: relative !important;
    z-index: 3 !important;
}
#so-booking-widget[data-template="diagonal"] .so-bw-step-label {
    color: var(--so-bw-text, #1f2937) !important;
    font-style: normal !important;
}
#so-booking-widget[data-template="diagonal"] .so-bw-step-count {
    color: var(--so-bw-text-secondary, #6b7280) !important;
}
/* Cards: white with dark text, single column */
#so-booking-widget[data-template="diagonal"] .so-bw-card-grid {
    grid-template-columns: 1fr !important;
}
#so-booking-widget[data-template="diagonal"] .so-bw-card {
    background: #ffffff !important;
    border: 1px solid var(--so-bw-border, #e5e7eb) !important;
    color: var(--so-bw-text, #1f2937) !important;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06) !important;
    border-radius: var(--so-bw-r-8) !important;
}
#so-booking-widget[data-template="diagonal"] .so-bw-card-title {
    color: var(--so-bw-text, #1f2937) !important;
}
#so-booking-widget[data-template="diagonal"] .so-bw-card-meta {
    color: var(--so-bw-text-secondary, #6b7280) !important;
}
/* Step body: white */
#so-booking-widget[data-template="diagonal"] .so-bw-step {
    background: #ffffff !important;
    position: relative !important;
    z-index: 3 !important;
}
/* Nav: white */
#so-booking-widget[data-template="diagonal"] .so-bw-nav {
    background: #f9fafb !important;
    border-top: 1px solid var(--so-bw-border, #e5e7eb) !important;
    position: relative !important;
    z-index: 3 !important;
}
/* Progress pill: restore under header */
#so-booking-widget[data-template="diagonal"] .so-bw-progress {
    display: block !important;
    height: 4px !important;
    border-radius: 999px !important;
    background: var(--so-bw-bg-tertiary, #f3f4f6) !important;
    overflow: hidden !important;
    margin: 0 0 0 !important;
    position: relative !important;
    z-index: 3 !important;
}
/* On mobile: collapse left panel */
/* was @media (max-width: 600px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    #so-booking-widget:where([data-cw~="600"])[data-template="diagonal"] .so-bw-container{
        padding-left: 0 !important;
    }
    #so-booking-widget:where([data-cw~="600"])[data-template="diagonal"] .so-bw-container::before{
        display: none !important;
    }


/* ── WHEEL-PICKERS ──────────────────────────────────────────────────────────
   Thumbnail: light iOS drum-picker — selected option has a BLUE pill highlight
   (full-width highlight behind text, rounded), non-selected options are FADED
   (reduced opacity), centered text, no radio circles visible. We approximate
   this with CSS: target the selected card to look highlighted, dim others. */
#so-booking-widget[data-template="wheel"] .so-bw-card {
    background: transparent !important;
    border: none !important;
    box-shadow: none !important;
    border-radius: var(--so-bw-r-12) !important;
    padding: 12px 20px !important;
    text-align: center !important;
    opacity: 0.40 !important;
    transition: opacity 0.2s ease, background 0.2s ease !important;
}
#so-booking-widget[data-template="wheel"] .so-bw-card-title {
    color: var(--so-bw-text, #1f2937) !important;
    font-size: 16px !important;
    font-weight: 500 !important;
}
#so-booking-widget[data-template="wheel"] .so-bw-card-meta {
    color: var(--so-bw-text-secondary, #6b7280) !important;
}
/* Selected / focused card: full opacity + blue pill background */
#so-booking-widget[data-template="wheel"] .so-bw-card--selected {
    background: color-mix(in srgb, var(--so-bw-primary, #0a84ff) 14%, #fff) !important;
    border: 1px solid color-mix(in srgb, var(--so-bw-primary, #0a84ff) 30%, #fff) !important;
    opacity: 1 !important;
    box-shadow: none !important;
}
/* First card slightly more visible as the "current row" default */
#so-booking-widget[data-template="wheel"] .so-bw-card:first-child {
    opacity: 0.80 !important;
}
/* Hide card icon for the drum-picker look */
#so-booking-widget[data-template="wheel"] .so-bw-card-icon,
#so-booking-widget[data-template="wheel"] .so-bw-card-check {
    display: none !important;
}
/* Drum container: soft gray roller body with inner-shadow */
#so-booking-widget[data-template="wheel"] .so-bw-card-grid {
    background: #f0f1f5 !important;
    border-radius: var(--so-bw-r-16) !important;
    padding: 8px !important;
    gap: 4px !important;
    box-shadow: inset 0 4px 12px rgba(0, 0, 0, 0.08),
                inset 0 -4px 12px rgba(0, 0, 0, 0.06) !important;
}

/* ── BOARDING-PASS ───────────────────────────────────────────────────────────
   Thumbnail: light card, blue "AURELIA · PASS" header, SERVICE/DATE/TIME tabs,
   white option cards with dark text, AND a BLUE vertical STUB on the RIGHT
   edge (vertical "AURELIA SPA" text + equaliser/barcode bars). Add the blue
   right stub via container::after. */
#so-booking-widget[data-template="boarding-pass"] .so-bw-container {
    padding-right: 64px !important;
    position: relative !important;
    overflow: hidden !important;
    -webkit-mask-image: none !important;
    mask-image: none !important;
    border: 1px solid var(--so-bw-border, #dce3ec) !important;
    border-radius: var(--so-bw-r-8) !important;
}
/* Blue right stub */
#so-booking-widget[data-template="boarding-pass"] .so-bw-container::after {
    content: "" !important;
    position: absolute !important;
    top: 0 !important;
    right: 0 !important;
    bottom: 0 !important;
    width: 60px !important;
    background: var(--so-bw-primary, #2b7de0) !important;
    /* Dashed left separation line (perforation) */
    border-left: 2px dashed rgba(255, 255, 255, 0.35) !important;
    /* Barcode bars pattern using repeating-linear-gradient */
    background-image:
        repeating-linear-gradient(
            0deg,
            transparent 0px,
            transparent 3px,
            rgba(255, 255, 255, 0.20) 3px,
            rgba(255, 255, 255, 0.20) 4px
        ) !important;
    background-color: var(--so-bw-primary, #2b7de0) !important;
    z-index: 2 !important;
}
/* Cards: white with dark text, dashed border */
#so-booking-widget[data-template="boarding-pass"] .so-bw-card {
    background: #ffffff !important;
    border: 1px solid var(--so-bw-border, #dce3ec) !important;
    border-style: solid !important;
    color: var(--so-bw-text, #1f2937) !important;
    border-radius: var(--so-bw-r-6) !important;
}
#so-booking-widget[data-template="boarding-pass"] .so-bw-card-title {
    color: var(--so-bw-text, #1f2937) !important;
}
#so-booking-widget[data-template="boarding-pass"] .so-bw-card-meta {
    color: var(--so-bw-text-secondary, #6b7280) !important;
}
/* On mobile: hide the right stub to avoid layout crush */
/* was @media (max-width: 480px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    #so-booking-widget:where([data-cw~="480"])[data-template="boarding-pass"] .so-bw-container{
        padding-right: 0 !important;
    }
    #so-booking-widget:where([data-cw~="480"])[data-template="boarding-pass"] .so-bw-container::after{
        display: none !important;
    }


/* ── BOLD-GRADIENT ────────────────────────────────────────────────────────────
   Thumbnail: dark LEFT panel (~40% width, black #111, "ATELIER STUDIO" label
   at top, "Let's begin." bold heading, "A booking in four steps." caption at
   bottom), RIGHT pane white with red/accent "CHOOSE A CATEGORY" step-label,
   white option cards, grey Continue. Two-pane split with dark left + white right.
   Use container padding-left + ::before for the left panel (same pattern as
   diagonal-split). The gradient base layout's gradient header must be suppressed. */
#so-booking-widget[data-template="gradient"] .so-bw-container {
    overflow: hidden !important;
    position: relative !important;
    padding-left: 220px !important;
    border-radius: var(--so-bw-r-12) !important;
    background: #ffffff !important;
    box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12) !important;
    border: 1px solid var(--so-bw-border, #e5e7eb) !important;
}
/* Dark left panel */
#so-booking-widget[data-template="gradient"] .so-bw-container::before {
    content: "" !important;
    position: absolute !important;
    top: 0 !important;
    left: 0 !important;
    bottom: 0 !important;
    width: 220px !important;
    background: #111111 !important;
    z-index: 2 !important;
}
/* Header: transparent with right-pane white look */
#so-booking-widget[data-template="gradient"] .so-bw-header {
    background: #ffffff !important;
    border-bottom: 1px solid var(--so-bw-border, #e5e7eb) !important;
    border-radius: 0 !important;
    position: relative !important;
    z-index: 3 !important;
}
/* Step label in accent colour (red-ish per thumb) */
#so-booking-widget[data-template="gradient"] .so-bw-step-label {
    color: var(--so-bw-primary-ink, var(--so-bw-primary, #3b82f6)) !important;
    font-size: 11px !important;
    font-weight: 700 !important;
    letter-spacing: 0.08em !important;
    text-transform: uppercase !important;
}
#so-booking-widget[data-template="gradient"] .so-bw-step-count {
    color: var(--so-bw-text-secondary, #6b7280) !important;
}
/* Cards: white with dark text, single column */
#so-booking-widget[data-template="gradient"] .so-bw-card-grid {
    grid-template-columns: 1fr !important;
}
#so-booking-widget[data-template="gradient"] .so-bw-card {
    background: #ffffff !important;
    border: 1px solid var(--so-bw-border, #e5e7eb) !important;
    border-radius: var(--so-bw-r-6) !important;
    color: var(--so-bw-text, #1f2937) !important;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06) !important;
}
#so-booking-widget[data-template="gradient"] .so-bw-card-title {
    color: var(--so-bw-text, #1f2937) !important;
}
#so-booking-widget[data-template="gradient"] .so-bw-card-meta {
    color: var(--so-bw-text-secondary, #6b7280) !important;
}
/* Step body: white */
#so-booking-widget[data-template="gradient"] .so-bw-step {
    background: #ffffff !important;
    position: relative !important;
    z-index: 3 !important;
}
/* Nav: light grey */
#so-booking-widget[data-template="gradient"] .so-bw-nav {
    background: #ffffff !important;
    border-top: 1px solid var(--so-bw-border, #e5e7eb) !important;
    position: relative !important;
    z-index: 3 !important;
}
/* Error banner: above the left panel */
#so-booking-widget[data-template="gradient"] .so-bw-error {
    position: relative !important;
    z-index: 3 !important;
}
/* Progress: hidden (stepbar is the indicator) */
#so-booking-widget[data-template="gradient"] .so-bw-progress {
    display: none !important;
}
/* On mobile: collapse left panel */
/* was @media (max-width: 600px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    #so-booking-widget:where([data-cw~="600"])[data-template="gradient"] .so-bw-container{
        padding-left: 0 !important;
    }
    #so-booking-widget:where([data-cw~="600"])[data-template="gradient"] .so-bw-container::before{
        display: none !important;
    }

/* Bold Gradient — POPULATE the dark left panel. The ::before above paints an
   EMPTY black rectangle, but the design file shows the left panel branded with
   a headline + caption. The widget already ships a real summary rail (the same
   element minimal/dark/studio use) that's always in the DOM — point this
   template at it, styled dark #111 to match the design, so the panel shows a
   headline + the LIVE booking summary instead of a blank box. The empty
   ::before is suppressed for this template so the two don't stack. Scoped to
   data-template-id="bold-gradient" so the other gradient-base templates keep
   their plain panel. */

/* ── STEPPER-WIZARD ──────────────────────────────────────────────────────────
   Thumbnail: light body, "VIVID · STEP 1 OF 6" accent label, large "Choose a
   category" heading, WHITE cards with dark text, rounded border. The typeform
   base uses a dark navy body (so the previous typeform body-colour rule broke
   stepper-wizard). stepper-wizard gets a LIGHT body + dark text. */

/* ── CHECKOUT ────────────────────────────────────────────────────────────────
   Thumbnail: light grey body, dark numbered-circle accordion — active step
   expanded with white card body + option cards, inactive steps collapsed as
   grey rows with number circles. Classic e-commerce checkout look.
   Left title ".neckout" in dark, right "ORDER SUMMARY" panel area. */
/* Active-step card container: white inset panel */
/* Nav: grey footer */

/* ── GAUGE-STACK ─────────────────────────────────────────────────────────────
   Thumbnail: very light (#fafaf8) warm-white body, large orange "01" step
   number (top-left, huge bold mono), "OF 06 STEPS" subtitle, "0%" right,
   thin progress bar below (orange fill), "Choose a category" heading, white
   outlined-square cards with dark text. Single-pane (no left rail). */
/* Hide the left-rail summary panel — gauge-stack is single-pane */
/* Large bold step number */
/* "OF 06 STEPS" secondary + "0%" percentage */
/* Progress bar: re-enable, orange */
/* Cards: white with 2px border */

/* ── WALLET-PASS / DARK-MODE ─────────────────────────────────────────────────
   Thumbnail wallet-pass: DARK body (#1a1f2e), a vivid TEAL/GREEN rounded
   header panel at top ("BUILDIYO PASS · DRAFT, Build your pass below…"),
   dark body, dark cards with rounded corners, WHITE text.
   The teal header is the .so-bw-header itself styled as a coloured pill/card. */
/* Step label below the teal header: white on dark body */

/* dark-mode: neon accent on dark, card text white */

/* ── GAUGE-STACK stepbar: show numbered stepbar instead of progress bar ───── */
/* Re-enable the progress bar (overrides the global stepbar block) */

/* ── END THUMBNAIL-MATCHED OVERRIDES ──────────────────────────────────────── */

/* ── SINGLE-PANE CORRECTION ───────────────────────────────────────────────────
   The two-pane summary RAIL belongs ONLY to the templates whose design has a
   left panel: modern-minimal, spa-mint, studio-booking, dark-mode. These other
   minimal/dark/studio templates have SINGLE-PANE designs (no side rail) — the
   rail was being inherited from their base layout. Hide it and reclaim the
   container's left padding so the content fills the card (matches the thumbs). */

/* ==========================================================================
   LEGIBILITY PASS — contrast fixes appended last so later source-order
   wins the !important cascade tie against earlier preview-mirror rules.

   Root problem: the "preview-mirror" blocks for [data-template="minimal"]
   (line ~4839) and [data-template="bubble"] (line ~4502) hard-code
     color: #ffffff !important
   on .so-bw-step-label/.so-bw-step-count for the ENTIRE layout family.
   Per-template-id variants that override the header to transparent or a
   light colour (floating-bubble, bubble-chat, zen-studio, bottom-sheet,
   inline-embed, popup-launcher, centre-stage, modern-minimal, spa-mint,
   calendly-classic, clinic-blue) then end up with white text on a near-
   white or transparent surface — invisible.

   Fix strategy: add per-template-id rules with specificity (1,2,0) +
   !important AFTER the preview-mirror block. Same !important priority +
   later source order = these win.
   ========================================================================== */

/* ── A. MINIMAL-layout single-pane templates (transparent / tinted header) ─
   zen-studio / bottom-sheet / centre-stage / inline-embed / popup-launcher
   Header background is transparent or a very light tint; the preview-mirror
   white text is invisible. Force dark text matching each template's palette.
   ─────────────────────────────────────────────────────────────────────────── */

/* zen-studio: earthy serif, cream header → dark warm brown titles */

/* bottom-sheet: tinted-green header → dark text on mint fill */

/* centre-stage: centred warm-cream header → dark brown titles */

/* inline-embed: transparent header (page bg shows through) → dark text */

/* popup-launcher: transparent header → dark indigo text */

/* ── B. MINIMAL-layout two-pane templates (white right-panel header) ─────────
   modern-minimal / calendly-classic / clinic-blue / spa-mint: all share the
   white right-pane header (#ffffff bg from the two-pane correction at line
   ~6163). The preview-mirror white text is invisible on white. Dark text fixes
   this without touching the left rail (which is correctly set per-template).
   ─────────────────────────────────────────────────────────────────────────── */

/* modern-minimal: clean orange-brown rail → dark charcoal right-pane title */

/* calendly-classic: deep blue rail → dark navy right-pane title */

/* clinic-blue: medical blue rail → dark charcoal right-pane title */

/* ── C. SPA-MINT: right-pane background + card surface contrast ───────────────
   The container gradient (#ecfdf5→#fff) tints the right content panel mint-
   green. Cards inherit this mint tint (color-mix primary 8% + #fff = pale
   sage). Both the background and card surface are too similar → text faint.
   Fix: warm off-white container body (matches palette bg #efe9dd tone),
   pure white cards with crisp dark text.
   ─────────────────────────────────────────────────────────────────────────── */
/* White cards on the warm beige body — clear lift + dark crisp text */

/* ── D. BUBBLE-layout transparent-header variants (floating-bubble, bubble-chat)
   The bubble preview-mirror sets white text for the primary-coloured "real"
   bubble-chat header. These two variants override header → transparent, making
   the white text invisible against the light grey container body.
   Dark text restores readability without affecting bubble-chat's green header
   (which is handled by the real bubble-chrome at line ~7653).
   ─────────────────────────────────────────────────────────────────────────── */

/* floating-bubble: header is now the bubble-layout primary-colour band (the
   earlier transparent override was removed at line ~7855 so the green band
   shows). White text from the bubble layout preview-mirror is correct here —
   no override needed; the legibility fix is handled by restoring the header. */

/* bubble-chat: light grey-blue container, transparent header → dark text */

/* ── E. WHEEL-PICKERS: unselected card opacity is too low for legibility ──────
   The drum-picker aesthetic fades non-selected rows to opacity: 0.40. On the
   light #f0f1f5 drum container this renders card titles at effective contrast
   ratio ~1.4:1 — well below WCAG AA (4.5:1). Raise to 0.70 so the text reads
   as a clear mid-grey hierarchy (dim but legible) while preserving the
   drum-picker depth cue. First-card "default" row goes to 0.88.
   ─────────────────────────────────────────────────────────────────────────── */
#so-booking-widget[data-template="wheel"] .so-bw-card {
    opacity: 0.70 !important;
}
#so-booking-widget[data-template="wheel"] .so-bw-card:first-child {
    opacity: 0.88 !important;
}
/* Selected card stays fully opaque */
#so-booking-widget[data-template="wheel"] .so-bw-card--selected {
    opacity: 1 !important;
}
/* Ensure card titles read at full weight inside the drum */
#so-booking-widget[data-template="wheel"] .so-bw-card-title {
    color: #1f2430 !important;
    font-weight: 600 !important;
}
#so-booking-widget[data-template="wheel"] .so-bw-card-meta {
    color: #5b6473 !important;
}

/* ── F. WHEEL-PICKERS header step-label: dark on soft tinted background ───────
   No !important override needed (no preview-mirror conflict for wheel layout)
   but --so-bw-text = #3a4250 may render faint at small size. Explicitly set
   a crisp dark colour so it reads clearly on the soft #f7f7f9 tinted header.
   ─────────────────────────────────────────────────────────────────────────── */
#so-booking-widget[data-template="wheel"] .so-bw-step-label {
    color: #1f2430 !important;
    font-weight: 700 !important;
}
#so-booking-widget[data-template="wheel"] .so-bw-step-count {
    color: #5b6473 !important;
}

/* ── END LEGIBILITY PASS ────────────────────────────────────────────────────── */

/* Three more header titles rendered WHITE on a LIGHT header (measured bg:
   classic-form / compact-sidebar = #f9fafb light grey; glass-modal =
   translucent white) — "Choose a Service" was invisible. Give the two light
   headers a dark title; dark-frost the glass header bar so its white title
   reads (matching the frosted cards). */

/* ── SELECTION FEEDBACK (accent on click) ─────────────────────────────────────
   Clicking a service/staff/location card, a calendar day, or a time slot must
   clearly show the template's ACCENT on EVERY template. The per-template-id card
   rules set border/background with !important (specificity 0,2,0) which was
   overriding the selected-state accent — so selection barely showed, and the old
   solid light-tint background washed out dark-card text. These ID-scoped rules
   (specificity 1,1,0) beat the per-id card rules, mark selection with a 2px
   accent border + ring, and use a TRANSLUCENT accent tint so text stays readable
   on light AND dark cards. */
#so-booking-widget[data-template] .so-bw-card.so-bw-card--selected {
    border-color: var(--so-bw-primary) !important;
    box-shadow: 0 0 0 2px var(--so-bw-primary) !important;
    background: color-mix(in srgb, var(--so-bw-primary) 15%, transparent) !important;
}
#so-booking-widget[data-template] .so-bw-slot-row.so-bw-slot-row--selected,
#so-booking-widget[data-template] .so-bw-slot-row.so-bw-slot-row--selected:hover {
    border-color: var(--so-bw-primary) !important;
    box-shadow: 0 0 0 2px var(--so-bw-primary) !important;
    background: color-mix(in srgb, var(--so-bw-primary) 16%, transparent) !important;
    color: var(--so-bw-text) !important;
    font-weight: 700;
}
#so-booking-widget[data-template] .so-bw-calendar-day.so-bw-calendar-day--selected,
#so-booking-widget[data-template] .so-bw-calendar-day.so-bw-calendar-day--selected:hover {
    background: var(--so-bw-primary) !important;
    box-shadow: 0 0 0 2px var(--so-bw-primary) inset !important;
}

/* ── STEP SIDE PADDING (cards inset from the outer border) ─────────────────────
   The standout layouts had ~0 horizontal step padding, so service/location
   cards and field rows ran edge-to-edge against the container's outer border
   (cramped). Inset the step content so cards sit comfortably within the card,
   matching the design's generous padding. */
#so-booking-widget[data-template="typeform"] .so-bw-step,
#so-booking-widget[data-template="swipe"] .so-bw-step,
#so-booking-widget[data-template="stories"] .so-bw-step,
#so-booking-widget[data-template="glass"] .so-bw-step,
#so-booking-widget[data-template="wheel"] .so-bw-step {
    padding-left: 24px !important;
    padding-right: 24px !important;
}
/* Timeline left this group with the fixed-height rule below, for the same
   reason: every other member is retired. Its step inset now comes from the
   shared `--so-bw-gutter` token that §17.10/M1 sets on the container, which is
   what widget-css-cascade.test.ts asks for — a literal x-padding here is how
   the "stair-step" (each band on a slightly different left edge) comes back. */
#so-booking-widget[data-template="timeline"] .so-bw-step {
    padding-left: var(--so-bw-gutter);
    padding-right: var(--so-bw-gutter);
    /* The step title above already carries the top inset, and row 1 hands this
       row whatever slack the brand block left over — so the step's own 24px on
       top of both is the third helping of the same space. */
    padding-top: 14px;
}
/* diagonal keeps its left teal-panel offset; only inset the right content. */
#so-booking-widget[data-template="diagonal"] .so-bw-step { padding-right: 24px !important; }
/* boarding-pass keeps its right ticket-stub; only inset the left content. */
#so-booking-widget[data-template="boarding-pass"] .so-bw-step { padding-left: 24px !important; }

/* was @media (min-width: 768px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    /* Standout layouts ALSO become fixed-height with a scrolling card list.
       They were intentionally left to grow tall, but users expect the card
       list to scroll within the card (a scroll bar), not stretch the page. The
       global .so-bw-body rule already makes the body the scroll region. */
    #so-booking-widget:where([data-cwmin~="768"])[data-template="typeform"] .so-bw-container,
    #so-booking-widget:where([data-cwmin~="768"])[data-template="swipe"] .so-bw-container,
    #so-booking-widget:where([data-cwmin~="768"])[data-template="stories"] .so-bw-container,
    #so-booking-widget:where([data-cwmin~="768"])[data-template="glass"] .so-bw-container,
    #so-booking-widget:where([data-cwmin~="768"])[data-template="wheel"] .so-bw-container,
    /* Timeline is NOT in this list any more. Every other id here is retired
       (see WidgetTemplateCatalog::RETIRED_LAYOUTS); Timeline was the one live
       member of a group whose "standout skins get a fixed-height card" premise
       no longer exists. It mattered concretely: this rule sets
       `display: flex !important`, which outranks the container GRID that
       §17.10/M1 now builds the sidebar with, so leaving it here silently
       collapsed the two-column shell back to a stack above 768px. Timeline
       states its own max-height in M1. */
    #so-booking-widget:where([data-cwmin~="768"])[data-template="boarding-pass"] .so-bw-container,
    #so-booking-widget:where([data-cwmin~="768"])[data-template="diagonal"] .so-bw-container{
        display: flex !important;
        flex-direction: column !important;
        max-height: min(78vh, 660px) !important;
    }


/* ── CARD HOVER (accent, consistent on every template) ────────────────────────
   Some templates used a hard-coded grey/dark hover (swipe #37303e, glass dark
   frost) or none (typeform), so hover didn't match the template's colour. Give
   every card an accent border + 1px ring + a SUBTLE accent tint (inset shadow,
   so the card's own background and text are preserved) + a soft lift. Specificity
   1,4,0 + !important beats the per-template hovers and the per-id card border. */
#so-booking-widget[data-template] .so-bw-card:hover:not(.so-bw-card--selected) {
    border-color: var(--so-bw-primary) !important;
    box-shadow: inset 0 0 0 999px color-mix(in srgb, var(--so-bw-primary) 8%, transparent),
                0 0 0 1px var(--so-bw-primary),
                0 6px 16px -8px rgba(0, 0, 0, 0.28) !important;
}

/* ==========================================================================
   THUMBNAIL-MATCHED HEADER OVERRIDES — appended last so later source-order
   wins at equal specificity. Brings each template's header (title, step
   indicator, background/treatment) in line with the design thumbnails at
   assets/template-thumbs/<id>.png.
   Specificity: [data-template-id] (0,2,0) or [data-template] (0,2,0) + later
   source order beats every earlier rule at the same specificity.
   ========================================================================== */

/* ── TYPEFORM (data-template-id="typeform") ──────────────────────────────────
   Thumbnail: dark navy body, large BOLD white "Choose a category" title,
   small "1 → Choose a category" breadcrumb. Earlier id-rule set font-weight:300
   (ultra-light) making the title faint. Fix: bold 700+, white on dark navy. */

/* ── KIOSK (data-template-id="kiosk") ───────────────────────────────────────
   Thumbnail: DARK NAVY body (#0e1a30), small teal "SELF SERVICE" eyebrow at
   top-left, large WHITE bold title "Choose a category", small "1/6" counter
   at top-right. Earlier rule set header background to var(--so-bw-primary)
   which rendered as a solid teal band. Fix: dark navy header, teal accent only
   as a tiny ::before eyebrow, white title, compact 1/6 step-count. */
/* "SELF SERVICE" teal eyebrow via pseudo-element above the step-indicator */
/* Step count: compact "1/6" style top-right */
/* Stepbar: hide the numbered stepbar; the "1/6" count + teal eyebrow is enough */
/* Progress bar: slim teal edge at container top */

/* ── GAUGE-STACK (data-template-id="gauge-stack") ───────────────────────────
   Thumbnail: light warm body, large orange "01" step NUMBER top-left (big
   mono bold), "OF 06 STEPS" small subtitle, "0%" percentage top-right, then
   a thin orange progress bar, then NORMAL-SIZED dark "Choose a category" title
   below. The earlier rule made the step-LABEL huge orange — wrong, because
   .so-bw-step-label = the page TITLE ("Choose a category") and
   .so-bw-step-count = "Step 1 of 6" (the number indicator).
   Fix: step-count → huge orange mono "01" number; step-label → normal dark. */
/* Stack the indicator vertically so the big number sits above the title */
/* "STEPS" label after the step number */

/* ── BOARDING-PASS (data-template-id="boarding-pass") ───────────────────────
   Thumbnail: "Choose a category" in normal mixed case (not all-caps).
   The id-rule had text-transform: uppercase on the step-label — remove it. */

/* ── SERVICE-GRID (data-template-id="service-grid") ─────────────────────────
   Thumbnail: vivid BLUE filled header band ("Book a Service" title in white,
   "Choose a category" subtitle below in smaller white text), dot-pill progress
   top-right. Earlier rule had background: transparent — wrong colour treatment.
   Fix: filled primary-blue header, white title + step-count. */
/* Dot-pill progress top-right on the blue band */

/* ── STORIES (data-template-id="stories") ───────────────────────────────────
   Thumbnail: dark purple body, white bold large title, top segmented progress.
   Step-label needs explicit white on the dark body. */

/* ── GLASS-MODAL (data-template-id="glass-modal") ───────────────────────────
   Thumbnail: gradient purple→magenta body, frosted modal card, white title.
   Step-label and step-count must be white on the frosted/gradient header. */

/* ── SWIPE-DECK (data-template-id="swipe-deck") ─────────────────────────────
   Thumbnail: dark charcoal/plum body, white bold "Choose a category" title,
   dot progress top-right. The swipe layout's preview-mirror block sets white
   text globally, but the id rule may not inherit it if source order differs.
   Explicitly set white title on dark body. */

/* ── WALLET-PASS (data-template-id="wallet-pass") ───────────────────────────
   Thumbnail: dark body (#1a1f2e), vivid teal rounded pill header with white
   title + subtitle. The step-label shown is "Choose a category" in white on
   the teal pill. Ensure strong white + ensure the pill header sits clearly
   distinct from the dark body. */

/* ── DARK-MODE (data-template-id="dark-mode") ───────────────────────────────
   Thumbnail (dark two-pane layout): dark body, white step title in right pane.
   The dark layout's two-pane rail already provides the left panel. The
   step-label needs strong white so it reads on the dark transparent header. */

/* ── COMPACT-SIDEBAR (data-template-id="compact-sidebar") ───────────────────
   Thumbnail: plain white/light header, small "STEP 1 / 6" accent label at top
   then larger "Choose a category" title. The compact layout forces a filled
   primary-color header but the id-rule overrides to light. The id-level step-
   label is already dark (#1a1a2e) from the legibility pass. Refine: make the
   step-count display as a small accent "STEP X / Y" label above the title,
   and give the step-label a clear prominent size. */
/* Stepbar: hide filled compact stepbar (light header needs minimal chrome) */
/* Progress: slim top accent bar */

/* ── BOTTOM-SHEET (data-template-id="bottom-sheet") ─────────────────────────
   Thumbnail: white bottom-sheet content area, the big green splash is a
   decorative area outside the widget step flow. Inside the step, "Choose a
   category" is a normal dark title on a white surface. The step-count shows
   as a thin progress row of dots. Ensure dark text on white. */

/* ── ZEN-STUDIO (data-template-id="zen-studio") ─────────────────────────────
   Thumbnail: warm cream/off-white body, small uppercase "AURELIA - CONCIERGE"
   eyebrow in muted tone, then ITALIC SERIF large "Choose a category" title.
   The eyebrow is a brand name but we can show a generic "STEP X OF Y" style.
   Ensure the title uses the serif font at comfortable size. */

/* ── CENTRE-STAGE (data-template-id="centre-stage") ─────────────────────────
   Thumbnail: pill-nav stepbar along the top, centered large title below.
   The centre-stage header has a warm tinted bg; title must be centered + dark.
   Stepbar is the studio/minimal layout's numbered stepbar; keep it visible
   across the top as the thumbnail shows pill nav tabs. */

/* ── DIAGONAL-SPLIT (data-template-id="diagonal-split") ─────────────────────
   Thumbnail: right-pane header has "Choose a category" as normal dark title
   (not italic, not bold-heading). Currently has font-style: normal from
   the earlier correction. Keep title clearly readable + properly sized. */

/* (A "TIMELINE thumbnail-matched header" heading sat here with no declarations
   under it, describing a header nothing implemented. Timeline's chrome is
   stated in §17.10/M1.) */
/* ── END THUMBNAIL-MATCHED HEADER OVERRIDES ──────────────────────────────── */

/* typeform: the CONTAINER was transparent (only the step had the dark gradient),
   so the header floated on the white page and its white title was invisible.
   Give the whole container the dark navy gradient — one cohesive dark card, with
   the header's white title now reading on dark (matches the design). */

/* ── CONFIRMATION SUMMARY BOX on dark themes ──────────────────────────────────
   The Booking-Received summary box uses --so-bw-bg-secondary (light by default)
   with light label/value text (--so-bw-text-secondary / --so-bw-text). On the
   dark-theme templates that left bg-secondary light, the labels and values were
   light-on-light = invisible (only "Pending"/headings read). Make the box a dark
   translucent panel matching the dark card so the light text is legible. */

/* typeform header is LEFT-aligned in the design (small "1 →" eyebrow + a
   left-aligned big title), not centered. Override the centered header. */
#so-booking-widget[data-template="typeform"] .so-bw-header {
    text-align: left !important;
    padding-left: 24px !important;
    padding-right: 24px !important;
}
#so-booking-widget[data-template="typeform"] .so-bw-step-indicator { align-items: flex-start !important; }
#so-booking-widget[data-template="typeform"] .so-bw-step-label,
#so-booking-widget[data-template="typeform"] .so-bw-step-count { text-align: left !important; }

/* "Change date · <date>" — the date sits after a middot separator with proper
   spacing (the old bare text-node separator was dropped by the x-if template). */
.so-bw-slot-back-date { font-weight: 700; }
/* `\0000B7`, not a literal "·". This stylesheet is served as `text/css` with no
   charset parameter and carries no BOM and no @charset rule, so the browser
   decodes it with the ENVIRONMENT encoding — the referring document's. That is
   UTF-8 on a normal WordPress page, which is why this looks fine most of the
   time; but anywhere the referrer is not UTF-8 (a themed page declaring
   ISO-8859-1, a cached response replayed without the parent, some proxies) the
   two UTF-8 bytes C2 B7 are read as Latin-1 and the separator renders as "Â·"
   next to the date. A CSS escape is pure ASCII in the file, so it decodes to
   the same character under every encoding. Six digits rather than four so no
   following space is swallowed as the escape terminator — the same reason the
   search hint below already writes `\21B5`. */
.so-bw-slot-back-date::before { content: "\0000B7"; margin: 0 7px; opacity: 0.5; font-weight: 400; }

/* diagonal-split: LIVE booking summary in the teal panel (Service / Specialist /
   Date / Time) like the design — the two-pane rail rendered transparently over
   the angled ::before teal background, with the title swapped for the tagline.
   Stacked label-above-value rows so long values (full date, staff name) wrap by
   word instead of overflowing the narrow column. */
#so-booking-widget[data-template="diagonal"] .so-bw-container::before { width: 220px !important; }

/* FONT — the whole widget follows the design file's typeface (Inter), loaded via
   the @import at the very top of this file. This overrides the per-template font
   tokens (the previous mono/Nunito/Georgia tokens were never loaded and fell
   back to system fonts, so no template matched the design).

   `:not([data-font])` is what keeps it from also overriding the ADMIN. The
   Studio's Font Family picker is wired end to end — BookingShortcode resolves
   the preset, emits `--so-bw-font` in its inline block, and even emits a Google
   Fonts <link> for Roboto / Open Sans / Poppins so the face is really loaded —
   but that declaration is not `!important`, and `!important` wins over any
   specificity. So the reset silently ate every choice and the picker rendered
   Inter on all nineteen templates. Verified on the live install: the inline CSS
   said Poppins, the <link> was present, computed style said Inter.

   The shortcode now marks the root `data-font="<preset>"` whenever the choice is
   a recognised preset, so this reset keeps doing its real job — overriding the
   dead per-TEMPLATE tokens — and stands aside for a deliberate one. */
#so-booking-widget[data-template]:not([data-font]) { --so-bw-font: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important; }
/* …but a template that redefines --so-bw-font on .so-bw-container must still
   win inside it. font-family is declared on #so-booking-widget (the ROOT), so
   the container inherited the already-RESOLVED value — Inter — and its own
   token was computed, inherited by every descendant, and never used for
   anything. That is why Editorial rendered as Inter with only the two elements
   naming the face explicitly (rail title, step label) coming out serif, and
   why it was indistinguishable from the other two Split templates. Re-declaring
   font-family here re-resolves the token at the container, so a per-template
   face reaches the whole widget. Inter everywhere else: the token is unchanged
   for templates that don't override it. */
#so-booking-widget .so-bw-container { font-family: var(--so-bw-font); }

/* Add-ons are now a dedicated step shown right after the service is selected —
   hide the inline on-card "+ Add extras" toggle and its expanding panel. */
#so-booking-widget .so-bw-card-extras-toggle,
#so-booking-widget .so-bw-card-extras { display: none !important; }
/* ==========================================================================
   The per-template signature blocks that used to live here (one ~120-line
   block per template, hardcoded hex + !important) were removed by the
   Guided Booking Layouts consolidation. They predated the design system and
   restated colours the shortcode already emits as --so-bw-primary, which
   meant every palette change had to be made twice and the two drifted.
   Signatures now live in section 15 at the end of the file and set hue and
   character only. See section 13 for why.
   ========================================================================== */

/* ==========================================================================
   13. GUIDED-BOOKING DESIGN SYSTEM
   --------------------------------------------------------------------------
   From the "Guided Booking Layouts" design file. That file draws the SAME
   booking — same five steps, same moment (choosing a date and time) — twelve
   different ways, and holds everything except architecture constant: one type
   scale, one neutral ramp, one spacing rhythm, one control behaviour.

   This section encodes the constants. Section 14 encodes the twelve
   architectures. Section 15 gives each template its hue and character and
   nothing else. The rule the file is built on, and the rule to keep:

       ARCHITECTURE decides where things go.
       The SIGNATURE decides only what colour and character they have.

   A template that needs to move something is asking for an architecture,
   not a signature override.
   ========================================================================== */

#so-booking-widget {
    /* Neutral ramp. Nine steps, warm-grey, tuned so 500 on white and 700
       on n50 both clear WCAG AA at body size. Templates set a hue; they
       never restate the ramp. */
    --so-bw-ds-n0:   #ffffff;
    --so-bw-ds-n25:  #fafafb;
    --so-bw-ds-n50:  #f4f5f7;
    --so-bw-ds-n100: #ebecf0;
    --so-bw-ds-n200: #dfe1e6;
    --so-bw-ds-n300: #c1c7d0;
    --so-bw-ds-n500: #5a6172;
    --so-bw-ds-n700: #42485a;
    --so-bw-ds-n900: #1b1f2a;

    /* IBM Plex Mono carries every NUMBER in the widget — times, dates,
       prices, step counts — and the uppercase micro-labels. It is the one
       piece of the design file's type system adopted wholesale, because
       tabular numerals are what stop a column of times from looking ragged.
       Body copy stays on Inter (and Newsreader / Space Grotesk for the
       serif and geometric templates), so no existing template shifts. */
    --so-bw-mono: 'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, monospace;

    /* One elevation, used by every card in every architecture. The long
       soft ambient shadow does the lifting; the 1px contact shadow keeps
       the edge crisp on white. */
    --so-bw-ds-shadow: 0 24px 48px -20px rgba(16, 18, 26, 0.18), 0 1px 2px rgba(16, 18, 26, 0.05);
    --so-bw-ds-shadow-sm: 0 12px 28px -14px rgba(16, 18, 26, 0.22);

    /* Every control in the design file is at least 44px tall — the whole
       file, not just the mobile breakpoint. Slots, day cells, buttons and
       list rows all inherit it, so nothing is a small target on a phone. */
    --so-bw-ds-tap: 44px;

    /* 120ms is the file's single duration. Fast enough that a tap feels
       instant, slow enough that a colour change is legible. */
    --so-bw-ds-motion: 120ms ease;

    /* Expressed on the shared step scale below, so the Appearance tab's
       Border Radius slider reaches the design system too. At the stock
       scale these still compute to 8 / 10 / 12 / 16px exactly. */
    --so-bw-ds-radius-chip: var(--so-bw-r-8);
    --so-bw-ds-radius-ctl: var(--so-bw-r-10);
    --so-bw-ds-radius-card: var(--so-bw-r-12);
    --so-bw-ds-radius-shell: var(--so-bw-r-16);

    /* ── THE AXIS PRIMITIVE ────────────────────────────────────────────────
       "A node on a line" is drawn in six places — Timeline's step rail and day
       axis, the axis form fields and the itinerary (both shared with Agenda),
       and the confirmation pin. Every one of them used to hand-derive its own
       offset from a gutter written down in prose, which is how they ended up
       with six node sizes, two line colours and a 1px drift nobody could
       explain (the confirmation pin sat at -23 where its own arithmetic said
       -22).

       The contract is two numbers per site and one formula:

         --so-bw-axis-x     the LINE'S CENTRE, measured from the padding box of
                            whatever element the node is positioned in. Negative
                            when the line sits outside that element, which is the
                            usual case — the node hangs off to the left.
         --so-bw-axis-node  the node's diameter.

         node left = calc(var(--so-bw-axis-x) - var(--so-bw-axis-node) / 2)
         line left = calc(var(--so-bw-axis-x) - var(--so-bw-axis-line-w) / 2)

       Set the two numbers, use the two formulas, and the node cannot fall off
       the line. Change a padding and only --so-bw-axis-x moves.

       The values below are defaults, not the whole story: each site overrides
       --so-bw-axis-x and --so-bw-axis-node for its own geometry. */
    --so-bw-axis-line-w: 2px;
    --so-bw-axis-node: 8px;
    --so-bw-axis-x: 0px;
    --so-bw-axis-line: var(--so-bw-ds-n100, #ebecf0);
    --so-bw-axis-ring: var(--so-bw-ds-n200, #dfe1e6);
}

/* ──────────────────────────────────────────────────────────────────────────
   13a. THE SHARED RADIUS SCALE
   --------------------------------------------------------------------------
   One multiplier, one ladder of steps, every corner in the file.

   The Appearance tab's Border Radius slider used to write only the eight
   SEMANTIC tokens (--so-bw-radius*, --so-bw-btn-radius, --so-bw-card-radius,
   --so-bw-input-radius, --so-bw-summary-radius). That reached the shell,
   cards, buttons and inputs — and nothing else. Two whole families sat
   outside it:

     - the --so-bw-ds-radius-* set above, which ~80 declarations read
       DIRECTLY (the calendar, slot panels, staff-profile panels, the
       confirmation block, the search input) and which also re-seats
       --so-bw-slot-radius and --so-bw-calendar-day-radius. Time slots and
       day cells therefore held 8px corners on every template at every
       slider position;
     - ~190 hardcoded px radii inside the per-template blocks, many of them
       !important, which no token override can reach at all. Those are the
       corners that give a template its character, so they are also the ones
       an admin most notices refusing to move.

   Both are now expressed as steps on ONE scale. A step is the value the
   design already used, multiplied by the slider's scale factor:

       --so-bw-r-14  ==  14px at the stock setting, 0 at Sharp, 35 at Pill

   So at the default the file computes byte-for-byte the values it did
   before — no template's look shifts until the slider moves — while 0 makes
   every corner in the widget genuinely square and 20 rounds all of them in
   proportion. A template that draws an asymmetric signature (the chat tail's
   18/18/18/4, Coral Pop's 22px cards) keeps that signature at every setting,
   because each component scales by the same factor and the ratio survives.
   Concentric pairs — a card inside a clipped shell, a media header against
   its container — stay concentric for the same reason.

   Two multipliers, not one. Anything from 18px up is already a shell-sized
   corner; at 2.5x a 28px dialog would land on 70px and read as a lozenge
   rather than a rounded panel. Those steps run on the capped scale instead,
   which tops out at 1.75 — 28px -> 49px, close to the 40px an uncapped
   --so-bw-radius-xl reaches at the same setting, so the two families still
   agree at the top of the slider.

   Declared HERE, on #so-booking-widget, and deliberately not on :root: a
   custom property that references another resolves against the element that
   declares it, and BookingShortcode writes the scale onto #so-booking-widget.
   Declaring the ladder at :root would resolve every step against the stock
   scale and the slider would move nothing. Everything the widget renders is
   a descendant of this element, so inheritance carries the steps down.
   ────────────────────────────────────────────────────────────────────────── */
#so-booking-widget {
    /* Overwritten by BookingShortcode when the admin moves the slider.
       Stock value is 1, i.e. every step below is its own design value. */
    --so-bw-radius-scale: 1;
    --so-bw-radius-scale-shell: 1;

    --so-bw-r-1: calc(1px * var(--so-bw-radius-scale));
    --so-bw-r-2: calc(2px * var(--so-bw-radius-scale));
    --so-bw-r-3: calc(3px * var(--so-bw-radius-scale));
    --so-bw-r-4: calc(4px * var(--so-bw-radius-scale));
    --so-bw-r-6: calc(6px * var(--so-bw-radius-scale));
    /* 7, 9 and 11 are the odd steps Wide Canvas, Week Board and Tabbed Rail
       were drawn on. They are here rather than rounded to the neighbouring
       even step because the ladder's job is to let every corner follow the
       slider, not to re-decide what those templates look like — snapping them
       would change three shipped designs to save three declarations. The
       ladder already carries 1, 2, 3, 13 and 15 for the same reason. */
    --so-bw-r-7: calc(7px * var(--so-bw-radius-scale));
    --so-bw-r-8: calc(8px * var(--so-bw-radius-scale));
    --so-bw-r-9: calc(9px * var(--so-bw-radius-scale));
    --so-bw-r-10: calc(10px * var(--so-bw-radius-scale));
    --so-bw-r-11: calc(11px * var(--so-bw-radius-scale));
    --so-bw-r-12: calc(12px * var(--so-bw-radius-scale));
    --so-bw-r-13: calc(13px * var(--so-bw-radius-scale));
    --so-bw-r-14: calc(14px * var(--so-bw-radius-scale));
    --so-bw-r-15: calc(15px * var(--so-bw-radius-scale));
    --so-bw-r-16: calc(16px * var(--so-bw-radius-scale));
    /* The outer edge at "Full Width", where the shell spans the page and has
       no background to round against. Deliberately NOT a step on the ladder
       and deliberately not scaled: the whole point of the Full Width block
       near the end of this file is that the outer edge stops tracking the
       setting while everything inside it keeps following. Set this to a small
       px value if a squared band ever reads too hard against a particular
       theme — one place, no selector changes. */
    --so-bw-fullwidth-shell-radius: 0;

    /* Shell-sized from here down — capped scale, see the note above. */
    --so-bw-r-18: calc(18px * var(--so-bw-radius-scale-shell));
    --so-bw-r-20: calc(20px * var(--so-bw-radius-scale-shell));
    --so-bw-r-22: calc(22px * var(--so-bw-radius-scale-shell));
    --so-bw-r-24: calc(24px * var(--so-bw-radius-scale-shell));
    --so-bw-r-26: calc(26px * var(--so-bw-radius-scale-shell));
    --so-bw-r-28: calc(28px * var(--so-bw-radius-scale-shell));
}

/* Re-seat the widget's semantic tokens on the ramp above, so every one of
   the nineteen templates inherits the same neutrals unless its signature
   says otherwise. Scoped to the widget root, declared once. */
#so-booking-widget .so-bw-container {
    --so-bw-text: var(--so-bw-ds-n900);
    --so-bw-text-secondary: var(--so-bw-ds-n500);
    --so-bw-text-tertiary: var(--so-bw-ds-n300);
    --so-bw-bg-secondary: var(--so-bw-ds-n25);
    --so-bw-bg-tertiary: var(--so-bw-ds-n50);
    --so-bw-border: var(--so-bw-ds-n200);
    --so-bw-header-border: var(--so-bw-ds-n100);
    --so-bw-transition: var(--so-bw-ds-motion);
    --so-bw-shadow-md: var(--so-bw-ds-shadow);
    --so-bw-radius-xl: var(--so-bw-ds-radius-shell);
    --so-bw-card-radius: var(--so-bw-ds-radius-card);
    --so-bw-btn-radius: var(--so-bw-ds-radius-ctl);
    --so-bw-input-radius: var(--so-bw-ds-radius-ctl);
    --so-bw-slot-radius: var(--so-bw-ds-radius-chip);
    --so-bw-calendar-day-radius: var(--so-bw-ds-radius-chip);
    --so-bw-btn-pad-y: 12px;
    --so-bw-btn-font-size: 14px;
    /* Was a raw `--so-bw-header-pad-x: 32px`, which put the header 8px
       further in than the step content and the nav (both 24px) on every
       architecture. The header follows the gutter now.
       ── …and it has to be bound HERE, not in section 1 ──────────────────
       Section 1 binds the same token to the same gutter, but on
       `#so-booking-widget`. A `var()` resolves against the element that
       DECLARES it, so that binding always read the ROOT's gutter (24px) —
       while `--so-bw-gutter` is overridden on `.so-bw-container` (16px on a
       phone, 20px at the tablet tier, just below). The header therefore kept
       24px at every width while the step and the nav followed the container,
       and the two section-12 tiers then pinned it to raw 32px / 22px on top.
       MEASURED before this line existed, on every architecture:

           container   header   step   nav
              1040       24      24     24
               900       24      24     24
               700       32      20     20     ← 12px of drift
               420       22      16     16     ←  6px of drift

       Declared on the container, it resolves against the container's gutter,
       so every tier — including tiers added later — is covered without
       restating the value. The section-1 binding stays as the fallback for a
       widget rendered without a `.so-bw-container`.

       The four architectures that deliberately choose their own inset
       (compact, studio) declare it on `[data-template="…"] .so-bw-container`
       at (1,2,0), so they still outrank this. */
    --so-bw-header-pad-x: var(--so-bw-gutter);
    --so-bw-header-label-size: 17px;
}

/* ── Responsive tiers for the tokens the block above owns ────────────────
   The tiers in section 12 set their tokens on `#so-booking-widget`, which is
   (1,0,0). The block above sets its tokens on `#so-booking-widget
   .so-bw-container`, which is (1,1,0) — and MEDIA QUERIES ADD NO
   SPECIFICITY. So for every token the block above declares, the section-12
   tier lost at every breakpoint and the mobile value never rendered:

     --so-bw-header-pad-x     wanted 22px, stayed 32px  (a 16px stair-step
                              against the step content's 16px on a phone)
     --so-bw-header-label-size wanted 16px, stayed 17px (header never
                              compacted)
     --so-bw-radius-xl        wanted 0,    stayed the shell radius (so the
                              card never went edge-to-edge on a phone, which
                              is the whole mobile treatment)

   Restated here on the SAME selector and AFTER it, so source order decides
   and the tier actually applies. Only the shadowed tokens belong here — the
   other twenty the section-12 tiers set are not declared above and work.

   The gutter tracks --so-bw-step-pad at each tier (24/20/16) rather than the
   old header aspiration of 22px: header and step share one token now, so a
   single value has to serve both, and matching the step keeps the content
   column exactly where it renders today while bringing the header and nav in
   to meet it. */
/* was @media (max-width: 480px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    #so-booking-widget:where([data-cw~="480"]) .so-bw-container{
        --so-bw-gutter: 16px;
        --so-bw-header-label-size: 16px;
        --so-bw-radius-xl: 0;
    }

/* was @media (min-width: 481px) and (max-width: 767px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    #so-booking-widget:where([data-cwmin~="481"][data-cw~="767"]) .so-bw-container{
        --so-bw-gutter: 20px;
        --so-bw-header-label-size: 17px;
    }


/* Dark-first templates keep the ramp inverted rather than re-declaring it. */
#so-booking-widget[data-theme="dark"] .so-bw-container,
#so-booking-widget[data-template-id="dusk-champagne"] .so-bw-container {
    --so-bw-text-secondary: #8b8fa0;
    --so-bw-text-tertiary: #565b6b;
}

/* ── Numerals ────────────────────────────────────────────────────────────
   Anything that is a quantity gets the mono face and tabular figures. */
#so-booking-widget .so-bw-step-count,
#so-booking-widget .so-bw-calendar-day,
#so-booking-widget .so-bw-time-slot,
#so-booking-widget .so-bw-slot-row,
#so-booking-widget .so-bw-rail-v,
#so-booking-widget .so-bw-rail-total span:last-child,
#so-booking-widget .so-bw-calendar-dow {
    font-family: var(--so-bw-mono);
    font-variant-numeric: tabular-nums;
    font-feature-settings: 'tnum' 1;
}

/* ── Micro-labels ────────────────────────────────────────────────────────
   The file's quietest device: 11px mono, wide tracking, uppercase. Used for
   section eyebrows and the summary keys. Never for anything a user must
   read at length. */
#so-booking-widget .so-bw-rail-k,
#so-booking-widget .so-bw-rail-total span:first-child,
#so-booking-widget .so-bw-slot-group-title {
    font-family: var(--so-bw-mono);
    font-size: 10.5px;
    font-weight: 500;
    letter-spacing: 0.14em;
    text-transform: uppercase;
}

/* ── Targets ─────────────────────────────────────────────────────────────
   44px minimum, everywhere, at every width. */
#so-booking-widget .so-bw-btn,
#so-booking-widget .so-bw-time-slot,
#so-booking-widget .so-bw-slot-row,
#so-booking-widget .so-bw-input,
#so-booking-widget .so-bw-select {
    min-height: var(--so-bw-ds-tap);
    transition: all var(--so-bw-ds-motion);
}
/* Slot rows deliberately excluded: they are full-width flex rows (see the
   base .so-bw-slot-row rule), not inline pills. This used to also name
   .so-bw-time-slot — a class the widget has not rendered for a long time. */
#so-booking-widget .so-bw-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
}
#so-booking-widget .so-bw-calendar-day {
    min-height: 40px;
    transition: all var(--so-bw-ds-motion);
}

/* One elevation on the shell, one on anything that floats above it.

   THROUGH `--so-bw-shadow-md`, NOT `--so-bw-ds-shadow` DIRECTLY, and the
   difference is the Studio's Shadow control working or not.

   The base rule for `.so-bw-container` reads `--so-bw-shadow-md` (~291) — the
   token BookingShortcode writes when an admin picks an elevation. This rule
   overrides that at higher specificity, and reading the design-system literal
   here severed the chain: the control wrote a token nothing on the shell read,
   so the setting was inert on all twenty-three templates.

   Naming the token instead is APPEARANCE-NEUTRAL. The block above declares
   `--so-bw-shadow-md: var(--so-bw-ds-shadow)` on this very element, so with the
   control untouched this computes to exactly what the literal did. What
   changes is only that an admin's choice now has somewhere to land.

   The other half of the fix is in BookingShortcode: the tokens are emitted on
   the container rather than the widget root, because a declaration on the
   container — the one above — shadows an inherited value from the root no
   matter how specific the root rule is. */
#so-booking-widget .so-bw-container {
    box-shadow: var(--so-bw-shadow-md);
}


/* ==========================================================================
   14. THE TWELVE ARCHITECTURES
   --------------------------------------------------------------------------
   One block per layout study. Each answers the same four questions
   differently: where does progress live, how much context stays on screen,
   are steps sequential or simultaneous, and how is the commit framed.

   All of them draw their accent from --so-bw-primary, which the shortcode
   emits per template. No architecture hardcodes a hue.
   ========================================================================== */

/* ── A · CENTRED (cloud-violet) ──────────────────────────────────────────
   Progress across the top, one narrow column beneath. Least chrome, most
   focus. The safe default: nothing on screen competes with the question. */
#so-booking-widget[data-template="centred"] .so-bw-container {
    max-width: 720px;
    margin-inline: auto;
}
#so-booking-widget[data-template="centred"] .so-bw-header {
    background: var(--so-bw-ds-n0);
    padding-bottom: 22px;
}
#so-booking-widget[data-template="centred"] .so-bw-step-indicator {
    flex-direction: column;
    align-items: center;
    gap: 4px;
    text-align: center;
}
/* The numbered stepper spans the full width with connecting rules, so the
   whole journey is legible before the first answer. */
#so-booking-widget[data-template="centred"] .so-bw-stepbar {
    margin-top: 18px;
    gap: 0;
}
#so-booking-widget[data-template="centred"] .so-bw-stepbar-item {
    flex: 1;
    flex-direction: column;
    gap: 7px;
    position: relative;
}
#so-booking-widget[data-template="centred"] .so-bw-stepbar-item::before,
#so-booking-widget[data-template="centred"] .so-bw-stepbar-item::after {
    content: '';
    position: absolute;
    top: 13px;
    height: 1.5px;
    width: 50%;
    background: var(--so-bw-ds-n200);
}
#so-booking-widget[data-template="centred"] .so-bw-stepbar-item::before { left: 0; }
#so-booking-widget[data-template="centred"] .so-bw-stepbar-item::after { right: 0; }
/* NB :first-of-type / :last-of-type below, NOT :first-child — Alpine leaves
   the <template x-for> in the DOM as child 0, so the first rendered item is
   never :first-child and the outer half-connector was being painted. The
   items are <div> and the template is <template>, so of-type is exact. */
#so-booking-widget[data-template="centred"] .so-bw-stepbar-item--done::before,
#so-booking-widget[data-template="centred"] .so-bw-stepbar-item--done::after,
#so-booking-widget[data-template="centred"] .so-bw-stepbar-item--active::before { background: var(--so-bw-primary); }
/* The rail runs BETWEEN steps, so the outer half-segments either side of
   the row are never drawn. This has to come last: the accent rules above
   score identically, and step 1 is both :first-child and --active, so on
   source order alone it would grow a stub of line pointing off the left
   edge into nothing. */
#so-booking-widget[data-template="centred"] .so-bw-stepbar-item:first-of-type::before,
#so-booking-widget[data-template="centred"] .so-bw-stepbar-item:last-of-type::after { background: transparent; }
#so-booking-widget[data-template="centred"] .so-bw-stepbar-dot {
    position: relative;
    z-index: 1;
    width: 26px;
    height: 26px;
    border-radius: 50%;
}
#so-booking-widget[data-template="centred"] .so-bw-progress { display: none; }
/* THE MEASURE, not the old 460px literal — but the month is the one block that
   is then deliberately narrowed again, and section 18.1 does it: 392px, because
   "unbounded, Centred's 520px measure produced 68px circles — a month that
   reads as a control panel rather than a date". That override is intentional
   and stays; this rule exists so the month is bounded BY the measure rather
   than by a number that predates it, on any width where 392 is not the tighter
   of the two.

   `width: 100%` for the same reason it is on the service group: the step is a
   column flex container, and an auto cross-axis margin switches off the stretch
   that would otherwise size these — without it the block is shrink-to-fit at
   its intrinsic width and never reaches its cap at all. */
#so-booking-widget[data-template="centred"] .so-bw-calendar {
    width: 100%;
    max-width: var(--so-bw-measure);
    margin-inline: auto;
}
/* The measure goes on the GROUP, not just the list. Centring only the list left
   its own "Morning / Afternoon / Evening" heading behind: the title is a
   sibling inside .so-bw-slot-group, so it kept the panel's full width and sat
   hard against the left edge while the chips it labels were centred up to
   345px away — a heading visibly detached from its own group. Constraining the
   group centres both together, and the list's own max-width then becomes a
   no-op inside it (kept for the ungrouped case, where there is no group). */
#so-booking-widget[data-template="centred"] .so-bw-slot-group,
#so-booking-widget[data-template="centred"] .so-bw-slot-list {
    width: 100%;
    max-width: var(--so-bw-measure);
    margin-inline: auto;
}
/* FOUR COLUMNS WAS A GUESS AT THE LABEL'S WIDTH, AND IT WAS WRONG.
   A chip reads "9:00 AM to 10:00 AM" and needs 172px to hold that on one line
   in this template's type. Four fixed columns of the panel's 630px give it 153,
   so EVERY chip broke mid-phrase — "9:00 AM to 10:00" on the first line, "AM"
   alone on the second — and a grid of two-line pills whose break points land in
   different places reads as crooked even though every box is identical. That is
   the "time slots are not aligned properly" report: not the boxes, the text
   inside them.

   auto-fill sized to the label fixes it at every width at once and replaces
   three fixed-count rules (4 up top, 3 at 640, 2 at 480) that each had to guess
   again. 176px is the measured 172 plus a little air: the grid lays as many
   whole tracks as the panel can hold and stretches them, so the chip is never
   narrower than its own text and never wider than it needs to be. */
#so-booking-widget[data-template="centred"] .so-bw-slot-list {
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 176px), 1fr));
}
/* NB the two halves of the label are deliberately NOT the same size here —
   "9:00 AM" at 14px/500 against "to 10:00 AM" at 12.5px/400, set by section
   22.1's type scale. That reads as a mismatch when the pill is too narrow and
   the phrase breaks across it, which is why it looked like part of this bug;
   with the pill sized to the label it is simply a de-emphasised tail. Left
   alone on purpose. */

/* ── N · DAYPART (daypart-teal) ──────────────────────────────────────────
   "Something after work." Clients who think in parts of the day should not
   have to read every time in the day to find their slot.

   Centred already groups slots under a small Morning / Afternoon / Evening
   heading — every architecture does, the widget always emits
   .so-bw-slot-group. So a coloured heading was never going to make Daypart a
   different template, which is exactly what it used to be: Centred, teal, and
   one dead rule (it styled .so-bw-slot-group-label, a class the widget does
   not render). Here the dayparts become the STRUCTURE: three side-by-side
   panels, each a self-contained part of the day, so "after work" is one
   glance at one panel rather than a scan down a flat list. */
#so-booking-widget[data-template="daypart"] .so-bw-container {
    max-width: 860px;
    margin-inline: auto;
}
#so-booking-widget[data-template="daypart"] .so-bw-header {
    background: var(--so-bw-ds-n0);
    padding-bottom: 22px;
}
#so-booking-widget[data-template="daypart"] .so-bw-progress { display: none; }
/* Calendar above, panels below — the three panels need the full width to sit
   beside each other, which is the whole proposition. */
#so-booking-widget[data-template="daypart"] .so-bw-datetime {
    grid-template-columns: 1fr;
}
/* Left-aligned, unlike Centred and Hero which genuinely do centre their whole
   date column. Here the month is followed by the three-part rack below, and
   that rack is a full-width `repeat(auto-fit, minmax(190px, 1fr))` grid — it
   needs the room and cannot be centred to match. A centred month therefore sat
   170px in while the rack that REPLACES it started at 0, so choosing a day
   slid the whole panel sideways. The month hangs from the same edge instead. */
#so-booking-widget[data-template="daypart"] .so-bw-calendar {
    max-width: 460px;
    margin-inline: 0;
}
/* The panel is just a rack for the three parts of the day. */
#so-booking-widget[data-template="daypart"] .so-bw-slot-panel {
    border: none;
    background: transparent;
    border-radius: 0;
    padding: 4px 0 0;
    display: grid;
    /* Container-driven, like every other grid in this file: three across when
       there is room, fewer when there isn't — never three squeezed columns. */
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 190px), 1fr));
    gap: 12px;
    align-items: start;
}
/* Everything in the panel that is NOT a part of the day spans the full row.
   The panel also holds its own header ("Time Slot", the timezone picker, the
   back-to-calendar link, the loading / empty / waitlist states) — as direct
   children they become grid items too, and without this they get dealt out
   into the three daypart columns like slots. */
#so-booking-widget[data-template="daypart"] .so-bw-slot-panel > *:not(.so-bw-slot-group) {
    grid-column: 1 / -1;
}
/* Each part of the day is its own card, so an empty evening reads as a quiet
   evening rather than as an absence. */
#so-booking-widget[data-template="daypart"] .so-bw-slot-group {
    border: 1px solid var(--so-bw-border);
    border-radius: var(--so-bw-ds-radius-card, 12px);
    background: var(--so-bw-card-bg);
    padding: 0 0 12px;
    overflow: hidden;
    gap: 0;
}
/* The daypart name is the panel's header — the design, not a subheading. */
#so-booking-widget[data-template="daypart"] .so-bw-slot-group-title {
    margin: 0 0 10px;
    padding: 10px 12px;
    background: color-mix(in srgb, var(--so-bw-primary) 8%, transparent);
    border-bottom: 1px solid var(--so-bw-border);
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.07em;
    text-transform: uppercase;
    /* The original worry — "never let 'Afternoon' stack a letter per line in
       a narrow panel" — is handled by the column floor, not by nowrap: the
       panel's tracks are minmax(min(100%, 190px), 1fr), so a column is either
       ≥190px (where an uppercase daypart name fits comfortably) or the only
       column at full width. What nowrap + ellipsis actually did was truncate
       the longer translations ("NACHMITTAG", "SENARANJA") on the 3-up desktop
       layout. Wrap to two lines instead; the group's header strip grows and
       the three panels stay top-aligned via align-items: start. */
    white-space: normal;
    overflow-wrap: anywhere;
    line-height: 1.3;
}
/* One time per line inside its part of the day: the column is already the
   grouping, so a grid inside a grid would just re-flatten the list. */
#so-booking-widget[data-template="daypart"] .so-bw-slot-list {
    display: flex;
    flex-direction: column;
    gap: 6px;
    padding: 0 12px;
}
#so-booking-widget[data-template="daypart"] .so-bw-slot-row {
    font-size: 13px;
}
/* was @media (max-width: 560px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    /* Stacked panels on a phone — still three distinct parts of the day, just
       read one under the other. */
    #so-booking-widget:where([data-cw~="560"])[data-template="daypart"] .so-bw-slot-panel{
        grid-template-columns: 1fr;
    }


/* ── B2 · EDITORIAL (editorial-gold) ─────────────────────────────────────
   Was a Split Canvas recolour — same 50/50 grid, same brand rail, only a
   serif face and a gold palette to tell them apart. That fought the template's
   own proposition: type is supposed to do the work here, and half the width
   spent on a standing context panel is exactly what a magazine page does NOT
   do. So: one measured column, no rail at all, hairline rules instead of
   cards, and the step position stated as a line of text rather than drawn as
   a row of numbered circles. */
#so-booking-widget[data-template="editorial"] .so-bw-container {
    max-width: 680px;          /* a reading measure, not a canvas */
    /* Was `display: block`, and that quietly opted Editorial out of the
       bounded shell (section 23). The body scrolls because it is a flex item
       with `flex: 1 1 auto; min-height: 0`; under a block parent neither
       applies, so the body grew to its content, ran past the container's
       max-height, and the container's own `overflow: hidden` cut it off —
       taking the commit bar with it. Nothing about a single reading column
       needs `block`; a one-column flex IS the shape, and it is what every
       other non-grid template already declares. */
    display: flex;
    flex-direction: column;
    border: none;
    box-shadow: none;
    background: var(--so-bw-bg);
}
/* No context panel. The summary lives in the review step, where a magazine
   would put the standfirst — not pinned beside every question. */
#so-booking-widget[data-template="editorial"] .so-bw-rail { display: none; }
/* GUTTER EXCEPTION — Editorial and Soft are the only two architectures that
   route their horizontal inset through `.so-bw-body` rather than the shared
   gutter, so their header and nav carry a matching literal instead of
   var(--so-bw-gutter). The consequence, which predates the gutter and is
   unchanged by it: `.so-bw-step` adds its own inset INSIDE the already-padded
   body, so the content column sits at body + gutter (8 + 24 = 32px here)
   while the header sits at 8px. Aligning them means deciding which element
   owns the inset for these two and zeroing the other — a per-architecture
   change with a visible effect on the content column, so it belongs with the
   architecture work rather than this cascade pass. Both are exempted in
   tests/unit/widget-css-cascade.test.ts.

   SETTLED FOR EDITORIAL IN SECTION 27, which is that architecture work: the
   inset moves onto header / body / nav as `--so-bw-ed-inset` and the step's
   is zeroed, so the masthead, the search field, the index and Continue share
   one left edge. That token is padding on a fluid scale and never a cap —
   the three literals below are superseded, not merely widened. Soft is
   unchanged and still deferred. */
#so-booking-widget[data-template="editorial"] .so-bw-header {
    background: transparent;
    border-bottom: 1px solid var(--so-bw-ds-n200);
    padding: 40px 8px 18px;
}
/* The masthead. Editorial's whole proposition is that type does the work
   colour does elsewhere, so this is not decoration — it is the template.
   Targeted `.so-bw-step-title` for its whole life; the live class is
   `.so-bw-step-label`, so the serif headline never rendered at size and the
   "magazine spread" opened with the same 17px heading as every other
   template. Second of Editorial's three dead rules (the dateline above was
   the first, the index-style service list below is the third). */
#so-booking-widget[data-template="editorial"] .so-bw-step-label {
    font-size: 34px;
    line-height: 1.15;
    letter-spacing: -0.02em;
    font-weight: 600;
}
/* Progress as a masthead dateline, not chrome.

   This rule spent its whole life targeting `.so-bw-step-counter`, a class the
   widget has never rendered (the live one is `.so-bw-step-count`, no "-er").
   Editorial hides both the stepbar and the progress bar, so the dateline is
   its ONLY progress indicator — and the template silently fell back to the
   stock 13px counter, i.e. the one template designed around type had the same
   progress chrome as every other.

   Retargeted onto the live class. It scores (1,1,1) against the base rule's
   (0,1,0) and section 13's (1,1,0), and sits after both, so it wins on
   specificity and source order alike. The mono face section 13 puts on every
   quantity is deliberately left inherited: "Step 3 of 6" is a count, and the
   system sets all counts in mono — a mono dateline under a Newsreader headline
   is the contrast the register is built on. */
#so-booking-widget[data-template="editorial"] .so-bw-stepbar,
#so-booking-widget[data-template="editorial"] .so-bw-progress { display: none; }
#so-booking-widget[data-template="editorial"] .so-bw-step-count {
    font-size: 11px;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    color: var(--so-bw-ds-n500);
}
/* ── THE SCROLLPORT GETS NO TOP PADDING ──────────────────────────────────
   Editorial, Frost and Soft were the three templates that put BLOCK padding
   on `.so-bw-body`, and `.so-bw-body` is the scrollport. Padding-top on a
   scroll container scrolls away with the content, while the search field
   inside it pins to the top of the port — so the padding became a live strip
   between the two, and the cards ran through it half-cut on their way up.
   Measured here: 26px of it on Editorial with 15,652 pixels of card visible
   in the strip, 18px/7,494 on Frost, 8px/4,032 on Soft.

   The inset itself is wanted; it just cannot live on the scroller. It moves
   to `.so-bw-step`, which is INSIDE the scroll region and scrolls with the
   cards, so the composition is unchanged at rest and there is no strip once
   it moves. Stated as `calc(var(--so-bw-step-pad) + n)` rather than a literal
   so the step's own token still drives it at every breakpoint.

   Sticky is constrained by its containing block's PADDING box, not its
   content box, so padding on the step does not stop the field reaching the
   top of the port — which is what makes this work and why the same padding
   on the body did not.

   The INLINE padding stays on the body: it does not sit between the port and
   anything pinned. The bottom padding stays too — the commit bar is outside
   the scrollport (section 23), so there is nothing pinned at the foot for it
   to strand. */
#so-booking-widget[data-template="editorial"] .so-bw-body { padding: 0 8px 8px; }
#so-booking-widget[data-template="editorial"] .so-bw-step { padding-top: calc(var(--so-bw-step-pad) + 26px); }
/* Rules, not boxes: a list of choices reads as an index.
   The card half of this used to name .so-bw-service-card / .so-bw-service-grid
   — classes the widget has never rendered — so for its whole life the
   treatment reached the SLOT list only and Editorial's service, staff and
   location steps rendered as ordinary boxes, the opposite of the stated
   intent. The card half is now primitive C2 in section 16, built against the
   live .so-bw-card / .so-bw-card-grid. What remains here is the slot list,
   which is the same idea applied to times. */
#so-booking-widget[data-template="editorial"] .so-bw-slot-row {
    border: 0;
    border-bottom: 1px solid var(--so-bw-ds-n200);
    border-radius: 0;
    box-shadow: none;
    background: transparent;
}
#so-booking-widget[data-template="editorial"] .so-bw-slot-list {
    grid-template-columns: 1fr;
    gap: 0;
}
#so-booking-widget[data-template="editorial"] .so-bw-nav {
    border-top: 1px solid var(--so-bw-ds-n200);
    background: transparent;
    padding: 18px 8px 32px;
}

/* ── B3 · DUSK (dusk-champagne) ──────────────────────────────────────────
   Also a Split recolour before this — the only difference from Editorial was
   which palette it loaded, which is why the two read as the same template.
   Dark-first deserves its own shape: the canvas runs full width so the dark
   surface is the point, and the summary docks as a slim bar along the BOTTOM
   (the running total a low-light booking flow wants in peripheral vision)
   instead of standing in a half-width column. */
#so-booking-widget[data-template="dusk"] .so-bw-container {
    max-width: 820px;
    display: grid;
    grid-template-columns: 1fr;
    grid-template-rows: auto 1fr auto auto;   /* header · body · rail · nav */
    min-height: 520px;
}
#so-booking-widget[data-template="dusk"] .so-bw-header {
    grid-row: 1;
    background: transparent;
    border-bottom: 1px solid var(--so-bw-border);
}
#so-booking-widget[data-template="dusk"] .so-bw-body { grid-row: 2; }
/* The docked summary bar: a horizontal strip, not a column. */
#so-booking-widget[data-template="dusk"] .so-bw-rail {
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px 22px;
    position: static;
    width: auto;
    grid-row: 3;
    padding: 12px 20px;
    color: #fff;
    background: color-mix(in srgb, var(--so-bw-primary) 22%, #0a0a14);
    border-top: 1px solid var(--so-bw-border);
}
/* The rail's own vertical furniture has to lie down for a horizontal bar. */
#so-booking-widget[data-template="dusk"] .so-bw-rail-head,
#so-booking-widget[data-template="dusk"] .so-bw-rail-sub { display: none; }
#so-booking-widget[data-template="dusk"] .so-bw-rail-list {
    flex: 1 1 auto;
    flex-direction: row;
    flex-wrap: wrap;
    gap: 6px 22px;
}
#so-booking-widget[data-template="dusk"] .so-bw-rail-row {
    border: 0;
    padding: 0;
    gap: 8px;
    width: auto;
}
#so-booking-widget[data-template="dusk"] .so-bw-rail-total {
    border: 0;
    margin: 0;
    padding: 0;
    flex: 0 0 auto;
    gap: 10px;
}
#so-booking-widget[data-template="dusk"] .so-bw-nav { grid-row: 4; }
/* was @media (max-width: 560px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    /* Stacked key/value pairs — a wrapping strip is unreadable at phone width. */
    #so-booking-widget:where([data-cw~="560"])[data-template="dusk"] .so-bw-rail{ flex-direction: column; align-items: stretch; }
    #so-booking-widget:where([data-cw~="560"])[data-template="dusk"] .so-bw-rail-list{ flex-direction: column; }


/* ── B4 · SOFT (coral-pop) ───────────────────────────────────────────────
   Was a Deck recolour, and its own description admitted as much ("gives the
   deck architecture a friendlier voice"). But Deck's whole proposition is a
   physical STACK with the next card peeking underneath, which is a different
   idea from this one — so the two read as the same template with different
   paint. Soft keeps the friendly register and drops the stack: one flat
   column, oversized rounded targets, no peeking layers, nothing overlapping.
   The register lives in the geometry, not just the palette. */
#so-booking-widget[data-template="soft"] .so-bw-container {
    max-width: 620px;
    display: flex;
    flex-direction: column;
    border-radius: var(--so-bw-r-28);
}
#so-booking-widget[data-template="soft"] .so-bw-rail { display: none; }
#so-booking-widget[data-template="soft"] .so-bw-header {
    background: transparent;
    padding: 26px 26px 12px;
    border-bottom: 0;
}
/* Block padding off the scrollport and onto the step — see the note on
   Editorial's body rule for why. 8px here. */
#so-booking-widget[data-template="soft"] .so-bw-body { padding: 0 26px 4px; }
#so-booking-widget[data-template="soft"] .so-bw-step { padding-top: calc(var(--so-bw-step-pad) + 8px); }
#so-booking-widget[data-template="soft"] .so-bw-nav {
    background: transparent;
    border-top: 0;
    padding: 12px 26px 26px;
}
/* Roomy targets: pill cards in one column, generous hit area, no stacking.
   !! HALF-DEAD: .so-bw-service-grid / .so-bw-service-card are not rendered
   (live: .so-bw-card-grid / .so-bw-card), so the pill treatment reaches the
   slot list and buttons but NOT the cards this template exists for. Same
   caveat as Editorial — see the banner in section 13. */
#so-booking-widget[data-template="soft"] .so-bw-service-grid,
#so-booking-widget[data-template="soft"] .so-bw-slot-list {
    grid-template-columns: 1fr;
    gap: 12px;
}
#so-booking-widget[data-template="soft"] .so-bw-service-card,
#so-booking-widget[data-template="soft"] .so-bw-slot-row,
#so-booking-widget[data-template="soft"] .so-bw-btn {
    border-radius: var(--so-bw-r-22);
    min-height: 56px;
    box-shadow: none;
}
#so-booking-widget[data-template="soft"] .so-bw-calendar-day { border-radius: var(--so-bw-r-14); }
/* The stepper reads as soft dots, not a numbered chain. `font-size: 0` and the
   hidden svg are what actually make it a DOT — shrinking the circle alone
   leaves the step numeral rendering inside (and spilling out of) a 9px target.
   Same treatment the Dialog template uses for its progress pips. */
#so-booking-widget[data-template="soft"] .so-bw-stepbar-label { display: none; }
#so-booking-widget[data-template="soft"] .so-bw-stepbar-dot {
    width: 9px;
    height: 9px;
    min-width: 9px;
    border-radius: 999px;
    font-size: 0;
    line-height: 0;
    overflow: hidden;
}
#so-booking-widget[data-template="soft"] .so-bw-stepbar-dot svg { display: none; }

/* ── D2 · FROST (glass-frost) ────────────────────────────────────────────
   Was a Hero recolour, which is why the two read as one template: Hero's whole
   idea is an OPAQUE card lifting off full-bleed imagery, and Frost was running
   that same shape with a frosted paint job. The propositions are opposite —
   Frost is layered translucency, so nothing should overlap and nothing should
   be opaque. Here the panels float as separate frosted sheets on a gradient
   field, evenly spaced, each with its own blur; no imagery, no negative
   margin, no card lifting off anything. */
#so-booking-widget[data-template="frost"] .so-bw-container {
    max-width: 720px;
    display: flex;
    flex-direction: column;
    gap: 12px;                 /* the sheets are separate objects, not one card */
    background: transparent;
    border: none;
    box-shadow: none;
    overflow: visible;
    padding: 4px;
}
#so-booking-widget[data-template="frost"] .so-bw-rail { display: none; }
/* Every surface is the same frosted sheet — the layout IS the material. */
#so-booking-widget[data-template="frost"] .so-bw-header,
#so-booking-widget[data-template="frost"] .so-bw-body,
#so-booking-widget[data-template="frost"] .so-bw-nav {
    background: rgba(255, 255, 255, 0.55);
    -webkit-backdrop-filter: blur(20px) saturate(160%);
    backdrop-filter: blur(20px) saturate(160%);
    border: 1px solid rgba(255, 255, 255, 0.7);
    border-radius: var(--so-bw-r-18);
    box-shadow: 0 8px 32px -18px rgba(20, 40, 80, 0.45);
}
#so-booking-widget[data-template="frost"] .so-bw-header {
    min-height: 0;             /* no hero band */
    /* 14px, was 18px. REPORTED as slightly tall, and MEASURED at 107px on a
       816px card: 36 of that was this padding, 34 the step line, and 39 the
       progress rule — which is 3px of hairline inside 16px of its own padding
       and 20px of margin (see the stepbar rule at the end of this block).
       Trimmed here and there rather than in one place, so no single element
       loses its breathing room; nothing is removed and the type is untouched. */
    padding: 14px var(--so-bw-gutter);
    color: var(--so-bw-ds-n900);
}
/* Block padding off the scrollport and onto the step — see the note on
   Editorial's body rule for why. 18px here (16px below 420px, further down). */
#so-booking-widget[data-template="frost"] .so-bw-body {
    margin: 0;                 /* Hero pulls this up by -28px; Frost must not */
    padding: 0 22px 18px;
}
#so-booking-widget[data-template="frost"] .so-bw-step { padding-top: calc(var(--so-bw-step-pad) + 18px); }
/* NB: overridden by the commit-bar rule in section 28.4, which is the same
   (1,1,0) and comes later — that one is what actually renders. */
#so-booking-widget[data-template="frost"] .so-bw-nav { padding: 14px var(--so-bw-gutter); }
/* Cards are lighter sheets again, so depth comes from stacked transparency
   rather than shadow.
   !! HALF-DEAD: .so-bw-service-card is not rendered (live: .so-bw-card), so
   this reaches the slot rows only. Frost's cards get their glass from the
   glass-frost signature in section 15 instead, which is why this template
   shows the problem less than Editorial and Soft. See section 13. */
#so-booking-widget[data-template="frost"] .so-bw-service-card,
#so-booking-widget[data-template="frost"] .so-bw-slot-row {
    background: rgba(255, 255, 255, 0.5);
    border: 1px solid rgba(255, 255, 255, 0.75);
    border-radius: var(--so-bw-r-14);
    box-shadow: none;
}
/* Progress as a hairline rule inside the header sheet — Hero uses fat white
   segments over its image, which would be invisible on frosted glass. */
#so-booking-widget[data-template="frost"] .so-bw-stepbar-label { display: none; }
#so-booking-widget[data-template="frost"] .so-bw-stepbar-item {
    flex: 1;
    height: 3px;
    border-radius: 999px;
    background: rgba(30, 60, 110, 0.18);
}
#so-booking-widget[data-template="frost"] .so-bw-stepbar-item--done,
#so-booking-widget[data-template="frost"] .so-bw-stepbar-item--active {
    background: var(--so-bw-primary);
}
#so-booking-widget[data-template="frost"] .so-bw-stepbar-dot { display: none; }
/* The rule is 3px tall and was sitting in 36px of space — 8px of padding above
   and below plus the shared 4/16 margin. That is fine for a bar with dots and
   labels in it, which is what those defaults were written for; on this template
   the bar IS the 3px line, so the spacing was carrying the header's height on
   its own. Halved, which still separates the line from the step text above and
   the sheet edge below. Frost only: every other layout keeps the shared
   spacing, because on those the bar has something in it.

   `[data-template-id]` carried purely for WEIGHT, the same way section 34 does
   it for Hero's stepbar. The padding being overridden is the stepper's
   never-cut floor (section 22, `#so-booking-widget[data-template]
   .so-bw-stepbar`), which is (1,1,0) like this rule and comes later in the
   file — so without the extra attribute this loses on order and only the
   margins here take effect. That floor exists so the box always outgrows the
   tallest DOT plus its focus ring; Frost hides the dots and the labels, so
   what it has to outgrow is a 3px line, and 4px still does. */
#so-booking-widget[data-template="frost"][data-template-id] .so-bw-stepbar {
    padding-top: 4px;
    padding-bottom: 4px;
    margin: 2px 0 10px;
}

/* ── D · HERO (immersive-rose) ───────────────────────────────────────────
   Imagery carries the first screen and the card lifts off it. Brand-led:
   the room, the work and the price land before a single control does. */
#so-booking-widget[data-template="hero"] .so-bw-container {
    max-width: 860px;
    background: transparent;
    border: none;
    box-shadow: none;
    overflow: visible;
}
#so-booking-widget[data-template="hero"] .so-bw-header {
    position: relative;
    min-height: 180px;
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
    padding: 24px var(--so-bw-gutter) 52px;
    border-radius: var(--so-bw-ds-radius-shell) var(--so-bw-ds-radius-shell) 0 0;
    border-bottom: none;
    color: #fff;
    background: var(--so-bw-primary);
    background:
        linear-gradient(180deg, rgba(20, 8, 12, 0.15), rgba(20, 8, 12, 0.62)),
        linear-gradient(140deg, var(--so-bw-primary), color-mix(in srgb, var(--so-bw-primary) 55%, #201018));
    background-size: cover;
    background-position: center;
}
#so-booking-widget[data-template="hero"] .so-bw-step-label { font-size: 24px; letter-spacing: -0.02em; color: #fff; }
#so-booking-widget[data-template="hero"] .so-bw-step-count { color: rgba(255, 255, 255, 0.82); }
/* Progress as segments, not a bar or a numbered stepper — the hero is
   already carrying the message, so the indicator stays almost silent. */
#so-booking-widget[data-template="hero"] .so-bw-stepbar-label,
#so-booking-widget[data-template="hero"] .so-bw-stepbar-dot { display: none; }
#so-booking-widget[data-template="hero"] .so-bw-stepbar { gap: 5px; margin-top: 14px; }
#so-booking-widget[data-template="hero"] .so-bw-stepbar-item {
    flex: 1;
    height: 4px;
    border-radius: 999px;
    background: rgba(255, 255, 255, 0.32);
}
#so-booking-widget[data-template="hero"] .so-bw-stepbar-item--done,
#so-booking-widget[data-template="hero"] .so-bw-stepbar-item--active { background: #fff; flex: 2; }
#so-booking-widget[data-template="hero"] .so-bw-progress { display: none; }
/* THE LIFT IS FOR THE HEADER, AND IT ATE THE BANNER.

   `.so-bw-body` is pulled up 28px so the white card overlaps the hero — the
   whole point of the layout, and it works because the four siblings sitting
   between header and body (the x-if <template>, .so-bw-error, .so-bw-loading,
   .so-bw-hold-timer) are all `display: none` until something activates them,
   so they collapse and the lift lands on the header.

   The moment the hold timer DOES render, it takes 41px between the two, the
   body's -28px pulls back over it, and the body wins the paint because it
   also carries `z-index: 1` against the banner's `auto`. Measured on the live
   install: timer 905→945, body starting at 917, and elementFromPoint at the
   banner's own centre returning .so-bw-nav. Roughly a third of "Slot held
   for 6:39" survived as a yellow sliver — reported as the timer hiding.

   Fixed on the BANNER rather than here, so the lift keeps working untouched
   whenever no banner is showing: see the `.so-bw-hold-timer` rule below,
   which contributes exactly the 28px back that this line takes away. */
#so-booking-widget[data-template="hero"] .so-bw-body {
    position: relative;
    z-index: 1;
    margin: -28px 24px 0;
    background: var(--so-bw-ds-n0);
    border: 1px solid var(--so-bw-ds-n100);
    border-radius: var(--so-bw-r-14);
    box-shadow: var(--so-bw-ds-shadow-sm);
}
/* …and the banners that sit in the lift's path give the 28px back.
   `margin-bottom` is the whole mechanism: these elements are `display: none`
   until they have something to say, and a display:none element contributes no
   margin at all — so the lift is untouched in the normal case and is cancelled
   exactly when a banner is actually on screen. `z-index: 2` is belt and braces
   against a sub-pixel overlap; the body is on 1.
   Kept off .so-bw-loading, which is position:absolute and never in the flow. */
#so-booking-widget[data-template="hero"] .so-bw-hold-timer,
#so-booking-widget[data-template="hero"] .so-bw-error,
#so-booking-widget[data-template="hero"] .so-bw-pay-retry {
    position: relative;
    z-index: 2;
    margin-bottom: 28px;
}
#so-booking-widget[data-template="hero"] .so-bw-nav {
    background: var(--so-bw-ds-n0);
    border-radius: 0 0 var(--so-bw-ds-radius-shell) var(--so-bw-ds-radius-shell);
    border: 1px solid var(--so-bw-ds-n100);
    border-top: none;
    margin: 0 0 0 0;
}
#so-booking-widget[data-template="hero"] .so-bw-btn--primary { border-radius: 999px; padding-inline: 28px; }

/* ── E · TABBED RAIL (tabbed-forest) ─────────────────────────────────────
   Steps as tabs so a client can jump back freely, with the running order
   pinned right. For people who revise their mind halfway through. */
#so-booking-widget[data-template="tabbed"] .so-bw-container {
    /* The fallback for a widget rendered without `data-template-id`. The live
       width comes from `--so-bw-tpl-maxw` in section 23.1, which the size radio
       scales; these two are deliberately kept in step, so change both. */
    max-width: 1360px;
    display: grid;
    grid-template-columns: 1fr 320px;
    grid-template-rows: auto 1fr auto;
}
#so-booking-widget[data-template="tabbed"] .so-bw-header { grid-column: 1; grid-row: 1; background: var(--so-bw-ds-n0); padding-bottom: 0; border-bottom: none; }
#so-booking-widget[data-template="tabbed"] .so-bw-body   { grid-column: 1; grid-row: 2; border-right: 1px solid var(--so-bw-ds-n100); }
#so-booking-widget[data-template="tabbed"] .so-bw-nav    { grid-column: 1; grid-row: 3; border-right: 1px solid var(--so-bw-ds-n100); }
#so-booking-widget[data-template="tabbed"] .so-bw-rail {
    display: flex;
    flex-direction: column;
    position: static;
    grid-column: 2;
    grid-row: 1 / -1;
    width: auto;
    padding: 28px 24px;
    color: var(--so-bw-text);
    background: var(--so-bw-ds-n25);
    border-left: 1px solid var(--so-bw-ds-n100);
}
/* The rail head is a LABEL, not a headline. Elsewhere the rail is the whole
   left pane and opens with a 19px/800 title; here it is a 320px column beside
   a working step, and an 800-weight headline in it competes with the step's
   own h2 for the same job. Mono, small, uppercase — the same register the
   template uses for its slot times and its money — with the step count under
   it, which is the one fact the hidden step indicator took away. */
#so-booking-widget[data-template="tabbed"] .so-bw-rail-head { margin-bottom: 14px; }
#so-booking-widget[data-template="tabbed"] .so-bw-rail-title {
    font-family: var(--so-bw-mono);
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--so-bw-ds-n700);
}
#so-booking-widget[data-template="tabbed"] .so-bw-rail-sub { display: none; }
#so-booking-widget[data-template="tabbed"] .so-bw-rail-step {
    margin-top: 5px;
    font-family: var(--so-bw-mono);
    font-size: 11px;
    color: var(--so-bw-ds-n500);
}
#so-booking-widget[data-template="tabbed"] .so-bw-rail-row { border-bottom-color: var(--so-bw-ds-n100); }
#so-booking-widget[data-template="tabbed"] .so-bw-rail-total { border-top-color: var(--so-bw-ds-n100); }
/* The rail values were painted in the accent. Two problems, one of them
   measured: --so-bw-primary is a BUTTON FILL, and as 12.5px text on n25 it
   runs 2.4-3.4:1 depending on the admin's hue (see section 13's note on
   --so-bw-primary-ink) — four failing strings in the one panel that is on
   screen the whole time. The other is that if everything in the rail is
   accent-coloured then nothing in it is: the accent's job here is to mark the
   Edit affordance and the chosen time, not to recolour the record. */
#so-booking-widget[data-template="tabbed"] .so-bw-rail-v {
    color: var(--so-bw-ds-n900);
    font-family: var(--so-bw-mono);
    font-size: 12.5px;
    font-weight: 500;
}
/* Keys: colour, not opacity, for the same reason the tabs stopped using it.
   `opacity: .62` on the base rail is tuned for a dark pane where the rail's
   own text is white; on this template's n25 column it lands a grey on a
   near-white and takes the label under the text floor. */
#so-booking-widget[data-template="tabbed"] .so-bw-rail-k {
    opacity: 1;
    color: var(--so-bw-ds-n500);
}
#so-booking-widget[data-template="tabbed"] .so-bw-rail-total span:first-child {
    opacity: 1;
    font-family: var(--so-bw-mono);
    font-size: 10px;
    letter-spacing: 0.12em;
    color: var(--so-bw-ds-n500);
}
#so-booking-widget[data-template="tabbed"] .so-bw-rail-total span:last-child {
    font-family: var(--so-bw-mono);
    font-size: 17px;
    font-weight: 600;
    color: var(--so-bw-ds-n900);
}
#so-booking-widget[data-template="tabbed"] .so-bw-step-indicator { display: none; }

/* ── Tabs: a REAL row ────────────────────────────────────────────────────
   The step indicator and the progress bar are both hidden here, so the tab
   strip is the ONLY thing telling the customer where they are. It has to be
   a tab strip, then, and it was not.

   The base primitive (section 7) is a numbered STEPPER: each item is a
   centred flex COLUMN — dot above wrapped label — and every item but the
   first draws a 2px connector line back to the one before it. Both are
   right for a progress indicator and wrong for a tab. This section's
   earlier pass restyled the padding and added an accent underline but never
   replaced the DIRECTION, so the "tabs" rendered as stepper columns
   carrying a connector line AND an underline at once.

   A layout that REPLACES a primitive's direction has to say so; inheriting
   half of one is the same mistake the numbered gutter made in 17.9.

   Geometry, and why: 48px tall so the whole tab clears the 44px tap floor
   rather than just its label; the ACTIVE tab takes the slack (`1 1 auto`)
   so the step you are on is the widest thing in the strip; a FUTURE tab
   collapses to its number alone (`0 0 34px`) because eight labels do not
   fit in a 720px pane and the four you have not reached are the four you
   do not need to read. */
#so-booking-widget[data-template="tabbed"] .so-bw-stepbar {
    gap: 0;
    align-items: stretch;
    border-bottom: none;
    /* ── The first tab's NUMBER sits on the page's left margin ───────────
       A tab needs internal padding — it is a click target with a hover
       tint and a focus ring, and text hard against its own edge reads as
       cramped. But that padding is also the reason the strip could not
       line up with anything: the step description, the search field, the
       card grid, the calendar, the form labels, the summary and the Back
       button all start at the gutter, and the tab's ink started at
       gutter + strip padding + tab padding. MEASURED at 1040: content at
       25, first tab's number at 41. This template hides the step
       indicator AND the progress bar, so the strip is the only thing
       carrying position — and it was the one element aligned to nothing.

       So the strip gives back exactly what the tab takes: one token sets
       the tab's left padding and the strip's pull-out, which is the only
       way the two cannot drift apart. Declared here so the items inherit
       it, and re-declared once at the narrow tier where the tabs tighten.

       Only the LEFT is corrected. The strip's right padding stays: tabs
       never fill the row, so the strip's right edge is not a design line
       and there is nothing there to align to. */
    --so-bw-tab-inset: 8px;
    margin: 0 0 0 calc(-1 * var(--so-bw-tab-inset));
    padding: 0 8px 0 0;
}
/* The strip's rule is made OF the tabs rather than drawn under them: each
   tab owns the 3px segment beneath itself, so the accent below the active
   tab is that same line in a different colour at the same height. Nothing to
   align by hand, and no doubled border where the two used to overlap. */
#so-booking-widget[data-template="tabbed"] .so-bw-stepbar-item {
    flex: 0 1 auto;
    min-width: 0;
    height: 48px;
    box-sizing: border-box;
    flex-direction: row;
    align-items: center;
    justify-content: flex-start;
    gap: 7px;
    /* The left value is the token the strip pulls itself back by. Change it
       in one place or the strip stops registering on the gutter. */
    padding: 0 10px 0 var(--so-bw-tab-inset);
    margin: 0;
    text-align: left;
    border-bottom: 3px solid var(--so-bw-ds-n100);
    border-radius: var(--so-bw-ds-radius-chip) var(--so-bw-ds-radius-chip) 0 0;
    transition: flex-basis 160ms ease,
                border-color var(--so-bw-ds-motion),
                background var(--so-bw-ds-motion);
}
/* The stepper's connector, gone. It is drawn by a pseudo-element on the
   item, and the --active / --done variants recolour it, so all three have
   to be cancelled or a completed tab keeps an accent line running out of
   its left edge into the tab before it. */
#so-booking-widget[data-template="tabbed"] .so-bw-stepbar-item::before,
#so-booking-widget[data-template="tabbed"] .so-bw-stepbar-item--done::before,
#so-booking-widget[data-template="tabbed"] .so-bw-stepbar-item--active::before {
    content: none;
}
#so-booking-widget[data-template="tabbed"] .so-bw-stepbar-item--done {
    cursor: pointer;
    border-bottom-color: color-mix(in srgb, var(--so-bw-primary) 22%, var(--so-bw-card-bg, #fff));
}
#so-booking-widget[data-template="tabbed"] .so-bw-stepbar-item--active {
    flex: 1 1 auto;
    border-bottom-color: var(--so-bw-primary);
}
/* Future: number only, centred in its own square. */
#so-booking-widget[data-template="tabbed"] .so-bw-stepbar-item:not(.so-bw-stepbar-item--done):not(.so-bw-stepbar-item--active) {
    flex: 0 0 34px;
    padding: 0;
    justify-content: center;
    cursor: default;
}
#so-booking-widget[data-template="tabbed"] .so-bw-stepbar-item:not(.so-bw-stepbar-item--done):not(.so-bw-stepbar-item--active) .so-bw-stepbar-label {
    display: none;
}

/* The number: a 22px pill, not a stepper's ringed dot. */
#so-booking-widget[data-template="tabbed"] .so-bw-stepbar-dot {
    width: 22px;
    height: 22px;
    border: 1px solid transparent;
    background: transparent;
    color: var(--so-bw-ds-n500);
    font-family: var(--so-bw-mono);
    font-size: 11px;
    font-weight: 600;
    box-shadow: none;
}
#so-booking-widget[data-template="tabbed"] .so-bw-stepbar-item:not(.so-bw-stepbar-item--done):not(.so-bw-stepbar-item--active) .so-bw-stepbar-dot {
    border-color: var(--so-bw-ds-n200);
}
#so-booking-widget[data-template="tabbed"] .so-bw-stepbar-item--done .so-bw-stepbar-dot {
    background: color-mix(in srgb, var(--so-bw-primary) 12%, var(--so-bw-card-bg, #fff));
    border-color: transparent;
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}
#so-booking-widget[data-template="tabbed"] .so-bw-stepbar-item--active .so-bw-stepbar-dot {
    background: var(--so-bw-primary);
    border-color: var(--so-bw-primary);
    color: var(--so-bw-on-primary, #fff);
    /* The stepper's 4px halo would collide with the tab above and below it
       in a 48px row. The segment underneath states "active" already. */
    box-shadow: none;
}

/* The label. One line, clipped — the base bar wraps under its dot because
   eight translated names in a centred column need the height, but a tab is
   a row, so a long name shortens instead of growing the strip. */
#so-booking-widget[data-template="tabbed"] .so-bw-stepbar-label {
    font-family: var(--so-bw-mono);
    font-size: 12px;
    font-weight: 500;
    letter-spacing: -0.2px;
    /* MEASURED: `line-height: 1` clipped the label vertically by 2px on 54 of
       the audited cases — the box is exactly the font size, so a descender
       plus a diacritic ("Terminbestätigung") has nowhere to go, and the
       `overflow: hidden` that makes the ellipsis work cuts it off. The tab is
       a 48px flex row, so the extra 3px costs the layout nothing. */
    line-height: 1.25;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    min-width: 0;
    max-width: none;
}
#so-booking-widget[data-template="tabbed"] .so-bw-stepbar-item--active .so-bw-stepbar-label {
    font-size: 13px;
    font-weight: 600;
}
#so-booking-widget[data-template="tabbed"] .so-bw-progress { display: none; }
/* was @media (max-width:900px) — now gated on the CONTAINER width
   via data-cw (see booking-widget.js _wireContainerWidth). :where()
   keeps specificity identical to the media query it replaces. */

    #so-booking-widget:where([data-cw~="900"])[data-template="tabbed"] .so-bw-container{ grid-template-columns: 1fr; }
    #so-booking-widget:where([data-cw~="900"])[data-template="tabbed"] .so-bw-rail{ grid-column: 1; grid-row: 4; border-left: none; border-top: 1px solid var(--so-bw-ds-n100); }
    #so-booking-widget:where([data-cw~="900"])[data-template="tabbed"] .so-bw-body,
    #so-booking-widget:where([data-cw~="900"])[data-template="tabbed"] .so-bw-nav{ border-right: none; }

/* ── G · AGENDA (agenda-indigo) ──────────────────────────────────────────
   No calendar grid at all. Openings listed forward in time, day by day, the
   way people actually think. Removes a whole decision when most clients
   simply want the soonest slot.
   .so-bw-agenda-list is emitted for every template — hidden for all of
   them — and revealed here, the same one-markup/one-code-path deal as
   Dual Month's second month. */
#so-booking-widget .so-bw-agenda-list { display: none; }
#so-booking-widget[data-template="agenda"] .so-bw-container {
    max-width: 960px;
    display: grid;
    grid-template-columns: 240px 1fr;
    grid-template-rows: auto 1fr auto;
    min-height: 520px;
}
#so-booking-widget[data-template="agenda"] .so-bw-header { grid-column: 2; grid-row: 1; background: var(--so-bw-ds-n0); }
#so-booking-widget[data-template="agenda"] .so-bw-body   { grid-column: 2; grid-row: 2; }
#so-booking-widget[data-template="agenda"] .so-bw-nav    { grid-column: 2; grid-row: 3; }
/* The steps become a vertical timeline down the left edge.
   ────────────────────────────────────────────────────────────────────────
   That sentence sat here for a long time describing something the CSS below
   did not do. The rail was laid out in column 1 as a plain SUMMARY — four
   key/value rows and a total — while `.so-bw-stepbar` was switched off two
   rules down, so Agenda rendered no node structure at all and the only
   progress signal left was the "Step n of m" count in the header. Four
   independent things said otherwise:

     • The Studio's gallery mock for this architecture (WidgetPage.vue,
       `case 'agenda'`) draws the left column as a NODE LIST — a dot per step,
       connected by a rule, filled up to the current one. That mock's own
       docblock promises the picker and the booking page "can only ever
       disagree about fidelity, never structure".
     • This comment.
     • The narrow-width block near the bottom of this file styles
       `[data-template="agenda"] .so-bw-stepbar` for wrapping — rules that
       cannot fire while the bar is display:none, i.e. written for a bar
       somebody expected to be visible.
     • Agenda is the ONLY one of the nineteen that sets `display: none` on
       the stepbar outright. Editorial suppresses its progress chrome too,
       but deliberately and with a documented replacement.

   Hiding the stepbar is right for this template — the mock shows one
   progress affordance, not two — so what was missing is the thing meant to
   replace it. The rail rows already name the journey (Service, Specialist,
   Date, Time) in the column the mock puts the nodes in, so they ARE the
   timeline; they just needed the nodes drawn on them. `--done` comes from
   booking-widget.js and is scoped to this template alone. */
#so-booking-widget[data-template="agenda"] .so-bw-rail {
    display: flex;
    flex-direction: column;
    position: static;
    grid-column: 1;
    grid-row: 1 / -1;
    width: auto;
    padding: 28px 22px;
    color: var(--so-bw-text);
    background: var(--so-bw-ds-n25);
    border-right: 1px solid var(--so-bw-ds-n100);
}
#so-booking-widget[data-template="agenda"] .so-bw-rail-title { font-size: 15px; font-weight: 600; }
#so-booking-widget[data-template="agenda"] .so-bw-rail-total { border-top-color: var(--so-bw-ds-n100); }
#so-booking-widget[data-template="agenda"] .so-bw-rail-v { color: var(--so-bw-primary); }

/* ── The axis ─────────────────────────────────────────────────────────────
   Gutter on the LIST rather than an indent on each row, so the nodes hang in
   their own column and a wrapped value still lines up with the label above
   it. The row's bottom rule goes: a hairline under every entry competes with
   the connector for the same job, and the connector is the one carrying
   state. */
#so-booking-widget[data-template="agenda"] .so-bw-rail-list {
    position: relative;
    padding-left: 24px;
}
#so-booking-widget[data-template="agenda"] .so-bw-rail-row {
    position: relative;
    border-bottom: 0;
    padding: 9px 0;
}
#so-booking-widget[data-template="agenda"] .so-bw-rail-row::before {
    content: '';
    position: absolute;
    left: -24px;
    top: 11px;
    width: 13px;
    height: 13px;
    border-radius: 50%;
    box-sizing: border-box;
    background: var(--so-bw-ds-n0);
    border: 2px solid var(--so-bw-ds-n200);
    z-index: 1;
}
/* Connector. Runs from the bottom of one node to the top of the next, so the
   numbers are a function of the node's size and the row's padding: the node
   sits at 11..24 inside a row whose padding is 9, so the line starts at 24
   and ends 9px into the next row. Move either and both must follow, or the
   rail comes apart into loose beads — same arithmetic the Timeline stepbar
   documents at its own ::after. */
#so-booking-widget[data-template="agenda"] .so-bw-rail-row:not(:last-child)::after {
    content: '';
    position: absolute;
    left: -18.5px;
    top: 24px;
    bottom: -9px;
    width: 2px;
    background: var(--so-bw-ds-n200);
    z-index: 0;
}
#so-booking-widget[data-template="agenda"] .so-bw-rail-row--done::before {
    background: var(--so-bw-primary);
    border-color: var(--so-bw-primary);
}
#so-booking-widget[data-template="agenda"] .so-bw-rail-row--done:not(:last-child)::after {
    background: var(--so-bw-primary);
}
/* The CURRENT step is the first row that is not done — either the one
   directly after the last done row, or the very first row when nothing is
   done yet. Expressed structurally so no second class has to be bound and
   kept in step with the first. */
#so-booking-widget[data-template="agenda"] .so-bw-rail-row--done + .so-bw-rail-row:not(.so-bw-rail-row--done)::before,
#so-booking-widget[data-template="agenda"] .so-bw-rail-row:first-child:not(.so-bw-rail-row--done)::before {
    border-color: var(--so-bw-primary);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--so-bw-primary) 18%, transparent);
}

/* Stays hidden: the rail above is now this template's progress indicator, and
   the mock draws one, not two. The narrow-width wrap rules further down this
   file that name `[data-template="agenda"] .so-bw-stepbar` are inert for the
   same reason — left in place because they cost nothing and describe how the
   bar would behave if a future revision ever brought it back. */
#so-booking-widget[data-template="agenda"] .so-bw-stepbar { display: none; }
#so-booking-widget[data-template="agenda"] .so-bw-progress { height: 3px; }
/* The month GRID is suppressed; the day list carries the whole decision.
   Only the grid, though — hiding the whole .so-bw-calendar left the date
   page with literally nothing in it, because the slot panel beside it only
   renders once a date is PICKED. The step measured 0px tall and the
   customer had no dates to choose from, so Agenda could never be booked.
   The month header stays: its arrows are the only way to look further
   ahead than the list reaches. */
#so-booking-widget[data-template="agenda"] .so-bw-calendar > .so-bw-calendar-scroll { display: none; }
/* ...and with the grid gone, the 460px cap goes with it.
   That cap is stated once for the whole widget and its reason is specific: past
   about 460px "the day CHIPS stop reading as a month and become a spreadsheet".
   It is a rule about a seven-column grid of little squares. This template has
   no such grid — the line above suppresses it outright and reveals
   `.so-bw-agenda-list` instead, which is a column of full-width day ROWS, the
   same shape as the service rows on step 1.
   Rows do not become a spreadsheet when they get wider; they just stop lining
   up with everything around them. Capped, the day list ran to 460px inside a
   660px step while the member strip and the step description above it ran the
   full width, so the one thing the step exists to present was also the only
   thing indented. Uncapped, it shares its edges with its own siblings. */
#so-booking-widget[data-template="agenda"] .so-bw-calendar { display: block; max-width: none; }
#so-booking-widget[data-template="agenda"] .so-bw-agenda-list {
    display: flex;
    flex-direction: column;
    gap: 8px;
    max-height: 460px;
    overflow-y: auto;
}
#so-booking-widget[data-template="agenda"] .so-bw-agenda-day {
    display: flex;
    align-items: center;
    justify-content: space-between;
    /* Date on the left, opening count on the right; the count drops under the
       date when a long-form date leaves it no room. */
    flex-wrap: wrap;
    gap: 4px 16px;
    width: 100%;
    padding: 14px 16px;
    font: inherit;
    font-size: 15px;
    text-align: left;
    cursor: pointer;
    color: var(--so-bw-text);
    background: var(--so-bw-ds-n0);
    border: 1px solid var(--so-bw-ds-n100);
    border-radius: var(--so-bw-ds-radius-card);
    transition: border-color var(--so-bw-ds-motion), background var(--so-bw-ds-motion);
}
#so-booking-widget[data-template="agenda"] .so-bw-agenda-day:hover {
    border-color: var(--so-bw-primary);
    background: var(--so-bw-ds-n25);
}
#so-booking-widget[data-template="agenda"] .so-bw-agenda-day-date {
    display: flex;
    align-items: baseline;
    gap: 8px;
    min-width: 0;
}
#so-booking-widget[data-template="agenda"] .so-bw-agenda-day-date strong { font-weight: 600; }
#so-booking-widget[data-template="agenda"] .so-bw-agenda-day-date span {
    font-size: 13px;
    color: var(--so-bw-ds-n500);
}
#so-booking-widget[data-template="agenda"] .so-bw-agenda-day-count {
    font-size: 13px;
    font-weight: 600;
    /* "5 openings" is translated and shares the row with a full date. The row
       wraps (below), so the count can wrap with it instead of overflowing the
       day button's right edge. */
    white-space: normal;
    overflow-wrap: anywhere;
    min-width: 0;
    text-align: right;
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}
#so-booking-widget[data-template="agenda"] .so-bw-agenda-empty {
    margin: 0;
    padding: 28px 4px;
    font-size: 14px;
    text-align: center;
    color: var(--so-bw-ds-n500);
}
/* ── A MONTH WITH NOTHING IN IT IS STILL A FULL-WIDTH STEP ────────────────
   REPORTED: "step 3 shows no dates for every month, and the text is in the
   left corner — not responsive, not nice."

   The second half is a LAYOUT fault and it is caused by the first half. The
   date pane is an auto-flow grid: whatever is visible takes a track. Normally
   Agenda shows one thing — the day list — so the pane measures a single
   948px column and the list runs the full width of the step. But an empty
   month reveals the slot panel (its third x-show clause is the only place
   that state gets explained), and the grid immediately splits 466 + 466. The
   day list, whose entire content is now one sentence, is squeezed into the
   left half — and a second box sits beside it repeating the same news in
   different words. Nothing is centred on anything, and the pane is twice as
   tall as the words in it.

   The right column has nothing to offer on this step: there is no date to
   pick, so there are no times to show, and the notice under the month header
   already says so. Removing it returns the pane to one track — the same
   full-width flow every other month gets — and the notice becomes the step's
   single answer instead of one of three.

   Gated on `--no-month`, so this is the empty month only: pick a date in a
   month that HAS one and the panel is back, beside the list, exactly as
   before. */
#so-booking-widget[data-template="agenda"] .so-bw-datetime--no-month .so-bw-slot-panel {
    display: none;
}
/* And the surviving notice is dressed as an empty STATE rather than as a
   validation chip: room around the sentence, a measure that keeps it to two
   lines at any width, and centred in the track it now owns. `margin-inline:
   auto` is what actually centres it — the base rule only centres the TEXT
   inside a box that was already full-bleed, which is why a 466px chip with a
   centred sentence still read as "text in the left corner". */
#so-booking-widget[data-template="agenda"] .so-bw-datetime--no-month .so-bw-calendar-empty {
    max-width: 34rem;
    margin: 20px auto 4px;
    padding: 26px 24px;
    border-radius: var(--so-bw-ds-radius-card);
    font-size: 14px;
}
#so-booking-widget[data-template="agenda"] .so-bw-slot-list { grid-template-columns: 1fr; gap: 8px; }
#so-booking-widget[data-template="agenda"] .so-bw-slot-row {
    justify-content: flex-start;
    gap: 16px;
    padding: 14px 16px;
    border-radius: var(--so-bw-ds-radius-card);
    font-size: 15px;
    font-weight: 600;
}
/* was @media (max-width:820px) — now gated on the CONTAINER width
   via data-cw (see booking-widget.js _wireContainerWidth). :where()
   keeps specificity identical to the media query it replaces. */

    /* Rows restated as well as columns — same reason as the split collapse
       above: the header is pinned to `grid-row: 1` for the right-hand pane,
       so moving it into column 1 without a new row lands it on top of the
       rail and the two paint over each other. */
    #so-booking-widget:where([data-cw~="820"])[data-template="agenda"] .so-bw-container{
        grid-template-columns: 1fr;
        grid-template-rows: auto auto 1fr auto;
    }
    #so-booking-widget:where([data-cw~="820"])[data-template="agenda"] .so-bw-rail{ grid-column: 1; grid-row: 1; border-right: none; border-bottom: 1px solid var(--so-bw-ds-n100); }
    #so-booking-widget:where([data-cw~="820"])[data-template="agenda"] .so-bw-header{ grid-column: 1; grid-row: 2; }
    #so-booking-widget:where([data-cw~="820"])[data-template="agenda"] .so-bw-body{ grid-column: 1; grid-row: 3; }
    #so-booking-widget:where([data-cw~="820"])[data-template="agenda"] .so-bw-nav{ grid-column: 1; grid-row: 4; }


/* ── M · TIMELINE (timeline-plum) — part 2 of 2: the DAY AXIS ────────────
   The opposite proposition to Agenda, which it used to be a recolour of.
   Agenda throws the calendar away and lists openings forward across days;
   Timeline KEEPS the calendar, you pick one day, and that day is then drawn
   as a vertical axis with the openings pinned along it. The gaps between
   slots are drawn as gaps, so the shape of the day — a busy morning, a dead
   afternoon — is visible at a glance. Reads like a diary page, not a list.

   Built entirely on the existing slot markup:
     .so-bw-slot-panel  →  the axis's container (the diary page)
     .so-bw-slot-group  →  one period (Morning / Afternoon / Evening)
     .so-bw-slot-row    →  one opening, hung off the axis by a node + tick */
#so-booking-widget[data-template="timeline"] .so-bw-container {
    /* The literal fallback for the token in §23.1, kept in step with it. The
       "one column, 680px" this used to say described the shell BEFORE it grew a
       sidebar; the day axis below still gets one undivided column, but it is
       now the pane's width rather than the whole card's. */
    max-width: 1040px;
}
/* Calendar above, axis below — never side by side: the axis needs the full
   width to read as a day, and a 300px column would squash it back into the
   pill grid this architecture exists to replace. */
#so-booking-widget[data-template="timeline"] .so-bw-datetime {
    grid-template-columns: 1fr;
}
/* The diary page. Flat and paper-like — the axis is the structure here, so
   a second card border around it just adds noise. */
#so-booking-widget[data-template="timeline"] .so-bw-slot-panel {
    border: none;
    background: transparent;
    border-radius: 0;
    padding: 4px 0 0;
    gap: 0;
}
/* Each period hangs off the axis. The gutter holds the period label and the
   axis line itself; ::before draws the line so it runs UNBROKEN through the
   whole group, including the empty space between two distant openings — that
   continuity is what makes a gap read as a gap rather than as nothing.
   Gutter geometry: padding-left − line-left is FIXED at 18px, which is what
   .so-bw-slot-row::before's -22px node offset is derived from. Change one and
   you must change the other, or the nodes fall off the line. The gutter is
   110px so the longest period name ("Afternoon") fits without truncating. */
#so-booking-widget[data-template="timeline"] .so-bw-slot-group {
    /* The gutter, once. The line's centre sits --so-bw-tl-gutter to the LEFT of
       the content edge, which is what every node below is measured against —
       so this is the only number that has to move when the gutter changes. */
    --so-bw-tl-gutter: 17px;
    --so-bw-tl-pad-l: 110px;
    position: relative;
    padding: 0 0 22px var(--so-bw-tl-pad-l);
    gap: 0;
}
#so-booking-widget[data-template="timeline"] .so-bw-slot-group::before {
    content: '';
    position: absolute;
    left: calc(var(--so-bw-tl-pad-l) - var(--so-bw-tl-gutter) - var(--so-bw-axis-line-w) / 2);
    top: 6px;
    bottom: 0;
    width: var(--so-bw-axis-line-w);
    background: var(--so-bw-axis-line);
}
/* The last period's line stops at its final opening instead of running on
   into the panel's padding, which read as "there is more below" when there
   was not. */
#so-booking-widget[data-template="timeline"] .so-bw-slot-group:last-child::before {
    bottom: 22px;
}
/* Period label sits IN the gutter, left of the axis — horizontal, aligned to
   the first opening of its period. */
#so-booking-widget[data-template="timeline"] .so-bw-slot-group-title {
    position: absolute;
    left: 0;
    top: 0;
    width: 80px;
    margin: 0;
    padding-top: 11px;
    text-align: right;
    font-size: 9.5px;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--so-bw-text-secondary, #5a6172);
    /* The label is absolutely positioned in an 80px gutter, so wrapping it
       cannot disturb the axis or the rows beside it — it just takes a second
       line in space the gutter already owns. nowrap + ellipsis truncated
       every period name longer than ~11 uppercase characters, which is most
       of them once translated. line-height is tightened so two lines still
       align against the first opening of the period. */
    white-space: normal;
    overflow-wrap: anywhere;
    line-height: 1.25;
}
/* Openings run down the axis, one per line. */
#so-booking-widget[data-template="timeline"] .so-bw-slot-list {
    display: flex;
    flex-direction: column;
    gap: 8px;
    padding-top: 4px;
}
/* Each opening is an entry on the axis: a node on the line, a tick reaching
   out to the row, and the time set left — a diary entry, not a centred pill. */
#so-booking-widget[data-template="timeline"] .so-bw-slot-row {
    --so-bw-axis-node: 10px;
    position: relative;
    justify-content: flex-start;
    text-align: left;
    padding-left: 16px;
    font-size: 14px;
    font-weight: 600;
    border-radius: var(--so-bw-ds-radius-card, 10px);
}
#so-booking-widget[data-template="timeline"] .so-bw-slot-row::before {
    /* The node, on the axis. Both offsets come from the formula in §13 rather
       than from the "92 − 110 + 1 − 5 = −22" that used to be written out here —
       the row inherits --so-bw-tl-gutter from its group, so the line's centre is
       that far left of the row's own edge. */
    content: '';
    position: absolute;
    left: calc((var(--so-bw-tl-gutter) + var(--so-bw-axis-node) / 2) * -1);
    top: 50%;
    width: var(--so-bw-axis-node);
    height: var(--so-bw-axis-node);
    margin-top: calc(var(--so-bw-axis-node) / -2);
    border-radius: 50%;
    background: var(--so-bw-card-bg, #fff);
    border: var(--so-bw-axis-line-w) solid var(--so-bw-axis-ring);
    transition: background 0.15s ease, border-color 0.15s ease;
}
#so-booking-widget[data-template="timeline"] .so-bw-slot-row::after {
    /* The tick joining node to row: it starts at the node's edge and runs to
       the row, so its width is whatever is left of the gutter. */
    content: '';
    position: absolute;
    left: calc((var(--so-bw-tl-gutter) - var(--so-bw-axis-node) / 2) * -1);
    top: 50%;
    width: calc(var(--so-bw-tl-gutter) - var(--so-bw-axis-node) / 2);
    height: var(--so-bw-axis-line-w);
    margin-top: calc(var(--so-bw-axis-line-w) / -2);
    background: var(--so-bw-axis-line);
}
#so-booking-widget[data-template="timeline"] .so-bw-slot-row:hover::before {
    border-color: var(--so-bw-primary);
}
#so-booking-widget[data-template="timeline"] .so-bw-slot-row--selected::before {
    background: var(--so-bw-primary);
    border-color: var(--so-bw-primary);
}
/* A taken slot is still drawn on the axis — a diary shows what is booked, that
   is the point — but its node is filled flat so the eye skips it.
   n200 (#dfe1e6) on white measured ~1.3:1, which delivered "skips it" and not
   much of "shows what is booked": at a 10px disc the node was effectively
   absent, and the row is `disabled` unless it is waitlistable, so the node was
   the only thing carrying the state. n300 is still visibly quieter than the
   accent an open slot gets, without being invisible. */
#so-booking-widget[data-template="timeline"] .so-bw-slot-row--booked::before {
    background: var(--so-bw-ds-n300, #c1c7d0);
    border-color: var(--so-bw-ds-n300, #c1c7d0);
}
/* Narrow screens: pull the gutter in and drop the period label above the
   axis rather than beside it — 92px of gutter on a 360px phone leaves
   nothing for the time itself. */
/* was @media (max-width: 560px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    /* ONE number moves the whole axis in now. The line, the nodes and the ticks
       are all derived from --so-bw-tl-pad-l and --so-bw-tl-gutter, so the two
       rules that used to restate `left: 8px` and `left: -22px` here are gone —
       they were the same arithmetic done again by hand, and the second copy is
       exactly what drifted. */
    /* MOVED FROM 560 TO 640, off the grandfathered rung and onto the shared
       ladder. 560 was inherited from the old 680px shell; with a sidebar above
       the axis the 110px gutter runs out of room a tier earlier.

       THE AXIS STAYS VERTICAL. This is the half of the audit's D8 that is worth
       acting on: a day drawn downwards is the one architecture in the catalogue
       that genuinely gets BETTER with the free height a phone has, and turning
       it into a pill grid here would delete the template's whole argument on
       the one screen that makes it. Only the gutter narrows and the period
       label changes home. (The step rail does lie down — see M3 for why the
       same reasoning does not carry over to it.) */
    #so-booking-widget:where([data-cw~="640"])[data-template="timeline"] .so-bw-slot-group{
        --so-bw-tl-pad-l: 26px;
        padding-top: 20px;
    }
    /* The period label becomes a STICKY SUB-HEAD over its own group rather than
       a caption pulled onto the line. Sitting on the line it read as a label
       for the first opening rather than for the period; sticky, it stays
       visible while that period's openings scroll past, which is the fact a
       customer scrolling a forty-slot day actually needs.

       It sticks to `.so-bw-slot-scroll`, which is the scroll container — the
       flattened panel has no box of its own to stick to (see the panel-head
       note in the shared section, which is the same problem). */
    #so-booking-widget:where([data-cw~="640"])[data-template="timeline"] .so-bw-slot-group-title{
        position: sticky;
        top: 0;
        z-index: 2;
        width: auto;
        padding: 6px 0;
        /* Back to the axis line, not to the group's own left edge: a full
           -26px put the first letter hard against the card and clipped it. */
        margin-left: calc(var(--so-bw-tl-gutter) * -1 - 1px);
        text-align: left;
        /* Opaque, or the openings scroll visibly through it. */
        background: var(--so-bw-bg, #fff);
    }


/* ── H · WEEK BOARD (weekboard-amber) ────────────────────────────────────
   A week of day columns, each showing what it can actually offer. Day and
   time collapse into ONE decision instead of two, and a quiet week is
   honestly visible rather than hidden behind a calendar.

   Driven by the week-board mode in booking-widget.js, which emits
   .so-bw-week-* from the `weekDays` getter.

   This block existed, finished, for the whole life of the consolidated
   catalogue — and nothing ever rendered it. There was no .so-bw-week markup
   anywhere in the widget, so Week Board fell through to the plain month
   calendar and shipped as Centred in amber: the one template in the catalogue
   that genuinely reframes how time is chosen was the one template that did
   not do its own thing. The markup exists now; the notes below record what
   the styling has to hold up.

   Two changes to what was written here before:

     • SEVEN columns, not five. The old grid was repeat(5, 1fr) — Monday to
       Friday. Salons, restaurants, clinics and classes are exactly the
       businesses this template is for, and they open at weekends; a five-wide
       board silently dropped two of their days. The month grid it replaces is
       Mon-first and seven wide, so five was also inconsistent with the
       calendar sitting behind the same decision.
     • The board OUTLIVES the date pick. .so-bw-calendar is
       x-show="!selectedDate", so Agenda's day list (which lives inside it)
       correctly disappears when it hands over to the slot panel. The board
       cannot: it *is* the time picker, and it has to stay up showing which
       slot is lit. Hence .so-bw-week-wrap is a sibling, and the calendar is
       hidden outright here rather than handed over to. */
#so-booking-widget[data-template="weekboard"] .so-bw-container { max-width: 1000px; }
#so-booking-widget[data-template="weekboard"] .so-bw-header { background: var(--so-bw-ds-n0); }
#so-booking-widget[data-template="weekboard"] .so-bw-stepbar-label { display: none; }
#so-booking-widget[data-template="weekboard"] .so-bw-stepbar { gap: 7px; }
#so-booking-widget[data-template="weekboard"] .so-bw-stepbar-dot { width: 7px; height: 7px; font-size: 0; border-radius: 999px; }
#so-booking-widget[data-template="weekboard"] .so-bw-stepbar-dot svg { display: none; }
#so-booking-widget[data-template="weekboard"] .so-bw-nav { background: var(--so-bw-ds-n25); }

/* Emitted for every template, revealed for one — the same contract
   .so-bw-agenda-list and .so-bw-calendar-second follow. */
#so-booking-widget .so-bw-week-wrap { display: none; }
#so-booking-widget[data-template="weekboard"] .so-bw-week-wrap { display: block; }
/* The board replaces the month outright. Not `.so-bw-calendar > .so-bw-calendar-scroll`
   (Agenda's trick, which keeps the month header for its arrows) because the
   board carries its own week pager and a second set of arrows meaning
   something different would be two ways to move through the same decision. */
#so-booking-widget[data-template="weekboard"] .so-bw-calendar { display: none; }
/* One column: the board needs the full width for seven days. */
#so-booking-widget[data-template="weekboard"] .so-bw-datetime { grid-template-columns: 1fr; }
/* The chosen day's full list still opens below the board — the board caps at
   eight times per column, so "+N more" has to lead somewhere real. */
#so-booking-widget[data-template="weekboard"] .so-bw-slot-panel { margin-top: 18px; }

#so-booking-widget .so-bw-week-pager {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    margin-bottom: 14px;
}
#so-booking-widget .so-bw-week-range {
    font-family: var(--so-bw-mono);
    font-size: 13px;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    color: var(--so-bw-text);
}
#so-booking-widget .so-bw-week {
    display: grid;
    grid-template-columns: repeat(7, minmax(0, 1fr));
    gap: 8px;
    align-items: start;
}
#so-booking-widget .so-bw-week-col { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
#so-booking-widget .so-bw-week-head {
    text-align: center;
    padding-bottom: 10px;
    border-bottom: 2px solid var(--so-bw-ds-n100);
}
#so-booking-widget .so-bw-week-col--active .so-bw-week-head { border-bottom-color: var(--so-bw-primary); }
/* A past day stays in the grid — the week is only honest if the days it
   cannot offer are visible as days it cannot offer. */
#so-booking-widget .so-bw-week-col--past { opacity: 0.45; }
#so-booking-widget .so-bw-week-dow { font-size: 11.5px; font-weight: 600; color: var(--so-bw-ds-n500); }
#so-booking-widget .so-bw-week-num {
    font-family: var(--so-bw-mono);
    font-size: 19px;
    font-weight: 600;
    margin-top: 2px;
}
/* The one-action commit. Full column width so the target is the whole cell,
   and never below the design system's 44px floor. */
#so-booking-widget .so-bw-week-slot {
    display: block;
    width: 100%;
    min-height: var(--so-bw-ds-tap);
    padding: 8px 4px;
    border: 1px solid var(--so-bw-border);
    border-radius: var(--so-bw-ds-radius-chip);
    background: var(--so-bw-card-bg);
    color: var(--so-bw-text);
    font-family: var(--so-bw-mono);
    font-size: 12px;
    font-weight: 500;
    font-variant-numeric: tabular-nums;
    text-align: center;
    cursor: pointer;
    transition: all var(--so-bw-ds-motion);
}
#so-booking-widget .so-bw-week-slot:hover { border-color: var(--so-bw-primary); }
#so-booking-widget .so-bw-week-slot--selected {
    background: var(--so-bw-primary);
    border-color: var(--so-bw-primary);
    color: #fff;
}
#so-booking-widget .so-bw-week-more {
    text-align: center;
    padding: 6px 2px;
    font-size: 11px;
    font-weight: 600;
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
    cursor: pointer;
}
#so-booking-widget .so-bw-week-empty {
    text-align: center;
    padding: 14px 4px;
    font-size: 11px;
    color: var(--so-bw-ds-n500);
    border: 1px dashed var(--so-bw-ds-n200);
    border-radius: var(--so-bw-ds-radius-ctl);
}
#so-booking-widget .so-bw-week-hint {
    margin-top: 12px;
    font-size: 12px;
    color: var(--so-bw-text-secondary);
    text-align: center;
}
/* Seven 44px-tall targets need roughly 88px of column each to keep a time like
   "10:30" on one line, so seven columns survive down to about 640px of widget.

   Below that the board does NOT reflow into four columns and then two. Four
   columns of a seven-day week is the one shape that actively misleads: the
   week silently wraps, Thursday sits under Sunday, and the row the customer
   reads as "a week" is three days short. A week that cannot be seven columns
   has to stop being columns.

   So it becomes seven day SECTIONS, each a heading with that day's times
   wrapping under it. Still one screen, still day-then-time in one decision,
   still every day present including the empty ones — the board's whole
   argument — just stacked instead of ruled. */
/* was @media (max-width: 640px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    #so-booking-widget:where([data-cw~="640"]) .so-bw-week{
        display: block;
    }
    #so-booking-widget:where([data-cw~="640"]) .so-bw-week-col{
        flex-direction: row;
        flex-wrap: wrap;
        align-items: center;
        gap: 6px;
        padding-bottom: 12px;
        margin-bottom: 12px;
        border-bottom: 1px solid var(--so-bw-ds-n100);
    }
    #so-booking-widget:where([data-cw~="640"]) .so-bw-week-col:last-child{
        margin-bottom: 0;
        border-bottom: 0;
    }
    /* The heading takes its own line and turns into a row: day name and
       numeral side by side, the way a diary entry is headed. */
    #so-booking-widget:where([data-cw~="640"]) .so-bw-week-head{
        display: flex;
        align-items: baseline;
        gap: 8px;
        flex: 0 0 100%;
        padding-bottom: 0;
        border-bottom: 0;
        text-align: left;
    }
    #so-booking-widget:where([data-cw~="640"]) .so-bw-week-num{ margin-top: 0; font-size: 16px; }
    /* Times wrap inline under the heading rather than filling the width one
       per line, which on a 360px phone would be seven screens of scrolling. */
    #so-booking-widget:where([data-cw~="640"]) .so-bw-week-slot{
        display: inline-flex;
        align-items: center;
        justify-content: center;
        width: auto;
        min-width: 78px;
        padding: 8px 12px;
    }
    #so-booking-widget:where([data-cw~="640"]) .so-bw-week-empty,
    #so-booking-widget:where([data-cw~="640"]) .so-bw-week-more{
        flex: 1 1 auto;
        padding: 8px 4px;
        text-align: left;
    }
    #so-booking-widget:where([data-cw~="640"]) .so-bw-week-empty{ border: 0; }


/* ── J · CARD DECK (pocket-violet, coral-pop) ────────────────────────────
   Steps as a physical stack, the next card peeking beneath. Progress is
   FELT rather than read, which suits a short booking on a phone where a
   numbered stepper is more chrome than help. */
#so-booking-widget[data-template="deck"] {
    background: color-mix(in srgb, var(--so-bw-primary) 10%, #f4f2f8);
    padding: 28px 24px 40px;
    border-radius: var(--so-bw-r-20);
}
#so-booking-widget[data-template="deck"] .so-bw-container {
    max-width: 600px;
    margin-top: 16px;
    border: 1px solid var(--so-bw-ds-n200);
    border-radius: var(--so-bw-r-14);
    box-shadow: 0 20px 44px -18px color-mix(in srgb, var(--so-bw-primary) 40%, transparent);
    overflow: visible;
}
/* The two cards beneath. Purely decorative, so they are inert. */
#so-booking-widget[data-template="deck"] .so-bw-container::before,
#so-booking-widget[data-template="deck"] .so-bw-container::after {
    content: '';
    position: absolute;
    height: 40px;
    background: var(--so-bw-ds-n0);
    border: 1px solid var(--so-bw-ds-n200);
    border-radius: var(--so-bw-r-14);
    pointer-events: none;
    z-index: -1;
}
#so-booking-widget[data-template="deck"] .so-bw-container::before { left: 24px; right: 24px; top: -16px; opacity: 0.55; }
#so-booking-widget[data-template="deck"] .so-bw-container::after  { left: 12px; right: 12px; top: -8px;  opacity: 0.8; }
#so-booking-widget[data-template="deck"] .so-bw-header { background: var(--so-bw-ds-n0); border-radius: var(--so-bw-r-14) var(--so-bw-r-14) 0 0; }
#so-booking-widget[data-template="deck"] .so-bw-step-label { font-size: 21px; letter-spacing: -0.02em; }
#so-booking-widget[data-template="deck"] .so-bw-stepbar-label { display: none; }
#so-booking-widget[data-template="deck"] .so-bw-stepbar { gap: 7px; }
#so-booking-widget[data-template="deck"] .so-bw-stepbar-dot { width: 7px; height: 7px; font-size: 0; border-radius: 999px; }
#so-booking-widget[data-template="deck"] .so-bw-stepbar-dot svg { display: none; }
#so-booking-widget[data-template="deck"] .so-bw-progress { display: none; }
#so-booking-widget[data-template="deck"] .so-bw-slot-list { grid-template-columns: repeat(2, 1fr); }


/* ── K · WIDE CANVAS (dashboard-plum) ────────────────────────────────────
   Desktop-first. Month and times in one row with the commit bar docked
   below. The most information-dense of the architectures, the fewest
   scrolls.

   It used to claim a THIRD column — "what to expect" guidance — and carried
   ~12 lines of .so-bw-guidance CSS for it. Nothing ever emitted that markup,
   and nothing could have: there is no per-service guidance field anywhere in
   the schema, the admin, or the API, so the copy the column would render does
   not exist as data. The block was removed rather than left as a promise the
   product cannot keep; the gallery description in WidgetPage.vue was corrected
   to match. Restoring it is a schema + admin + API change, not a stylesheet
   one — see the .so-bw-week note under WEEK BOARD for the same distinction. */
/* 920px — an embeddable widget, not a page section. This was briefly 80vw, and
   that was the wrong shape: it made the card track the viewport, so on a large
   monitor the two columns drifted far enough apart that the summary stopped
   reading as belonging to the list beside it.

   This rule is the FALLBACK path and is deliberately kept in step with the
   token in section 23.2. The real width comes from `--so-bw-tpl-maxw` via
   `[data-template][data-template-id] .so-bw-container`, which needs BOTH
   attributes; an install that has never picked a template renders without
   `data-template-id`, and then this is the rule that applies. The two must
   agree or those installs would be the only ones at a different width. */
#so-booking-widget[data-template="wide"] .so-bw-container { max-width: 920px; }
#so-booking-widget[data-template="wide"] .so-bw-header { background: var(--so-bw-ds-n0); }
#so-booking-widget[data-template="wide"] .so-bw-step-indicator { align-items: center; }
#so-booking-widget[data-template="wide"] .so-bw-stepbar-dot { width: 20px; height: 20px; font-size: 10px; }
#so-booking-widget[data-template="wide"] .so-bw-progress { display: none; }
/* The month/times row is set up further down, against the REAL wrapper
   (.so-bw-datetime). An earlier version of this block targeted an invented
   .so-bw-datetime-grid class that the markup never had, so the rule was
   dead and the calendar sprawled to the full 1,120px. */
/* The commit bar is docked: selection and price on the left, actions right. */
#so-booking-widget[data-template="wide"] .so-bw-nav {
    background: var(--so-bw-ds-n25);
    border-top: 1px solid var(--so-bw-ds-n100);
}
/* The 980px stack for this architecture lives with the real .so-bw-datetime
   rule further down. The media query that used to sit here targeted
   .so-bw-datetime-grid — the same invented class the comment above records —
   so it never matched anything either. */

/* ── The seven survivors ────────────────────────────────────────────────
   Each keeps the one thing the twelve don't offer. */

/* Daypart and Timeline used to live here, as per-id "signatures" bolted onto
   Centred and Agenda. Both were dead code — they styled .so-bw-slot-group-label
   and .so-bw-time-slot, neither of which the widget renders (the real classes
   are .so-bw-slot-group-title and .so-bw-slot-row) — which is why each was
   indistinguishable from the architecture it borrowed, bar the accent colour.
   Neither idea was ever a "signature" anyway: both were structural. They are
   architectures N and M in section 14 now. */

/* Dual Month used to live here too, as a rule that set the ONE month's grid to
   repeat(14, 1fr). That does not produce two months — it spreads a single
   month's 42 cells over 14 columns, i.e. a mangled version of the calendar
   Wide Canvas already had. Two months needs a second month to be BUILT, which
   is a widget change, not a stylesheet one; it is architecture O now (see
   section 14) and booking-widget.js populates calendarDaysNext for it. */

/* Frost — the one surface treatment in the catalogue. Hero's architecture,
   but the card is glass rather than paper. */
#so-booking-widget[data-template-id="glass-frost"] {
    background: linear-gradient(150deg, #dbe6f5, #eef3fb 45%, #d7e3f4);
    padding: 32px 20px;
    border-radius: var(--so-bw-r-20);
}
#so-booking-widget[data-template-id="glass-frost"] .so-bw-body,
#so-booking-widget[data-template-id="glass-frost"] .so-bw-nav {
    background: rgba(255, 255, 255, 0.62);
    backdrop-filter: blur(18px) saturate(150%);
    -webkit-backdrop-filter: blur(18px) saturate(150%);
    border-color: rgba(255, 255, 255, 0.7);
}
#so-booking-widget[data-template-id="glass-frost"] .so-bw-card {
    background: rgba(255, 255, 255, 0.55);
    border-color: rgba(255, 255, 255, 0.75);
}

/* Dusk — the dark register, and the only template designed dark-first.
   Champagne is too light to carry white text, so selected states invert. */
#so-booking-widget[data-template-id="dusk-champagne"] .so-bw-container {
    --so-bw-text: #e9e6df;
    --so-bw-bg: #14161d;
    --so-bw-bg-secondary: #14161d;
    --so-bw-bg-tertiary: #1f2330;
    --so-bw-card-bg: #1a1d26;
    --so-bw-card-bg-hover: #20242f;
    --so-bw-border: #2a2f3d;
    --so-bw-header-border: #2a2f3d;
    /* The INK end of the design-system ramp has to invert with the canvas.
       Headings read from --so-bw-ds-n900 and muted copy from n500/n700 — none
       of them from --so-bw-text — so on this dark-first template they were
       still the stock near-black: the step title and the sign-in heading
       rendered black-on-black and simply looked absent. */
    --so-bw-ds-n900: #f2efe8;
    --so-bw-ds-n700: #cfcbc0;
    --so-bw-ds-n500: #9a9689;
    --so-bw-ds-n300: #4a4f5e;
    --so-bw-ds-n200: #343948;
    --so-bw-ds-n100: #2a2f3d;
    --so-bw-ds-n50:  #20242f;
    --so-bw-ds-n25:  #191c25;
    --so-bw-ds-n0:   #14161d;
}
#so-booking-widget[data-template-id="dusk-champagne"] .so-bw-header,
#so-booking-widget[data-template-id="dusk-champagne"] .so-bw-body,
#so-booking-widget[data-template-id="dusk-champagne"] .so-bw-nav {
    background: #14161d;
    color: #e9e6df;
}
#so-booking-widget[data-template-id="dusk-champagne"] .so-bw-rail {
    background: linear-gradient(168deg, #1c1f29, #0f1117);
}
#so-booking-widget[data-template-id="dusk-champagne"] .so-bw-btn--primary,
#so-booking-widget[data-template-id="dusk-champagne"] .so-bw-slot-row--selected,
#so-booking-widget[data-template-id="dusk-champagne"] .so-bw-calendar-day--selected {
    color: #241d12;
}

/* Editorial — the serif register. Type does the work colour does elsewhere,
   so the only signature is a face and a headline that earns its size. */
#so-booking-widget[data-template-id="editorial-gold"] .so-bw-container {
    --so-bw-font: 'Newsreader', Georgia, serif;
    --so-bw-bg: #faf9f6;
    --so-bw-bg-secondary: #faf9f6;
}
#so-booking-widget[data-template-id="editorial-gold"] .so-bw-step-label,
#so-booking-widget[data-template-id="editorial-gold"] .so-bw-rail-title {
    font-family: 'Newsreader', Georgia, serif;
    font-weight: 500;
    letter-spacing: -0.015em;
}
#so-booking-widget[data-template-id="editorial-gold"] .so-bw-header,
#so-booking-widget[data-template-id="editorial-gold"] .so-bw-body,
#so-booking-widget[data-template-id="editorial-gold"] .so-bw-nav { background: #faf9f6; }

/* Coral Pop — the soft register, the counterpart to Dusk's dark one. It is
   Card Deck's architecture with a different VOICE, the way Dusk is Split's.

   It used to be nothing but these radius tokens, and they were invisible: an
   18px card corner beside a 12px one is a difference you can measure and not
   one you can see, so Coral Pop and Card Deck rendered as the same design in
   two hues. A register has to land at a glance or it isn't one. So the blush
   surfaces are painted on, the corners go fully round, and the cards run one
   per row with room to breathe — soft is now something you can see before you
   read a word. */
#so-booking-widget[data-template-id="coral-pop"] .so-bw-container {
    --so-bw-ds-radius-chip: 999px;
    --so-bw-ds-radius-ctl: 999px;
    --so-bw-ds-radius-card: var(--so-bw-r-22);
    --so-bw-ds-radius-shell: var(--so-bw-r-28);
    --so-bw-ds-tap: 52px;
    --so-bw-slot-radius: 999px;
    --so-bw-btn-radius: 999px;
    --so-bw-card-radius: var(--so-bw-r-22);
    --so-bw-input-radius: 999px;
    --so-bw-card-pad: 18px;
    --so-bw-card-gap: 16px;
    --so-bw-bg-secondary: #fff5f4;
    --so-bw-bg-tertiary: #ffeceb;
    --so-bw-border: #f6d9d6;
}
#so-booking-widget[data-template-id="coral-pop"] .so-bw-container,
#so-booking-widget[data-template-id="coral-pop"] .so-bw-header { border-radius: var(--so-bw-r-28) var(--so-bw-r-28) 0 0; }
#so-booking-widget[data-template-id="coral-pop"] .so-bw-nav { border-radius: 0 0 var(--so-bw-r-28) var(--so-bw-r-28); }
/* Blush surfaces — the shell is warm, the cards stay paper-white so they lift
   off it. This is the whole difference from Card Deck's white-on-white. */
#so-booking-widget[data-template-id="coral-pop"] .so-bw-header,
#so-booking-widget[data-template-id="coral-pop"] .so-bw-body,
#so-booking-widget[data-template-id="coral-pop"] .so-bw-nav { background: #fff5f4; }
#so-booking-widget[data-template-id="coral-pop"] .so-bw-card { border-radius: var(--so-bw-r-22); }
#so-booking-widget[data-template-id="coral-pop"] .so-bw-card-icon { border-radius: 999px; }
/* One card per row. A friendly template does not ask you to compare two
   columns of options — and it stops long service names wrapping in a 260px
   cell, which is exactly the cramped feeling the register is meant to avoid. */
#so-booking-widget[data-template-id="coral-pop"] .so-bw-card-grid {
    grid-template-columns: 1fr;
    gap: 10px;
}
/* One column makes the list about twice as tall, and the deck shell is
   deliberately max-height:none so its peeking cards aren't clipped. That is
   fine: the deck's commit bar is position:sticky;bottom:0, so it stays docked
   to the viewport while the longer list scrolls past it. Do NOT add a
   max-height here — it would clip the peek the architecture is named for. */
#so-booking-widget[data-template-id="coral-pop"] .so-bw-btn { border-radius: 999px; }
#so-booking-widget[data-template-id="coral-pop"] .so-bw-input,
#so-booking-widget[data-template-id="coral-pop"] .so-bw-select { border-radius: 999px; }

/* The sticky search field paints the whole strip it pins into.
   ------------------------------------------------------------------------
   Cards were visible ABOVE and BELOW the pinned field while the list moved,
   and the field itself read as translucent. Neither is a transparency bug in
   the field — both are unpainted bands around it:

   ABOVE — `[data-template="soft"] .so-bw-body { padding: 8px 26px 4px }`.
   A sticky element offsets from the scrollport's PADDING edge, so `top: 0`
   parks the field 8px down and scrolled content keeps passing through that
   8px strip. The body's top padding is dropped here and reissued as padding
   on the field's own wrapper, which has a background and therefore covers it.

   BELOW — the wrapper's `margin: 0 0 14px`. Margin is never painted, so the
   next card slid through a 14px window directly under the field. Converted to
   padding for the same reason.

   The wrapper background is the BODY's blush, not `--so-bw-card-bg`. The
   consolidated sticky rule at the end of the file sets the card token, which
   on this template is white and would band a white stripe across a blush
   canvas. That rule is later at (1,2,0), so the attribute is repeated here to
   reach (1,3,0) rather than relying on source order. */
/* The strip runs to the top of the scroll region — no band above it.
   `.so-bw-step` carries `padding: var(--so-bw-step-pad) var(--so-bw-gutter)`,
   and --so-bw-step-pad is 24px. That 24px sat between the widget header and
   the first thing in the step, so the pinned field had an empty band above it
   on arrival and only closed the gap once you had scrolled past it. Dropping
   the step's TOP padding lets the strip's own 10px do that spacing instead,
   which is padding the strip actually paints — so the distance from the header
   looks the same whether the list is scrolled or not. The step's side and
   bottom padding are untouched. */
#so-booking-widget[data-template-id="coral-pop"] .so-bw-step { padding-top: 0; }
#so-booking-widget[data-template-id="coral-pop"] .so-bw-body {
    /* No strip above the pinned field (see above), and the side inset drops to
       the 20px the cards use so the full-bleed strip below can be measured
       against one number instead of two. */
    padding: 0 20px 4px;
    /* `scroll-behavior: smooth` is set on .so-bw-body further down the file.
       Smooth scrolling animates the scroll offset, and a sticky element's
       position is resolved against that offset every frame — so the field
       lags the list by a frame or two and visibly wobbles as it settles.
       Sticky wants the real offset, not an animated one. */
    scroll-behavior: auto;
}
/* FULL-BLEED. The wrapper is pulled out over the body's 20px side padding and
   given it back as its own padding, so the opaque strip runs edge to edge.
   Without this it spans only the content box and cards travel visibly through
   the two 20px side channels beside the field — which reads exactly like a
   transparent search bar, because you are seeing content move at the same
   height as the bar. */
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-search {
    /* One number for the strip's inset. The icon and the clear button are
       positioned against THIS element, not against the input, so they have to
       be offset by it — see their rules below. Kept as a custom property so the
       three can never drift apart. */
    --so-bw-cp-strip-pad: 20px;
    margin: 0 calc(var(--so-bw-cp-strip-pad) * -1);
    padding: 10px var(--so-bw-cp-strip-pad) 12px;
    /* `background-color`, not the `background` shorthand: the shorthand also
       resets background-image, and a template or Studio theme that sets a
       gradient on this element would be silently dropped rather than layered. */
    background-color: #fff5f4;
    /* The field owns the strip outright — nothing scrolling under it composites
       through, whatever a host theme does to blend modes further up. */
    backdrop-filter: none;
    isolation: isolate;
    /* NO `will-change: transform` / `translateZ(0)` here — deliberately.
       They were tried to stop the field trembling and are the wrong tool:
       Chrome applies sticky offsets ON THE COMPOSITOR via a sticky constraint,
       and hand-promoting the element to its own layer opts out of that path
       and hands positioning back to the main thread — which is precisely the
       condition that makes a sticky element lag the scroll and shimmer. The
       promotion made the symptom it was added to cure. */
    /* Above everything that scrolls beneath it. The consolidated sticky rule
       sets 4, which clears an ordinary card — but a card that opens its extras
       or shows a media carousel paints children at 2 and an edge overlay above
       those, and a hovered card lifts on a transform that creates its own
       stacking context. 20 puts the field out of that argument entirely.

       It cannot reach the commit bar despite being the larger number: the nav
       is a SIBLING of .so-bw-body, so the two are ordered against each other at
       container level and this value only ranks the field among the things
       inside the body with it. */
    z-index: 20;
}
/* WHITE FIELD on the blush strip, borderless.
   Opaque by literal, not by token: the field sits over its own scrolling list,
   so a token that any theme could set to a translucent value (several dark
   templates set `--so-bw-card-bg` to `rgba(255,255,255,0.06)`) would put the
   see-through bug straight back.

   The border is removed, so the only thing separating a white field from the
   blush strip behind it is the shadow — #ffffff on #fff5f4 is a 3% luminance
   step, which is not an edge. Without it the control loses its shape and reads
   as text floating on the canvas, so the shadow is doing the border's job
   rather than decorating. */
#so-booking-widget[data-template-id="coral-pop"] .so-bw-search-input {
    background-color: #ffffff;
    border: 0;
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
    color: #2a1f1e;
    /* The icon and the text sit together: the glyph occupies 16px from x=14,
       so a 36px text inset leaves a 6px gap after it — one unit, rather than a
       glyph pinned to the far edge with a gulf between. */
    padding-left: 36px;
    padding-right: 38px;
}
#so-booking-widget[data-template-id="coral-pop"] .so-bw-search-input::placeholder {
    color: rgba(42, 31, 30, 0.45);
}
/* Focus is a ring only — there is no border left to recolour. Both shadows are
   restated because box-shadow does not merge across rules; dropping the first
   would take the field's own edge away exactly when it is being used. */
#so-booking-widget[data-template-id="coral-pop"] .so-bw-search-input:focus {
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05), 0 0 0 3px var(--so-bw-primary-light);
}
/* Icon and clear button read against the FIELD's white ground now, not the
   dark one — same ink as the text it sits beside, at a lower weight so it
   supports the placeholder instead of competing with it. */
/* Both are absolutely positioned against .so-bw-search, and that element is now
   full-bleed — its padding box starts 20px OUTSIDE the input. A bare `left:
   14px` therefore lands 6px to the left of the field's own edge, which is what
   detached the glyph from the text it belongs to. Adding the strip inset puts
   the icon 14px inside the INPUT: it spans 14–30px of the field, and the text
   inset of 36px leaves a 6px gap after it. */
#so-booking-widget[data-template-id="coral-pop"] .so-bw-search-icon {
    left: calc(var(--so-bw-cp-strip-pad, 20px) + 14px);
    color: rgba(42, 31, 30, 0.55);
}
#so-booking-widget[data-template-id="coral-pop"] .so-bw-search-clear {
    right: calc(var(--so-bw-cp-strip-pad, 20px) + 10px);
    color: rgba(42, 31, 30, 0.55);
}
#so-booking-widget[data-template-id="coral-pop"] .so-bw-search-clear:hover {
    background: rgba(42, 31, 30, 0.08);
    color: #2a1f1e;
}

/* The recap's `display: none` default stood here, with Accordion and
   Conversation the only two layouts that turned it back on. Both are
   withdrawn and the markup went with them, so the default is styling nothing
   — widget-architecture-markup.test.ts is what catches that, and the rule is
   deleted rather than left behind. */

/* ── Wide Canvas surfaces, switched OFF for everyone ─────────────────────
   Same contract the recap above used to state, and for the same reason: the
   markup is emitted once for all twenty-two architectures, and exactly one
   reveals it (section C9). Without this block every template would grow a
   summary column, a chip bar and two new row buttons.

   ORDER MATTERS AGAINST ALPINE. `x-show` writes an inline `display: none`
   when its expression is false and REMOVES the property when true — it does
   not write `display: block`. So a hidden element is hidden by the inline
   style, and a "shown" element falls back to whatever the cascade says,
   which here is `none` on twenty-one templates and a real value on Wide
   Canvas. Both directions therefore work without any template needing to
   know about `x-show`. This is the same behaviour that the `.so-bw-step-host`
   note in booking-widget.js records the hard way, in reverse: there, CSS had
   to carry a value BECAUSE Alpine erases the inline one. */

/* The step's titled section header. `contents` rather than `none`, because it
   WRAPS `.so-bw-step-desc` — a paragraph the other twenty-one templates have
   always rendered. Flattening the box leaves that paragraph laying out as a
   child of `.so-bw-step` exactly as it did before this wrapper existed, so the
   markup change is invisible everywhere but here. Verified there is no
   `.so-bw-step > *` or `:first-child` rule in this file that the extra level
   of nesting would have broken. */
#so-booking-widget .so-bw-svchead { display: contents; }
#so-booking-widget .so-bw-svchead__icon,
#so-booking-widget .so-bw-svchead__title { display: none; }

/* Category chips. The other templates keep listing every category under its
   own `.so-bw-group-title`, which is what they do today — and `svcCat` stays
   'all' for the whole session there, so `filteredGroupedServices` never
   narrows behind their backs. */
#so-booking-widget .so-bw-catbar { display: none; }

/* The explicit commit button on a service or staff card. Everywhere else the
   whole card is the click target and a second affordance inside it would just be
   another thing to tab to for the same action.

   `.so-bw-card-add` used to be listed here too — a +/- icon button on the
   service card. It is gone from the markup: a card showing both a checkbox and
   a plus offers two controls for one decision, so the two were consolidated
   onto this one worded button. Its rules went with it rather than being left
   behind styling nothing, which widget-architecture-markup.test.ts enforces. */
#so-booking-widget .so-bw-card-pick { display: none; }

/* A staff member's ROLE as its own line. The other templates keep showing one
   line — `member.bio || member.title` — which is what they show today. */
#so-booking-widget .so-bw-card-role { display: none; }

/* The summary column, and the selection facts docked in the nav bar. */
#so-booking-widget .so-bw-sumpanel { display: none; }
#so-booking-widget .so-bw-navstats { display: none; }

/* Accordion revealed one half of .so-bw-recap here as collapsed, editable
   answer rows, and Conversation the other half as a chat transcript. Both
   templates are withdrawn, so both blocks go with them, along with the
   .so-bw-chat / -ask / -said selectors they were the only users of. */

/* ── Structural padding, restored above the box reset ────────────────────
   `#so-booking-widget *, ::before, ::after { padding: 0 }` near the top of
   this file is a hard reset — it exists so a host theme's `p { padding }`
   or `div { padding }` can't leak into the widget. Being ID-qualified it
   scores (1,0,0), which beats every plain `.so-bw-*` class rule (0,1,0).

   Until the consolidation that was invisible, because each base layout
   restated the paddings inside `#so-booking-widget[data-template="…"]`
   blocks at (1,1,1) and quietly won. Retiring those layouts took the
   padding with them and the whole widget collapsed to zero spacing — the
   header sat flush to the top edge and the step count clipped.

   So the tokenised defaults are restated here ONCE at (1,1,0), which
   clears the reset without touching it. Architectures and signatures keep
   overriding the TOKENS, never these declarations. */
#so-booking-widget .so-bw-header {
    padding: var(--so-bw-header-pad-y-top) var(--so-bw-header-pad-x) var(--so-bw-header-pad-y-bottom);
}
#so-booking-widget .so-bw-step { padding: var(--so-bw-step-pad) var(--so-bw-gutter); }
#so-booking-widget .so-bw-card { padding: var(--so-bw-card-pad); }

/* Selection cards carry role="button" + tabindex="0" so the booking flow can be
   completed from the keyboard. A focusable control with no visible focus ring is
   only nominally reachable — you can tab to it but cannot tell where you are —
   so the ring is part of that fix, not a separate polish item. Matches the
   convention already used by .so-bw-card-more / .so-bw-card-profile. */
#so-booking-widget .so-bw-card[role="button"]:focus-visible {
    outline: 2px solid var(--so-bw-primary);
    outline-offset: 2px;
}
/* The x-inset comes from the gutter, not a literal, so the nav stays on the
   same left edge as the header and the step content — including at the
   breakpoints, where the gutter tightens once for all three. */
#so-booking-widget .so-bw-nav  { padding: 16px var(--so-bw-gutter); }
#so-booking-widget .so-bw-btn  { padding: var(--so-bw-btn-pad-y) var(--so-bw-btn-pad-x); }
#so-booking-widget .so-bw-time-slot { padding: clamp(8px, calc(var(--so-bw-cw, 100vw) * 0.01), 11px) clamp(8px, calc(var(--so-bw-cw, 100vw) * 0.012), 14px); }
#so-booking-widget .so-bw-input,
#so-booking-widget .so-bw-textarea,
#so-booking-widget .so-bw-select {
    padding: var(--so-bw-input-pad-y) var(--so-bw-input-pad-x);
}
#so-booking-widget .so-bw-summary {
    padding: var(--so-bw-summary-pad-y) var(--so-bw-summary-pad-x);
}
/* Architecture overrides that must still outrank the block above.
   These vary the VERTICAL padding only — the horizontal inset stays on
   var(--so-bw-gutter) so the header cannot drift off the step content's left
   edge. An architecture that genuinely wants a different inset overrides the
   gutter token (see Inline below), which moves all three regions together. */
#so-booking-widget[data-template="tabbed"] .so-bw-header { padding-bottom: 0; }
#so-booking-widget[data-template="hero"] .so-bw-header { padding: 24px var(--so-bw-gutter) 52px; }
#so-booking-widget[data-template="tabbed"] .so-bw-rail,
#so-booking-widget[data-template="agenda"] .so-bw-rail { padding: 28px 24px; }

/* ── Calendar, per the design file ───────────────────────────────────────
   Every one of the twelve layout studies draws the month the same way: a
   borderless 7-column grid of ROUNDED CHIPS separated by a 4px gap, with
   mono weekday initials above it. Available days are a pale fill, the
   selected day is solid accent, unavailable days are bare — no chip at all,
   so the shape of a quiet month is legible at a glance.

   The base rules build a bordered TABLE instead: hairlines on every cell's
   right and bottom edge, `gap: 0`, `border-radius: 0`, the grid clipped by
   `overflow: hidden`. Until the consolidation each base layout re-skinned
   that itself; with those blocks retired the table showed through on all
   nineteen templates, which is the single biggest way the widget diverged
   from the file. Corrected here once, for every architecture. */
#so-booking-widget .so-bw-calendar {
    border: none;
    background: transparent;
    padding: 0;
}
#so-booking-widget .so-bw-calendar-weekdays {
    border: none;
    border-radius: 0;
    overflow: visible;
    gap: 4px;
    margin-bottom: 6px;
}
#so-booking-widget .so-bw-calendar-weekdays span {
    border: none;
    background: transparent;
    padding: 0 0 2px;
    font-family: var(--so-bw-mono);
    font-size: 10.5px;
    font-weight: 500;
    letter-spacing: 0.04em;
    color: var(--so-bw-ds-n500);
}
#so-booking-widget .so-bw-calendar-grid {
    border: none;
    border-radius: 0;
    overflow: visible;
    gap: 4px;
}
#so-booking-widget .so-bw-calendar-day {
    border: none;
    border-radius: var(--so-bw-calendar-day-radius);
    background: var(--so-bw-ds-n50);
    font-family: var(--so-bw-mono);
    font-variant-numeric: tabular-nums;
    font-size: 13px;
    font-weight: 400;
    aspect-ratio: auto;
    min-height: 40px;
}
#so-booking-widget .so-bw-calendar-day:not(.so-bw-calendar-day--disabled):hover {
    background: var(--so-bw-ds-n100);
}
/* Unavailable days carry no chip — the gaps ARE the information. */
#so-booking-widget .so-bw-calendar-day--disabled,
#so-booking-widget .so-bw-calendar-day--disabled:hover {
    background: transparent;
    color: var(--so-bw-ds-n300);
    cursor: default;
}
/* ── AN OPEN DAY HAS TO LOOK OPEN ────────────────────────────────────────
   REPORTED, repeatedly: "whenever I book, the date is not showing."

   The dates were there. Physiotherapy offers ten of them in August and the
   server returns all ten — verified against /slots/available, and the grid
   renders ten enabled buttons. What was missing was any sign that they WERE
   buttons.

   The rule above is the whole design in two lines: an open day carries a
   chip, a closed day carries none, and the gaps do the talking. It fails on
   the chip. `--so-bw-ds-n50` is #f4f5f7 — MEASURED at 1.09:1 against the
   white card it sits on, which is under the 3:1 floor for a non-text UI
   element by a factor of three and below the threshold where most people see
   an edge at all. So the customer met a month of pale numerals in which
   nothing looked pressable, read it as "no dates", and said so.

   And what little signal there was pointed the wrong way: grey is the
   universal disabled colour. The days you CAN book wore it; the days you
   cannot wore nothing.

   The chip becomes a tint of the template's own accent, its numeral takes the
   accent's ink at 600, and the ladder finally reads in one direction —
   available (10% tint) → hover (18%) → selected (solid). The design's idea is
   untouched: a closed day still carries no chip.

   Every template inherits it, because every template has this fault: the
   accent comes from --so-bw-primary, so Bundle's moss, Agenda's teal and
   Coral Pop's coral each get their own. The nine templates that state their
   own available-day treatment still win — theirs is (1,2,0) like this and
   comes later in the file.

   Placed BEFORE `--selected` and `--today` so both still outrank it: a
   selected day stays solid, today keeps its ring.

   A TINT ALONE WAS NOT ENOUGH, and the arithmetic says why: 10% of the accent
   over white lands at 1.15:1 — a fifteenth of a percent better than the grey
   it replaced. The FILL cannot carry this on a light template at any weight
   that still reads as a quiet month rather than a wall of colour. So the
   chip gets an EDGE, which buys contrast without buying area: a 1px accent
   ring at 45% measures ~2:1 against the card, and the numeral inside it is
   full-strength accent ink at 600 against a near-white fill. Between them the
   day is unmistakably a control.

   `:not(--selected)` keeps a chosen day solid: without it the ring would
   paint a lighter inner edge inside the filled chip. Scored (1,3,0), the same
   as the `--today` rule below, and stated first — so today's 1.5px ring still
   wins on source order and remains the one day that reads differently. */
#so-booking-widget .so-bw-calendar-day--available:not(.so-bw-calendar-day--selected) {
    background: color-mix(in srgb, var(--so-bw-primary) 12%, transparent);
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
    font-weight: 600;
    box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--so-bw-primary) 45%, transparent);
}
/* (1,3,0), matching the generic hover two rules up — at (1,2,0) this would be
   outranked by it and an open day would go GREY under the pointer, which is
   the same wrong signal in miniature. */
#so-booking-widget .so-bw-calendar-day--available:not(.so-bw-calendar-day--disabled):hover {
    background: color-mix(in srgb, var(--so-bw-primary) 18%, transparent);
}
#so-booking-widget .so-bw-calendar-day--selected,
#so-booking-widget .so-bw-calendar-day--selected:hover {
    background: var(--so-bw-primary);
    color: #fff;
    font-weight: 600;
}
#so-booking-widget .so-bw-calendar-day--today:not(.so-bw-calendar-day--selected) {
    box-shadow: inset 0 0 0 1.5px var(--so-bw-primary);
}

/* ── Back is a ghost, Continue is the commit ─────────────────────────────
   The base rule paints .so-bw-btn--secondary in the SAME solid accent as
   the primary button, so every footer offered two equally loud actions and
   the loudest one pointed backwards. In the design file the retreat is
   always a quiet outline and only the commit is filled. */
#so-booking-widget .so-bw-btn--secondary {
    background: transparent;
    color: var(--so-bw-ds-n700);
    border: 1px solid var(--so-bw-ds-n200);
}
#so-booking-widget .so-bw-btn--secondary:hover:not(:disabled) {
    background: var(--so-bw-ds-n25);
    border-color: var(--so-bw-ds-n300);
    color: var(--so-bw-ds-n900);
}
/* Dark-first templates need the ghost to read against ink, not paper. */
#so-booking-widget[data-template-id="dusk-champagne"] .so-bw-btn--secondary {
    color: #b9bccb;
    border-color: #2a2f3d;
}
#so-booking-widget[data-template-id="dusk-champagne"] .so-bw-btn--secondary:hover:not(:disabled) {
    background: #1a1d26;
    color: #e9e6df;
}

/* ── The month is a mini-month, never full-bleed ─────────────────────────
   No layout study draws the calendar wider than about 460px: past that the
   day chips stop reading as a month and become a spreadsheet. The base rule
   lets it fill whatever width it is given, which on the roomier
   architectures (Wide, Tabbed, Week Board) meant a 1,000px sprawl. Capped
   once here; Dual Month opts out below because two months legitimately need
   the room.

   ── ...but it hangs from the left, it does not float ──────────────────────
   The cap arrived with `margin-inline: auto`, which CENTRES the capped month
   inside whatever it is given. Every other block on the date step — the step
   description, the policies panel, the member strip, the slot list that
   replaces this very element once a day is picked — starts at the step's
   content edge. So on any architecture roomier than the cap, the month alone
   sat in the middle of the step with nothing above or below it to line up
   with, and picking a date made the panel jump sideways as the centred month
   was swapped for a full-width slot list.

   It was one rule producing the same defect on twelve of the nineteen, and
   the offsets prove it: every one measured exactly (content - 460) / 2 —
   Agenda 100 in 660, Hero 166 in 752, Daypart 170 in 800, Dusk 150 in 760,
   Dialog 191 in 702. The seven that looked fine only looked fine because
   their step is no wider than the cap, so `auto` had no room to act.

   `0`, not `auto`. The cap is the part that was wanted; the centring was the
   part that came along with it. Templates that genuinely want the month
   centred can still say so — Wide Canvas and Dual Month set their own
   placement below — but it is now an opt-in rather than the default nobody
   chose. */
#so-booking-widget .so-bw-calendar {
    max-width: 460px;
    margin-inline: 0;
}

/* ── K · Wide Canvas: month and times in ONE row ─────────────────────────
   The widget normally treats date and time as two PAGES of the same step —
   `.so-bw-calendar` is `x-show="!selectedDate"`, so choosing a day swaps the
   month out for the slot list. Every other architecture is happy with that.
   Wide Canvas is not: its whole proposition is that the month and the times
   sit side by side so the client never loses the month while scanning times.

   x-show writes `display: none` as an INLINE style, so re-showing the
   calendar needs `!important` — that is what this rule is for, and it is
   the only place in the file that overrides Alpine rather than CSS. */
/* RETIRED — the split now lives in ONE place, in the Wide Canvas · DATE &
   TIME block of section 16, and is driven by `usesSplitDatetime` in
   booking-widget.js rather than by fighting Alpine with !important.

   What was here was a second, older implementation of the same idea: a
   `5fr 4fr` datetime grid, a two-column slot list, a 980px collapse, and
   `display: block !important` on the calendar to defeat x-show. Section 16
   then declared its own datetime grid, slot list and 900px collapse — and
   being later, won some of those and not others. The result was a layout
   assembled from two disagreeing sources, which is why the columns did not
   line up: the calendar kept a `margin-inline` and a display mode from here
   while its widths came from there.

   Deleting rather than merging, because the JS getter makes the !important
   unnecessary and one authority is the whole point. */

/* ── Width: match the page, then let the architecture decide ─────────────
   Two separate constraints were fighting the design.

   1. The THEME. Block themes clamp every direct child of .entry-content to
      --wp--style--global--content-size (645px here) while the page's own
      heading and notice blocks sit at --wp--style--global--wide-size
      (1130px). The widget was rendering at half the width of the banner
      immediately above it, and the roomier architectures were crushed —
      Tabbed Rail wants 1040px and was clipping its summary rail mid-word.
      BookingShortcode now emits `class="alignwide"` on inline embeds; this
      is the fallback for themes that don't implement the class themselves.

   2. The SIZE control. Studio's "Widget Size" radio drives --so-bw-maxw,
      but every selector consuming it was written for the retired base
      layouts ([data-template="minimal"|"dark"|"studio"|…]), so after the
      consolidation the control silently did nothing on all nineteen
      templates. Re-pointed below.

      `medium` is deliberately absent: it is the historical default that
      every existing install carries, so honouring it as an 820px cap would
      quietly shrink every architecture — including the ones whose whole
      point is the room. Medium therefore means "use the architecture's own
      designed width", and small / large / full remain explicit choices. */
/* The inline widget is capped at 1100px and centred, rather than running to
   the theme's full wide-size (1340px here). min() keeps a narrower theme
   authoritative.

   This cap was originally chosen to match the identity banner that used to
   render directly above the widget, so the two lined up. That banner has since
   been removed, so the ORIGINAL REASON is gone — but the 1100px is now simply
   the width every existing install renders at, and lifting it would silently
   widen the widget on every page. It stays a deliberate width, not an
   accident; change it only on purpose. Note the Full Width interaction
   documented immediately below. */
#so-booking-widget.alignwide {
    max-width: min(var(--wp--style--global--wide-size, 1200px), 1100px);
    margin-inline: auto;
}
/* ...EXCEPT at "Full Width", which is the one size that has to get past it.
   The cap above lands on the ROOT; the size control below sets max-width on
   the CONTAINER INSIDE the root. They are different elements, so they never
   compete on specificity and the cap simply always won — "Full Width 100%"
   resolved to 100% OF 1100px. Measured on the live install at a 1920px
   viewport: data-size="full", --so-bw-maxw: 100%, computed max-width 1100px,
   rendered 1100px.

   Small (660) / Medium (820) / Large (1000) all sit under 1100 and so were
   never affected, which is why the control looked like it worked everywhere
   except the one place it visibly did not.

   100% here resolves against the theme's content box — which was only half the
   answer, because in a block theme that box is `.entry-content
   .has-global-padding` and its padding is INSIDE it. Filling the column
   therefore still left the theme's gutter (measured: 50px a side at 1920, so a
   widget set to 100% rendered 1772px). The root's alignment class now carries
   the rest of it: see the sprintf in BookingShortcode.

   The selector carries BOTH alignment classes because the root's class now
   follows the size: BookingShortcode emits `alignfull` at Full Width (that is
   what lets a block theme cancel its own global padding — see the note at that
   sprintf) and `alignwide` for the other three. Keeping `.alignwide` here as
   well is not dead weight: it is what an embed rendered by a cached/older
   plugin build still matches, so Full Width keeps clearing the 1100px cap
   there instead of silently regressing to it. */
#so-booking-widget.alignwide[data-size="full"] {
    max-width: 100%;
}

/* …and `none`, not `100%`, for the full-aligned root — the last 52px of this.
   `100%` resolves against the parent's CONTENT box (1820 inside 50px of global
   padding) while WordPress's `.has-global-padding > .alignfull` gives the
   element margin-inline:-50px to span the padding. A cap of 1820 with 1920 of
   space to fill does not centre — the negative left margin still pulls the box
   left, so it renders 1868 wide sitting at l=24 / r=76, off-centre by the very
   gutter this was meant to remove (measured). `none` hands the width back to
   the alignfull rule, which is the one thing on the page that knows what edge
   to edge means here.

   Still safe without a block theme: with no `.alignfull` styles the root is a
   plain block, so `max-width:none` resolves to its parent's width exactly as
   `100%` did. It can never be WIDER than the column, so there is no page
   overflow to trade for this. */
#so-booking-widget.alignfull[data-size="full"] {
    max-width: none;
}

/* ── Wide Canvas caps Full Width at 90vw ─────────────────────────────────
   `max-width: none` above hands the width to WordPress's own `.alignfull`
   rule, which spans the theme's global padding to reach the true page edge.
   That is right for the other architectures: Full Width means edge to edge,
   and that is what the admin asked for by choosing it.

   Wide Canvas is capped instead. It is a two-column booking surface, and at
   1920px edge to edge the step and its summary end up ~900px apart with the
   customer's eye crossing the whole monitor between the thing they picked and
   the panel confirming it. 90vw keeps a page margin on either side so the
   widget still reads as embedded rather than as the page itself, while being
   clearly wider than the 920px default — so Full Width remains a meaningful
   choice rather than a no-op. It was briefly 80vw; 90vw is the same idea with
   a thinner margin, and the margin is the only thing this number controls.

   `margin-inline: auto` is load-bearing here, not decoration. The alignfull
   rule gives the root a NEGATIVE inline margin to swallow the theme's global
   padding; capping the width without resetting that margin leaves the box
   pulled left by the padding it no longer needs to cover, so it renders
   off-centre by exactly that gutter — the same failure the note above records
   for `100%`. `auto` overrides the negative value and re-centres what is left.

   Scoped to this template: the other twenty-one keep true edge-to-edge. */
#so-booking-widget.alignfull[data-size="full"][data-template="wide"] {
    /* `none`, NOT `100vw`. Full Width means the whole available width, and `none`
       is how the other twenty-one architectures get it — handing the width to the
       parent, i.e. to WordPress's own `.alignfull` on a themed page and to the
       body on the embed route.
       `100vw` was tried and is a trap: per spec it INCLUDES the scrollbar gutter,
       so the moment the page has a vertical scrollbar the card is ~15px wider
       than the space it has to sit in — horizontal overflow, and the card's width
       visibly changing whenever the bar appears or disappears. Reported from the
       field at a 1496x312 viewport, where the card's own height floor was forcing
       that bar (see the height rule, whose floor came down as the other half of
       this fix). Not reproducible in headless Chromium, which uses overlay
       scrollbars that occupy no space and so never expose the mismatch.
       This is also why the earlier 80vw and 90vw caps were tolerable: they were
       small enough that the 15px never showed as overflow. */
    max-width: none;
    margin-inline: auto;
    /* No page spacing under a full-screen widget. The base rule gives every
       widget `margin-bottom: 40px` so a block stacked below it does not sit
       flush - correct for a card IN a page, meaningless for one that IS the page,
       and here actively harmful: it made the document 40px taller than the
       viewport (measured docScrollHeight 346 against vh 312), which is a page
       scrollbar, and the scrollbar is what made the width move. */
    margin-block: 0;
}

/* The container never centred: `.so-bw-container { margin: 0 auto }` scores
   (0,1,0) and the `#so-booking-widget * { margin: 0 }` reset scores (1,0,0),
   so every architecture sat hard against the left edge of the widget root.
   Same trap as the padding restored earlier in this file — the retired base
   layouts used to re-state it at ID strength and hid the problem. */
#so-booking-widget .so-bw-container {
    margin-inline: auto;
}

/* SUPERSEDED BY SECTION 23 — kept only as the no-template-id fallback.
   These three replace the template's width with an absolute one and have no
   `medium` companion, which is what made the control run backwards on six
   templates (Small rendered Inline 280px WIDER than Medium). Section 23
   matches on both template attributes for (1,3,0) and outranks them wherever
   the widget tag is complete, which is every path BookingShortcode renders.
   Left in place so a tag emitted without data-template-id still gets a size
   response rather than none at all. Do not extend these; add to section 23. */
#so-booking-widget[data-size="small"] .so-bw-container { max-width: min(660px, 100%); }
#so-booking-widget[data-size="large"] .so-bw-container { max-width: min(1000px, 100%); }
#so-booking-widget[data-size="full"]  .so-bw-container { max-width: 100%; }

/* A rail that runs out of room must wrap, never clip. The summary values
   are user data (service names, staff names) and can be arbitrarily long. */
#so-booking-widget .so-bw-rail-title,
#so-booking-widget .so-bw-rail-sub,
#so-booking-widget .so-bw-rail-v {
    overflow-wrap: anywhere;
}

/* ── The widget is a CARD, not a column that grows down the page ─────────
   SUPERSEDED by the RESPONSIVE AUTO-HEIGHT section near the end of this file.
   The cap below produced a second scrollbar inside the page's own on
   content-heavy steps (Review in particular); the card now grows with its
   step and keeps only a min-height. This block is left in place because its
   flex plumbing (the container column, the chrome's flex-shrink: 0, the
   step-host chain) is still what makes the layout work — only the heights
   and the body's overflow are overridden later.

   Original rationale, for context:
   Every layout study in the design file is a bounded card: header pinned,
   commit bar pinned, and the step content scrolling between them. The
   widget instead let each step push the page as tall as its content, so a
   long service list or a full month ran on for hundreds of pixels and the
   Continue button drifted below the fold — the opposite of a template.

   Bounded here, once, for every architecture:
     header   — fixed
     body     — the only scrolling region
     nav      — fixed, so the commit is always reachable

   `display:flex` is deliberately at (1,1,0): the four grid architectures
   (Split, Accordion, Tabbed, Agenda) declare `display:grid` at (1,2,0) and
   keep it, and their `grid-template-rows: auto 1fr auto` already gives the
   body the same flexible middle row. min-height:0 is what actually lets a
   flex/grid child shrink below its content and scroll at all. */
#so-booking-widget .so-bw-container {
    display: flex;
    flex-direction: column;
    max-height: min(760px, 88vh);
}
#so-booking-widget .so-bw-body {
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
}

/* Thin, quiet scrollbar — the same 5px grey used by the admin's scroll
   panels, so a scrolling widget never grows a heavy native bar.

   SUPERSEDED by the tokenised block further down (search: --so-bw-sb-thumb),
   which recolours the bar from the template's own foreground instead of these
   two literals and states so in its own note. The standard properties are
   removed rather than left as a dead pair: they were the LAST unguarded
   `scrollbar-width` on this element, and one of those anywhere in the cascade
   is enough to switch Chromium off the -webkit- pseudo-elements for the whole
   element — so this block, which nothing else in it still wins, was quietly
   deciding how the bar painted for most users. The -webkit- rules below are
   superseded by the later block's in the ordinary way and are harmless. */
#so-booking-widget .so-bw-body::-webkit-scrollbar { width: 5px; }
#so-booking-widget .so-bw-body::-webkit-scrollbar-track { background: transparent; }
#so-booking-widget .so-bw-body::-webkit-scrollbar-thumb {
    background: #c1c7d0;
    border-radius: 999px;
}
#so-booking-widget .so-bw-body::-webkit-scrollbar-thumb:hover { background: #a7aebb; }

/* Drop the sticky-nav overlap guard. It reserved clamp(72px,calc(var(--so-bw-cw, 100vw) * 0.1),96px) at
   the foot of EVERY step so a `position: sticky; bottom: 0` nav couldn't
   cover the last row — but a sticky element still occupies its normal flow
   space, so nothing was ever covered, and now the nav is a pinned flex item
   outside the scroll area entirely. The guard was pure dead space: it made
   each step ~200px taller than its content, which is most of why the widget
   read as a long page rather than a card. `!important` only because the
   rule it replaces uses it. */
#so-booking-widget[data-template] .so-bw-step,
#so-booking-widget .so-bw-step {
    padding-bottom: var(--so-bw-step-pad) !important;
}

/* Hero and Deck paint outside their own box (the card lifts off the hero
   image; the deck shows two cards peeking above). Clipping them to the
   card's max-height would cut those off, so they size to content. */
#so-booking-widget[data-template="hero"] .so-bw-container,
#so-booking-widget[data-template="deck"] .so-bw-container {
    max-height: none;
}

/* ── Only the LIST scrolls, never the step around it ─────────────────────
   Bounding the card (above) made .so-bw-body the scroll region, which was
   too coarse: the step heading and the per-step search bar are inside the
   body, so scrolling a long service list carried the search field off the
   top — you had to scroll back up to type. The same applied to staff,
   locations, times and the month.

   So the shell is fixed and the list inside it scrolls, with the bar
   directly beside the cards:

     .so-bw-body   fixed   (no scrolling of its own)
     .so-bw-step   column  (heading + search pinned, list takes the rest)
     the list      scrolls

   min-height:0 on both the step and the list is what actually allows a
   flex child to shrink below its content; without it the list reports its
   full height, the step grows, and nothing scrolls at all. */
/* The body remains the scroll region.
   The intent here was finer: pin each step's heading and search bar and
   scroll ONLY the list beside them. Three approaches failed — the flex
   chain from .so-bw-container down to .so-bw-card-grid is broken by a
   block-level wrapper Alpine renders inside .so-bw-body that keeps
   min-height:auto and refuses to shrink, so the list always reports its
   full height and never becomes a scroll container.

   Leaving `overflow: hidden` here would have been far worse than the
   original problem: the card is bounded, so anything past the fold was
   CLIPPED AND UNREACHABLE — a customer could not scroll to the services
   below the first few. Reverted to a scrolling body until the wrapper is
   properly addressed in the markup. */
#so-booking-widget .so-bw-body {
    overflow-y: auto;
    overflow-x: hidden;
    flex: 1 1 auto;
    min-height: 0;
}
#so-booking-widget .so-bw-step {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-height: 0;
}

/* The scrolling regions, one per step. The card grid covers Service, Staff,
   Location and Extras; the slot list covers Time; the date/time wrapper
   covers the month. max-height is deliberately absent — they fill whatever
   the bounded card leaves after the pinned heading, search and nav. */
#so-booking-widget .so-bw-card-grid,
#so-booking-widget .so-bw-slot-list,
#so-booking-widget .so-bw-datetime {
    /* Neutralised: see the .so-bw-body note above. These were the intended
       per-list scroll regions; with the flex chain broken they only produced
       nested scrollers that clipped. */
    min-height: 0;
}
#so-booking-widget .so-bw-card-grid::-webkit-scrollbar,
#so-booking-widget .so-bw-slot-list::-webkit-scrollbar,
#so-booking-widget .so-bw-datetime::-webkit-scrollbar { width: 6px; }
#so-booking-widget .so-bw-card-grid::-webkit-scrollbar-track,
#so-booking-widget .so-bw-slot-list::-webkit-scrollbar-track,
#so-booking-widget .so-bw-datetime::-webkit-scrollbar-track { background: transparent; }
#so-booking-widget .so-bw-card-grid::-webkit-scrollbar-thumb,
#so-booking-widget .so-bw-slot-list::-webkit-scrollbar-thumb,
#so-booking-widget .so-bw-datetime::-webkit-scrollbar-thumb {
    background: #c1c7d0;
    border-radius: 999px;
}
#so-booking-widget .so-bw-card-grid::-webkit-scrollbar-thumb:hover,
#so-booking-widget .so-bw-slot-list::-webkit-scrollbar-thumb:hover,
#so-booking-widget .so-bw-datetime::-webkit-scrollbar-thumb:hover { background: #a7aebb; }

/* The old 60vh cap fought the bounded card: the list would stop at 60vh
   while the card had more room, leaving a gap above the nav. The flex rules
   above size it exactly. */
#so-booking-widget .so-bw-card-grid--scroll,
#so-booking-widget .so-bw-scroll-wrap--on { max-height: none; }

/* Wide Canvas puts month and times side by side, so the WRAPPER must not
   scroll as one — each column scrolls independently. */
#so-booking-widget[data-template="wide"] .so-bw-datetime {
    overflow: visible;
    padding-right: 0;
}
/* Hero and Deck size to content (they paint outside their box), so nothing
   inside them needs to scroll either. */
#so-booking-widget[data-template="hero"] .so-bw-card-grid,
#so-booking-widget[data-template="hero"] .so-bw-slot-list,
#so-booking-widget[data-template="hero"] .so-bw-datetime,
#so-booking-widget[data-template="deck"] .so-bw-card-grid,
#so-booking-widget[data-template="deck"] .so-bw-slot-list,
#so-booking-widget[data-template="deck"] .so-bw-datetime {
    overflow: visible;
    min-height: auto;
}

/* The flex item between .so-bw-body and .so-bw-step. It must grow to fill
   the bounded card AND be allowed to shrink, or the step inside it keeps
   its full content height and the list never scrolls. Alpine's x-show owns
   this element's `display`, so the value is set here rather than inline. */
#so-booking-widget .so-bw-step-host {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-height: 0;
}

/* Alpine also leaves an UNCLASSED block div directly inside .so-bw-body
   wrapping the step flow. It is not the display:contents host above — it
   carries no style attribute at all — and with min-height:auto it refuses
   to shrink, which stops the flex chain and leaves the list unscrollable.
   Matched structurally because there is no hook on it. */
#so-booking-widget .so-bw-body > div:not([class]) {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-height: 0;
}

/* ── Only the body flexes; the chrome keeps its size ─────────────────────
   Making the container a flex column turned every direct child into a flex
   item, and flex items shrink by default. With the card bounded, the fixed
   chrome — hold timer, membership banner, error bar, header, nav — was
   being squeezed to make room for the body. The hold countdown lost its
   height entirely and vanished from the page, which is a functional loss:
   it is the only thing telling a customer their slot is reserved and for
   how long.

   flex-shrink:0 on every child, and only .so-bw-body is allowed to grow
   and shrink. */
#so-booking-widget .so-bw-container > * {
    flex-shrink: 0;
}
#so-booking-widget .so-bw-container > .so-bw-body {
    flex: 1 1 auto;
    min-height: 0;
}

/* ── Make the scrollbar findable ─────────────────────────────────────────
   The bar was there but effectively invisible: `scrollbar-width: thin` with
   a transparent track renders as a faint overlay on Windows that only
   surfaces mid-gesture, so the list looked truncated rather than
   scrollable. A booking widget must not hide the fact that there are more
   services below the fold.

   `scrollbar-gutter: stable` also reserves the track permanently, so the
   cards no longer shift sideways the moment the list becomes scrollable.

   ── The 9px it costs, and why `both-edges` is NOT the answer ─────────────
   The reserved track comes out of the body's content box, so the step's right
   inset is the gutter PLUS the track while the header's and the nav's is just
   the gutter. MEASURED at a 1040px container, insets from the container edge:

       value                  header      step       nav
       stable (this)          25 / 345    25 / 354   25 / 345
       stable both-edges      25 / 345    33 / 354   25 / 345
       auto                   25 / 345    25 / 346   25 / 345

   `both-edges` looks like the fix and is worse. The header and the nav are
   SIBLINGS of `.so-bw-body`, not children, so a gutter reserved inside the
   body moves the step and nothing else: both-edges pushes the step's LEFT
   edge to 33 while the header and nav stay at 25. That trades a discrepancy
   on the right for one on the left — on the strong line that all nine steps,
   the header and the nav share, and the one a customer actually reads down.
   `auto` gives back the sideways shift this rule exists to prevent.

   So `stable` stays here, on the shared rule, and the 9px lands on the step's
   right edge. Its visible consequence, recorded so it is a known rather than a
   mystery: the nav's right-aligned Continue button sits ~8px outside the card
   grid's right edge.

   An architecture that cares can cancel it, and one does — see Wide Canvas's
   `--so-bw-sbw`, which reads the width THIS FILE gives the bar (5px at base,
   8px from the 768 tier) and gives it back with
   `padding-right: calc(var(--so-bw-gutter) - var(--so-bw-sbw))`. The reserved
   width is not knowable in general, but it is knowable here, because the
   widget styles its own scrollbar. Any layout adopting that should read the
   token rather than restate the number. */
/* ── …but tinted by the TEMPLATE, not hardcoded ──────────────────────────
   The colours above were literals (#b6bcc8 on #f1f2f5). That is a pale grey
   bar: correct against the light templates it was written for, but on the
   dark-first ones — Dusk, dark-mode, terminal, glass — it lit a bright strip
   down a near-black surface, the single most obvious "this doesn't belong to
   the design" detail left in the widget. Being the last .so-bw-body rule in
   the file, it also silently beat the per-template dark overrides further up
   (search: rgba(255, 255, 255, 0.22)), so those never took effect.

   Deriving both colours from --so-bw-text instead makes the bar the same hue
   as the template's own foreground at low opacity. It therefore adapts to any
   palette — light, dark, or a custom one an admin picks in the Studio — with
   no per-template rules to keep in sync, which is what let the old ones drift
   out of sync in the first place.

   Each token is declared twice: a plain rgba first, then the color-mix. A
   browser without color-mix support keeps the fallback rather than dropping
   the colour entirely. */
#so-booking-widget .so-bw-container {
    --so-bw-sb-track: rgba(128, 134, 150, 0.10);
    --so-bw-sb-track: color-mix(in srgb, var(--so-bw-text, #1f2937) 8%, transparent);
    --so-bw-sb-thumb: rgba(128, 134, 150, 0.42);
    --so-bw-sb-thumb: color-mix(in srgb, var(--so-bw-text, #1f2937) 30%, transparent);
    --so-bw-sb-thumb-hover: rgba(128, 134, 150, 0.62);
    --so-bw-sb-thumb-hover: color-mix(in srgb, var(--so-bw-text, #1f2937) 50%, transparent);
}
#so-booking-widget .so-bw-body {
    /* Reserve the track so cards don't shift sideways the moment the list
       becomes scrollable. */
    scrollbar-gutter: stable;
    /* Momentum + a settled stop, so the panel reads like a phone surface
       rather than a desktop pane, and a flick inside it never scrolls the
       host page out from under the widget. */
    scroll-behavior: smooth;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior: contain;
}
/* ── Why the standard properties are behind an @supports ──────────────────
   `scrollbar-width` and `scrollbar-color` were declared on .so-bw-body
   directly, right above a `::-webkit-scrollbar` block that styles the same
   bar. Only one of the two was ever taking effect: Chromium DISABLES its
   -webkit- scrollbar pseudo-elements outright the moment either standard
   property is set to a non-initial value. So every Chromium user — most of
   them — got the platform's plain "thin" bar in the token colours, and the
   8px width, the pill radius and the hover state below were dead code that
   read as if it were live.

   The two syntaxes can't both apply, so pick per engine. Firefox does not
   support `selector()` with a -webkit- pseudo-element, so the query matches
   there and Firefox keeps exactly the bar it has today. Chromium and Safari
   do support it, skip the block, and the pseudo-elements finally run.

   Nothing about scrolling changes in either engine — only which of the two
   already-written rule sets paints the bar. */
@supports not selector(::-webkit-scrollbar) {
    #so-booking-widget .so-bw-body {
        scrollbar-width: thin;
        scrollbar-color: var(--so-bw-sb-thumb) var(--so-bw-sb-track);
    }
}
#so-booking-widget .so-bw-body::-webkit-scrollbar { width: 8px; }
#so-booking-widget .so-bw-body::-webkit-scrollbar-track {
    background: var(--so-bw-sb-track);
    border-radius: 999px;
}
#so-booking-widget .so-bw-body::-webkit-scrollbar-thumb {
    background: var(--so-bw-sb-thumb);
    border-radius: 999px;
    transition: background 160ms ease;
}
#so-booking-widget .so-bw-body::-webkit-scrollbar-thumb:hover {
    background: var(--so-bw-sb-thumb-hover);
}

/* ==========================================================================
   SECTION 12b — PER-TEMPLATE MOBILE OVERRIDES
   ==========================================================================
   Templates added after the last mobile pass (hero, dialog, chat, deck,
   centred) never got phone rules, so at a real 390px handset they still
   rendered their desktop geometry.

   Two recurring causes, both worth avoiding in new templates:

   1. They hardcode `padding` / `margin` on .so-bw-header, .so-bw-body or the
      shell instead of setting the --so-bw-*-pad-* tokens. A raw value at
      `#id[attr] .class` (specificity 1,2,0) outranks the base mobile rules,
      so the desktop geometry survived — up to 32px a side, a sixth of the
      screen gone before any content. Tokenised templates shrink for free.
   2. Desktop-only chrome (deck's stacked-card pseudo-elements, hero's 180px
      image well) has no mobile escape hatch and turns into wasted space or
      visual noise once the canvas is narrow.

   Overrides live HERE, at the end of the file, so equal specificity resolves
   by source order — the same convention as the rest of section 12b.
   ========================================================================== */
/* was @media (max-width: 480px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    /* ── UNIVERSAL FLOOR (applies to EVERY template) ─────────────────────
       A backstop, not a layout. Templates are free to style themselves, but
       none of them may exceed the screen: a single over-wide child gives the
       whole widget a horizontal scrollbar, and on a phone that reads as the
       template being broken even when the rest of it is fine.

       `overflow-x: clip` rather than `hidden` deliberately — `hidden` on an
       ancestor creates a scroll container, which silently kills any
       `position: sticky` descendant. `clip` crops without that side effect.

       This is a floor for templates NOT individually handled below (and for
       any added later that forget mobile entirely); it is not a substitute
       for a template collapsing its own multi-column layout. */
    #so-booking-widget:where([data-cw~="480"]){
        overflow-x: clip;
    }
    /* `max-width: 100%` was the whole declaration, and being !important it was
       the cascade winner on EVERY narrow container — including every floating
       panel, where it discarded the size tokens outright: floating/small/Inline
       rendered 400px against the 304px it asked for, i.e. WIDER than requested,
       which no setting could undo. Keeping the size maths inside `min(100%, …)`
       preserves the promise this rule exists for — nothing may exceed the
       screen — while letting a size still mean something at phone widths.

       `:not([data-size="full"])` is required: without it this (1,3,0) rule also
       outranks section 23's dedicated `full` rule at (1,4,0)... it does not, but
       it DOES win against the shared size rule that `full` relies on, and full
       stopped being edge-to-edge (measured 400 -> 380). Excluding it here keeps
       `full` meaning full. */
    #so-booking-widget:where([data-cw~="480"]):not([data-size="full"]) .so-bw-container{
        max-width: min(100%, calc(var(--so-bw-tpl-maxw, 100%) * var(--so-bw-size-scale, 1))) !important;
    }
    #so-booking-widget:where([data-cw~="480"])[data-size="full"] .so-bw-container{
        max-width: 100% !important;
    }
    /* Media is the usual culprit for a few stray pixels of overflow. */
    #so-booking-widget:where([data-cw~="480"]) img,
    #so-booking-widget:where([data-cw~="480"]) svg,
    #so-booking-widget:where([data-cw~="480"]) video{
        max-width: 100%;
    }

    /* ── NO VISIBLE SCROLLBARS ON A PHONE ────────────────────────────────
       Real handsets use overlay bars: they fade in during the gesture and
       take no layout width. A desktop-style bar — worse, a chunky one with
       arrow buttons — both looks wrong on a phone and permanently steals
       width from content that has 390px to work with.

       Applied to the whole subtree because the widget has many scroll
       regions (body, card grid, slot panel, calendar, stepbar) and each was
       styled separately over time; a blanket rule here cannot drift out of
       sync the way per-region rules did.

       Scrolling is NOT affected — wheel, trackpad, touch and keyboard all
       work exactly as before. Only the painting of the bar is suppressed. */
    #so-booking-widget:where([data-cw~="480"]),
    #so-booking-widget:where([data-cw~="480"]) *{
        scrollbar-width: none; /* Firefox */
        -ms-overflow-style: none; /* legacy Edge */
    }
    #so-booking-widget:where([data-cw~="480"])::-webkit-scrollbar,
    #so-booking-widget:where([data-cw~="480"]) *::-webkit-scrollbar{
        display: none; /* WebKit / Blink */
        width: 0;
        height: 0;
    }

    /* CENTRED — the column COUNT is no longer set here. The diagnosis was right
       ("four slot columns leave roughly 80px per chip … which truncates times
       like 10:30 AM") and the remedy was a second guess at the number: two.
       Section 17.1 now sizes the track from the label's measured width, so the
       count falls out of the available space at every tier and this tier does
       not have to know it. The max-width release stays. */
    #so-booking-widget:where([data-cw~="480"])[data-template="centred"] .so-bw-slot-list{
        max-width: 100% !important;
    }

    /* HERO (immersive-rose, glass-frost) — three desktop assumptions stack up
       badly on a phone: a 180px minimum hero, 32px side padding with a 52px
       overlap well, and a body card inset 24px a side while the nav beneath
       stays full-width, so the card and footer edges visibly fail to line up.
       Shrink the hero, keep the overlap (the layout is built on it), and pull
       the card inset in so the two edges agree. */
    #so-booking-widget:where([data-cw~="480"])[data-template="hero"] .so-bw-header{
        min-height: 120px !important;
        padding: 16px var(--so-bw-gutter) 30px !important;
    }
    #so-booking-widget:where([data-cw~="480"])[data-template="hero"] .so-bw-step-label{
        font-size: 19px !important;
    }
    #so-booking-widget:where([data-cw~="480"])[data-template="hero"] .so-bw-body{
        margin: -20px 6px 0 !important;
    }
    /* The lift is 20px here, so the banners' give-back has to be 20 too — at
       28 they would push the card down by the 8px difference. */
    #so-booking-widget:where([data-cw~="480"])[data-template="hero"] .so-bw-hold-timer,
    #so-booking-widget:where([data-cw~="480"])[data-template="hero"] .so-bw-error,
    #so-booking-widget:where([data-cw~="480"])[data-template="hero"] .so-bw-pay-retry{
        margin-bottom: 20px;
    }

    /* DIALOG / CHAT — plain oversized header padding, no structural reason. */

    /* DIALOG — the shell inset sells the "floating over the page" look. On a
       phone there is no page visible around it, so it is only lost width. */

    /* DECK (pocket-violet, coral-pop) — the stacked-card illusion is two
       pseudo-elements offset above the container; at this width they read as
       stray lines rather than depth, and the negative top offset can clip at
       the viewport edge. Drop them, then reclaim both the shell padding and
       the margin-top that only existed to make room for them. */
    #so-booking-widget:where([data-cw~="480"])[data-template="deck"]{
        padding: 16px 6px 24px !important;
    }
    #so-booking-widget:where([data-cw~="480"])[data-template="deck"] .so-bw-container::before,
    #so-booking-widget:where([data-cw~="480"])[data-template="deck"] .so-bw-container::after{
        display: none !important;
    }
    #so-booking-widget:where([data-cw~="480"])[data-template="deck"] .so-bw-container{
        margin-top: 0 !important;
    }

    /* ── IN-PANEL SCROLLING (deck, hero, frost, soft) ────────────────────
       The single-scroll-region setup — bounded container, flex column, body
       takes the overflow — lives in `@media (min-width: 768px)` and names
       only the seven original layouts. So on a phone these four scroll the
       whole PAGE instead: the header and the Continue button slide away with
       the content, and a long service list means scrolling back up to move on.

       Bound the root to the viewport and let the body take the overflow, so
       the header stays put, the footer stays reachable, and only the content
       between them moves — which is how a booking app behaves on a phone.

       The bar itself is hidden: scrolling is by wheel and touch only, matching
       the phone frame around it. `overscroll-behavior: contain` stops a flick
       that reaches the end from scrolling the host page underneath, and
       `touch-action: pan-y` keeps vertical drags scrolling rather than being
       swallowed as gestures. */
    #so-booking-widget:where([data-cw~="480"])[data-template="deck"],
    #so-booking-widget:where([data-cw~="480"])[data-template="hero"],
    #so-booking-widget:where([data-cw~="480"])[data-template="frost"],
    #so-booking-widget:where([data-cw~="480"])[data-template="soft"]{
        display: flex;
        flex-direction: column;
        max-height: 100vh;
        max-height: 100dvh;
    }
    #so-booking-widget:where([data-cw~="480"])[data-template="deck"] .so-bw-container,
    #so-booking-widget:where([data-cw~="480"])[data-template="hero"] .so-bw-container,
    #so-booking-widget:where([data-cw~="480"])[data-template="frost"] .so-bw-container,
    #so-booking-widget:where([data-cw~="480"])[data-template="soft"] .so-bw-container{
        display: flex;
        flex-direction: column;
        flex: 1 1 auto;
        min-height: 0;
    }
    #so-booking-widget:where([data-cw~="480"])[data-template="deck"] .so-bw-body,
    #so-booking-widget:where([data-cw~="480"])[data-template="hero"] .so-bw-body,
    #so-booking-widget:where([data-cw~="480"])[data-template="frost"] .so-bw-body,
    #so-booking-widget:where([data-cw~="480"])[data-template="soft"] .so-bw-body{
        flex: 1 1 auto;
        min-height: 0;
        overflow-y: auto;
        overflow-x: hidden;
        overscroll-behavior: contain;
        -webkit-overflow-scrolling: touch;
        touch-action: pan-y;
        scrollbar-width: none; /* Firefox */
        -ms-overflow-style: none; /* legacy Edge */
    }
    #so-booking-widget:where([data-cw~="480"])[data-template="deck"] .so-bw-body::-webkit-scrollbar,
    #so-booking-widget:where([data-cw~="480"])[data-template="hero"] .so-bw-body::-webkit-scrollbar,
    #so-booking-widget:where([data-cw~="480"])[data-template="frost"] .so-bw-body::-webkit-scrollbar,
    #so-booking-widget:where([data-cw~="480"])[data-template="soft"] .so-bw-body::-webkit-scrollbar{
        display: none; /* WebKit / Blink — wheel and touch still scroll */
    }


/* ── HALF-CONVERTED SUMMARY RAIL ─────────────────────────────────────────
   The compact-rail rule further up (search: "rail becomes a compact summary
   on top") turns a side panel into a horizontal summary strip on narrow
   screens — but it is scoped to minimal / dark / studio, the only rail
   layouts that existed when it was written. Its CHILD rules
   (.so-bw-rail-head / -list / -row / -total) are NOT scoped and apply to
   every rail.

   So the rail layouts added later — split, agenda, tabbed — end
   up half converted on a phone: the contents flatten into a horizontal strip
   while the container keeps flex-direction: column and its 24px side-panel
   padding. A compact summary rendered inside a full-height panel is why Dusk
   (a split template) reads as a different design on mobile than the one
   picked in the gallery, even though its layout and colours resolve
   correctly. Finish the conversion for those layouts.

   Same 680px breakpoint as the original rule so the two switch together —
   at 480px the rail would already have spent 200px of screen looking wrong.
   ────────────────────────────────────────────────────────────────────── */
/* was @media (max-width:680px) — now gated on the CONTAINER width
   via data-cw (see booking-widget.js _wireContainerWidth). :where()
   keeps specificity identical to the media query it replaces. */

    #so-booking-widget:where([data-cw~="680"])[data-template="agenda"] .so-bw-rail,
    #so-booking-widget:where([data-cw~="680"])[data-template="tabbed"] .so-bw-rail{
        position: static;
        width: auto;
        flex-direction: row;
        flex-wrap: wrap;
        align-items: center;
        gap: 6px 16px;
        border-radius: var(--so-bw-r-12);
        padding: 14px 16px;
    }
    /* The rail stacks ABOVE the flow in split/agenda and BELOW it in
       tabbed, so its breathing room goes on opposite sides. */
    #so-booking-widget:where([data-cw~="680"])[data-template="agenda"] .so-bw-rail{
        margin: 0 0 14px;
    }
    #so-booking-widget:where([data-cw~="680"])[data-template="tabbed"] .so-bw-rail{
        margin: 14px 0 0;
    }

    /* ── AGENDA'S AXIS IS A COLUMN, AND THIS TIER IS A ROW ────────────────
       The block above turns the rail into a wrapping horizontal strip. Agenda
       draws its rail as a NODE LIST (section G): a 24px gutter on the list,
       a 13px dot at `left: -24px` on every row, and a 2px connector running
       from one dot to the next. All three are stated in the row's own
       coordinates, which is exactly right while the rows are stacked — the
       dots hang in the gutter and the connector joins them down the column.

       Turned sideways, `left: -24px` means "24px left of THIS row", and every
       row is now at a different x. So each dot was painted on top of the item
       BEFORE it — measured at 420px: the SPECIALIST node sitting across the
       end of "Physiotherapy" — and the connectors, still 2px wide and ~22px
       tall, hung between the chips as loose vertical ticks joining nothing.
       That is the "overlapping / not responsive" half of the report.

       The state the axis carries is worth keeping, so the node stays and
       becomes an inline marker instead: `position: static` makes the ::before
       a flex item of the row it belongs to, so it sits before its own label at
       any x, and the done/current colour rules keep working untouched. The
       connector has no horizontal meaning and goes.

       Specificity ties the base rules at (1,2,0) — a pseudo-element adds
       nothing — and wins on source order, which is why these live here rather
       than beside the axis they amend.

       The tightening on the last two is not cosmetic: this rail is capped at
       `min(160px, 22vh)` (section BOUNDED SHELL rule 4) so it cannot starve
       the step below it, and the flattened content ran ~174px — enough to cut
       "TOTAL / Free" in half at the cap. Losing the gutter, the connector and
       the subtitle brings it back inside without touching the cap. */
    #so-booking-widget:where([data-cw~="680"])[data-template="agenda"] .so-bw-rail-list{
        padding-left: 0;
    }
    #so-booking-widget:where([data-cw~="680"])[data-template="agenda"] .so-bw-rail-row::before{
        position: static;
        align-self: center;
        top: auto;
        left: auto;
        width: 9px;
        height: 9px;
        flex: 0 0 auto;
    }
    #so-booking-widget:where([data-cw~="680"])[data-template="agenda"] .so-bw-rail-row:not(:last-child)::after{
        display: none;
    }
    #so-booking-widget:where([data-cw~="680"])[data-template="agenda"] .so-bw-rail-row{
        padding: 5px 0;
        gap: 8px;
    }
    #so-booking-widget:where([data-cw~="680"])[data-template="agenda"] .so-bw-rail-sub{
        display: none;
    }


/* ==========================================================================
   CONTENT-ADAPTIVE GUARD — text is never clipped, on any of the 19 templates
   ==========================================================================

   Every rule that used to clip text has been corrected at its own source (the
   card meta's line-clamp, the add-on name's ellipsis, the button and stepbar
   nowrap, the daypart and timeline group titles, the timezone combobox, the
   Studio block's three truncations). This section is the BACKSTOP for the
   same class of bug: a per-template block written later, at (1,1,0) with
   !important, that reintroduces a fixed height or a truncation on a text
   node.

   Why it exists as a section rather than as more per-template rules: the file
   carries fifteen architectures plus four propositions on top of them, and
   the clipping properties are not layout — no template's identity depends on
   cutting a service name in half. So they are settled once, here, for all
   nineteen, exactly as the calendar and the bounded card are.

   Specificity: `#so-booking-widget[data-template] .x` is (1,2,0), which beats
   every `#so-booking-widget .x` per-template rule in this file (1,1,0),
   !important included. The `[data-template]` presence selector matches all
   nineteen layouts and nothing else, so the plain-widget case (no Studio save,
   no attribute) is covered by the duplicate (1,1,0) selector in each block.
   The !important is only there to beat the per-template !important it exists
   to correct.

   TWO DELIBERATE EXCEPTIONS, neither of which this section may touch:
     .so-bw-sp__sr        — the visually-hidden rating text. Its 1px box,
                            overflow: hidden and nowrap ARE the sr-only
                            pattern; wrapping it would paint it on screen.
     .so-bw-recurring-day — a weekday chip. nowrap is what makes it WIDEN for
                            a three-letter abbreviation instead of stacking
                            one letter per line.
   Both are named so a future sweep does not "fix" them.

   Not touched either, for the same reason they are not clipping:
     - Scroll regions (the tz list, the agenda list, .so-bw-sp__body). A
       max-height paired with overflow-y: auto bounds the BOX; the content
       stays reachable.
     - The bounded card itself. NOTE: since superseded — see RESPONSIVE
       AUTO-HEIGHT below, which releases .so-bw-container's max-height, the
       body's scrolling and the card/slot/time list caps. The card is now as
       tall as its step and the page is the scroll region.
     - Circular counters holding a single digit (.so-bw-card-check,
       .so-bw-recurring-preview-num, .so-bw-card-extras-count) and icon
       boxes — geometry around a glyph, never translated text.
   ────────────────────────────────────────────────────────────────────────── */

/* ── 1 · Text nodes wrap, and grow their box ─────────────────────────────
   Everything here is admin-authored content (service, staff, location and
   add-on names, descriptions, group headings), translated UI copy, or
   customer-entered data. All three are unbounded in length by definition. */
#so-booking-widget .so-bw-step-label,
#so-booking-widget .so-bw-step-desc,
#so-booking-widget .so-bw-step-count,
#so-booking-widget .so-bw-brand__title,
#so-booking-widget .so-bw-brand__subtitle,
#so-booking-widget .so-bw-stepbar-label,
#so-booking-widget .so-bw-group-title,
#so-booking-widget .so-bw-card-title,
#so-booking-widget .so-bw-card-meta,
#so-booking-widget .so-bw-card-extra-name,
#so-booking-widget .so-bw-card-extra-duration,
#so-booking-widget .so-bw-card-extra-price,
#so-booking-widget .so-bw-slot-panel-title,
#so-booking-widget .so-bw-slot-panel-subtitle,
#so-booking-widget .so-bw-slot-group-title,
#so-booking-widget .so-bw-slot-row-flag,
#so-booking-widget .so-bw-slot-back-date,
#so-booking-widget .so-bw-summary-label,
#so-booking-widget .so-bw-summary-value,
#so-booking-widget .so-bw-summary-section-title,
#so-booking-widget .so-bw-summary-svc-item,
#so-booking-widget .so-bw-summary-svc-meta,
#so-booking-widget .so-bw-multi-summary-label,
#so-booking-widget .so-bw-multi-summary-meta,
#so-booking-widget .so-bw-confirmation-title,
#so-booking-widget .so-bw-confirmation-text,
#so-booking-widget .so-bw-confirmation-detail-label,
#so-booking-widget .so-bw-confirmation-detail-value,
#so-booking-widget .so-bw-rail-title,
#so-booking-widget .so-bw-rail-sub,
#so-booking-widget .so-bw-rail-k,
#so-booking-widget .so-bw-rail-v,
#so-booking-widget .so-bw-agenda-day-date,
#so-booking-widget .so-bw-agenda-day-count,
#so-booking-widget .so-bw-time-range-label,
#so-booking-widget .so-bw-time-range-times,
#so-booking-widget .so-bw-policies-item,
#so-booking-widget .so-bw-policies-group-title,
#so-booking-widget .so-bw-policy-panel-label,
#so-booking-widget .so-bw-policy-panel-value,
#so-booking-widget .so-bw-policy-panel-text,
#so-booking-widget .so-bw-policy-panel-title,
#so-booking-widget .so-bw-label,
#so-booking-widget .so-bw-field-help,
#so-booking-widget .so-bw-checkbox-label,
#so-booking-widget .so-bw-badge,
#so-booking-widget .so-bw-btn,
#so-booking-widget[data-template] .so-bw-step-label,
#so-booking-widget[data-template] .so-bw-step-desc,
#so-booking-widget[data-template] .so-bw-step-count,
#so-booking-widget[data-template] .so-bw-brand__title,
#so-booking-widget[data-template] .so-bw-brand__subtitle,
#so-booking-widget[data-template] .so-bw-stepbar-label,
#so-booking-widget[data-template] .so-bw-group-title,
#so-booking-widget[data-template] .so-bw-card-title,
#so-booking-widget[data-template] .so-bw-card-meta,
#so-booking-widget[data-template] .so-bw-card-extra-name,
#so-booking-widget[data-template] .so-bw-card-extra-duration,
#so-booking-widget[data-template] .so-bw-card-extra-price,
#so-booking-widget[data-template] .so-bw-slot-panel-title,
#so-booking-widget[data-template] .so-bw-slot-panel-subtitle,
#so-booking-widget[data-template] .so-bw-slot-group-title,
#so-booking-widget[data-template] .so-bw-slot-row-flag,
#so-booking-widget[data-template] .so-bw-slot-back-date,
#so-booking-widget[data-template] .so-bw-summary-label,
#so-booking-widget[data-template] .so-bw-summary-value,
#so-booking-widget[data-template] .so-bw-summary-section-title,
#so-booking-widget[data-template] .so-bw-summary-svc-item,
#so-booking-widget[data-template] .so-bw-summary-svc-meta,
#so-booking-widget[data-template] .so-bw-multi-summary-label,
#so-booking-widget[data-template] .so-bw-multi-summary-meta,
#so-booking-widget[data-template] .so-bw-confirmation-title,
#so-booking-widget[data-template] .so-bw-confirmation-text,
#so-booking-widget[data-template] .so-bw-confirmation-detail-label,
#so-booking-widget[data-template] .so-bw-confirmation-detail-value,
#so-booking-widget[data-template] .so-bw-rail-title,
#so-booking-widget[data-template] .so-bw-rail-sub,
#so-booking-widget[data-template] .so-bw-rail-k,
#so-booking-widget[data-template] .so-bw-rail-v,
#so-booking-widget[data-template] .so-bw-agenda-day-date,
#so-booking-widget[data-template] .so-bw-agenda-day-count,
#so-booking-widget[data-template] .so-bw-time-range-label,
#so-booking-widget[data-template] .so-bw-time-range-times,
#so-booking-widget[data-template] .so-bw-policies-item,
#so-booking-widget[data-template] .so-bw-policies-group-title,
#so-booking-widget[data-template] .so-bw-policy-panel-label,
#so-booking-widget[data-template] .so-bw-policy-panel-value,
#so-booking-widget[data-template] .so-bw-policy-panel-text,
#so-booking-widget[data-template] .so-bw-policy-panel-title,
#so-booking-widget[data-template] .so-bw-label,
#so-booking-widget[data-template] .so-bw-field-help,
#so-booking-widget[data-template] .so-bw-checkbox-label,
#so-booking-widget[data-template] .so-bw-badge,
#so-booking-widget[data-template] .so-bw-btn {
    /* The four that together mean "never truncate": text may break, the box
       may not clip, and no ellipsis or clamp may substitute for either. */
    white-space: normal !important;
    text-overflow: clip !important;
    -webkit-line-clamp: none !important;
    overflow-wrap: anywhere !important;
    word-break: break-word !important;
    /* Releases a flex/grid item's automatic minimum, which is what makes the
       break above actually possible inside a row. */
    min-width: 0 !important;
    /* Deliberately NOT !important: this is a containment floor, and a template
       or a base rule with a considered cap (the summary label's 45% column)
       should keep it. Both are relaxations of the same constraint, so the
       stronger one winning is the correct outcome either way. */
    max-width: 100%;
    /* Content decides the height, always. */
    height: auto !important;
    max-height: none !important;
}

/* Same set, minus the pieces whose `display` is load-bearing: restoring a
   normal flow display is what undoes a `-webkit-box` line-clamp, but it must
   not flatten a flex row or an inline-flex pill. */
#so-booking-widget .so-bw-card-meta,
#so-booking-widget .so-bw-card-title,
#so-booking-widget .so-bw-summary-value,
#so-booking-widget .so-bw-confirmation-detail-value,
#so-booking-widget[data-template] .so-bw-card-meta,
#so-booking-widget[data-template] .so-bw-card-title,
#so-booking-widget[data-template] .so-bw-summary-value,
#so-booking-widget[data-template] .so-bw-confirmation-detail-value {
    display: block !important;
    overflow: visible !important;
}

/* ── 2 · Rows reflow instead of squeezing ────────────────────────────────
   Each of these is a label/value or content/meta pair in a flex row. Wrapping
   the ROW is what lets both keep their text: the second item takes its own
   line rather than being compressed to a column narrower than its longest
   word. `gap` gets a row component so a wrapped line is not flush. */
#so-booking-widget .so-bw-step-indicator,
#so-booking-widget .so-bw-card-badges,
#so-booking-widget .so-bw-card-extra-row,
#so-booking-widget .so-bw-summary-row,
#so-booking-widget .so-bw-summary-svc-item,
#so-booking-widget .so-bw-multi-summary-row,
#so-booking-widget .so-bw-confirmation-detail,
#so-booking-widget .so-bw-rail-row,
#so-booking-widget .so-bw-nav,
#so-booking-widget[data-template] .so-bw-step-indicator,
#so-booking-widget[data-template] .so-bw-card-badges,
#so-booking-widget[data-template] .so-bw-card-extra-row,
#so-booking-widget[data-template] .so-bw-summary-row,
#so-booking-widget[data-template] .so-bw-summary-svc-item,
#so-booking-widget[data-template] .so-bw-multi-summary-row,
#so-booking-widget[data-template] .so-bw-confirmation-detail,
#so-booking-widget[data-template] .so-bw-rail-row,
#so-booking-widget[data-template] .so-bw-nav {
    flex-wrap: wrap;
    height: auto;
    max-height: none;
}

/* ── 3 · Content containers are sized by their content ───────────────────
   A card, a group panel, a dialog body or a confirmation block may be given a
   minimum, never a fixed height, and may not clip what overflows it — the
   widget's own scroll regions (section note above) are where bounded height
   belongs.

   `:not(.so-bw-sp__panel)` on every .so-bw-card selector below, because the
   staff-profile popup's panel deliberately carries that class to inherit the
   template's card surface (see the next section). The panel IS one of the
   widget's scroll regions — capped at the viewport with a scrolling body — so
   an !important height: auto here would out-rank its non-important cap
   despite the panel's higher specificity, unbound it, and push its footer
   off-screen on a short window. */
#so-booking-widget .so-bw-card:not(.so-bw-sp__panel),
#so-booking-widget .so-bw-card-top,
#so-booking-widget .so-bw-card-body,
#so-booking-widget .so-bw-card-extras,
#so-booking-widget .so-bw-slot-group,
#so-booking-widget .so-bw-slot-row,
#so-booking-widget .so-bw-time-slot,
#so-booking-widget .so-bw-summary-section,
#so-booking-widget .so-bw-confirmation,
#so-booking-widget .so-bw-confirmation-details,
#so-booking-widget .so-bw-policies,
#so-booking-widget .so-bw-policy-panel,
#so-booking-widget[data-template] .so-bw-card:not(.so-bw-sp__panel),
#so-booking-widget[data-template] .so-bw-card-top,
#so-booking-widget[data-template] .so-bw-card-body,
#so-booking-widget[data-template] .so-bw-card-extras,
#so-booking-widget[data-template] .so-bw-slot-group,
#so-booking-widget[data-template] .so-bw-slot-row,
#so-booking-widget[data-template] .so-bw-time-slot,
#so-booking-widget[data-template] .so-bw-summary-section,
#so-booking-widget[data-template] .so-bw-confirmation,
#so-booking-widget[data-template] .so-bw-confirmation-details,
#so-booking-widget[data-template] .so-bw-policies,
#so-booking-widget[data-template] .so-bw-policy-panel {
    height: auto !important;
    max-height: none !important;
}

/* Cards and rows separately, because `overflow: visible` on the two group
   panels above would undo a border-radius the design depends on (the daypart
   group's header strip, the summary section's rounded first row) — those clip
   a BACKGROUND, at auto height, so they never clip text. */
#so-booking-widget .so-bw-card:not(.so-bw-sp__panel),
#so-booking-widget .so-bw-card-top,
#so-booking-widget .so-bw-card-body,
#so-booking-widget .so-bw-slot-row,
#so-booking-widget .so-bw-time-slot,
#so-booking-widget[data-template] .so-bw-card:not(.so-bw-sp__panel),
#so-booking-widget[data-template] .so-bw-card-top,
#so-booking-widget[data-template] .so-bw-card-body,
#so-booking-widget[data-template] .so-bw-slot-row,
#so-booking-widget[data-template] .so-bw-time-slot {
    overflow: visible !important;
}

/* ── 5 · Narrow viewports ────────────────────────────────────────────────
   The floors that stop a reflowed layout from becoming a one-word column.
   Below 380px the widget is edge-to-edge with ~16px of padding either side,
   which leaves ~290px — enough for a wrapped label but not for two of the
   grid columns some layouts still ask for at that width. */
/* was @media (max-width: 380px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    /* Two slot columns at 290px give ~135px per chip, which holds "10:30 AM"
       plus its padding. One column below that, rather than a truncated two. */
    #so-booking-widget:where([data-cw~="380"])[data-template] .so-bw-time-grid{
        grid-template-columns: repeat(auto-fill, minmax(min(100%, 116px), 1fr));
    }
    /* The nav's two buttons go full-width and stack, so neither has to
       compress a wrapped label into a two-word column. */
    #so-booking-widget:where([data-cw~="380"])[data-template] .so-bw-nav .so-bw-btn{
        flex: 1 1 100%;
    }
    /* A label/value pair has no useful two-column form at this width. */
    #so-booking-widget:where([data-cw~="380"])[data-template] .so-bw-summary-label,
    #so-booking-widget:where([data-cw~="380"])[data-template] .so-bw-confirmation-detail-label{
        flex: 1 1 100%;
        max-width: 100%;
    }
    #so-booking-widget:where([data-cw~="380"])[data-template] .so-bw-summary-value,
    #so-booking-widget:where([data-cw~="380"])[data-template] .so-bw-confirmation-detail-value{
        flex: 1 1 100%;
        text-align: left !important;
    }


/* ── 6a · Frost: buy the month its 7 columns back on a phone ─────────────
   Frost is the only template whose ROOT is a painted surface — a gradient
   field with `padding: 32px 20px` (the sheets float on it) — and its body
   sheet adds 22px more either side. On a 320px screen that is 84px of chrome,
   which left 194px for a grid whose 7 tappable columns need 248px. The month
   still scrolled inside its own region rather than escaping the card, so
   nothing was unreachable, but the Sunday column and the "S" of its weekday
   header sat behind a hidden scrollbar, which reads as clipped — the exact
   impression this pass exists to remove.

   Trimmed here to 10px + 14px, which yields 264px for the grid: the full week
   fits, no scroll, and the frosted-sheet look is intact (the sheets are still
   inset from the gradient, just less). Only below 420px, so the design's
   intended generosity survives everywhere it fits. */
/* was @media (max-width: 420px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    #so-booking-widget:where([data-cw~="420"])[data-template-id="glass-frost"]{
        padding: 24px 10px;
    }
    /* The x-inset is narrowed by retuning the GUTTER TOKEN, not by hardcoding
       14px onto each sheet. header/step/nav all read the token, so one
       declaration keeps their edges aligned — a per-rule literal is what
       re-opens the stair-step this section exists to close (and is what
       widget-css-cascade's x-inset invariant forbids). Only the vertical
       padding, which the invariant leaves free, is set per sheet. */
    #so-booking-widget:where([data-cw~="420"])[data-template="frost"] .so-bw-container{
        --so-bw-gutter: 14px;
    }
    /* Trimmed in step with the base rule (18→14), so the phone sheet stays
       tighter than the desktop one rather than overtaking it. */
    #so-booking-widget:where([data-cw~="420"])[data-template="frost"] .so-bw-header{
        padding-top: 12px;
        padding-bottom: 12px;
    }
    /* Same move as the base rule — the narrow-width inset is 16px, and it
       belongs on the step for the same reason (it is the scrollport). */
    #so-booking-widget:where([data-cw~="420"])[data-template="frost"] .so-bw-body{
        padding: 0 var(--so-bw-gutter) 16px;
    }
    #so-booking-widget:where([data-cw~="420"])[data-template="frost"] .so-bw-step{
        padding-top: calc(var(--so-bw-step-pad) + 16px);
    }
    #so-booking-widget:where([data-cw~="420"])[data-template="frost"] .so-bw-nav{
        padding-top: 12px;
        padding-bottom: 12px;
    }


/* ── 6b · The month grid must scroll inside itself, not out of the card ───
   .so-bw-calendar-scroll-inner carries a 248px floor (7 tappable day columns)
   and .so-bw-calendar-scroll is `overflow-x: auto` to scroll it when the
   column is narrower than that. But .so-bw-datetime is a grid, and a grid
   item's automatic minimum is its MIN-CONTENT — 248px, because of that floor.
   So on the layouts with the least content width (Frost at 320px has 228px
   after its 22px body padding and 20px card inset) the track was floored at
   248px and the calendar was painted 11px past the card's right edge, Sunday
   column and weekday header included. Frost's container is `overflow-x:
   visible`, so it hung outside the card in plain sight.

   Releasing the item's automatic minimum puts the floor back where it belongs
   — inside the scroll region, where it produces a short horizontal scroll of
   the month instead of an overflow of the widget. Same mechanism as the
   `.so-bw-container > * { min-width: 0 }` phone rule near the top of the file,
   applied to the date step's own grid. */
#so-booking-widget .so-bw-datetime > *,
#so-booking-widget .so-bw-calendar,
#so-booking-widget .so-bw-calendar-scroll,
#so-booking-widget[data-template] .so-bw-datetime > *,
#so-booking-widget[data-template] .so-bw-calendar,
#so-booking-widget[data-template] .so-bw-calendar-scroll {
    min-width: 0;
}

/* ── 7 · The step bar on a narrow screen ─────────────────────────────────
   .so-bw-stepbar is a horizontal `overflow-x: auto` row of `flex: 1 1 0;
   min-width: 54px` items. Nine steps therefore need ~490px before anything
   about the labels matters, so below that the last steps sit outside the card
   — and with `scrollbar-width: none` there is no scrollbar hinting that they
   are there, so they read as cut off. (Wrapping the labels, which the base
   rule now does, is what stops them being truncated; it does nothing about
   the ROW being wider than the card.)

   Wrapped into rows of four here, so every step stays visible and inside the
   card. The connector lines are suppressed in this mode: they are drawn from
   each item to its left neighbour (`left: -50%; right: 50%`), which is a lie
   once the neighbour is on the row above.

   Scoped to the ten layouts that actually SHOW their labels — the other nine
   (split, dialog, hero, frost, weekboard, chat, deck, soft, inline) hide them
   and render the items as fixed-height progress segments, where `flex: 1 1 0`
   is load-bearing and `flex: 0 1 auto` would collapse a segment with no
   content to zero width. Timeline is excluded too: its bar is a vertical
   column, which has no horizontal overflow to solve. */
/* was @media (max-width: 560px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    #so-booking-widget:where([data-cw~="560"])[data-template="centred"] .so-bw-stepbar,
    #so-booking-widget:where([data-cw~="560"])[data-template="tabbed"] .so-bw-stepbar,
    #so-booking-widget:where([data-cw~="560"])[data-template="agenda"] .so-bw-stepbar,
    #so-booking-widget:where([data-cw~="560"])[data-template="wide"] .so-bw-stepbar,
    #so-booking-widget:where([data-cw~="560"])[data-template="daypart"] .so-bw-stepbar,
    #so-booking-widget:where([data-cw~="560"])[data-template="dusk"] .so-bw-stepbar,
    #so-booking-widget:where([data-cw~="560"])[data-template="editorial"] .so-bw-stepbar{
        flex-wrap: wrap;
        justify-content: center;
        gap: 10px 2px;
        overflow-x: visible;
    }
    #so-booking-widget:where([data-cw~="560"])[data-template="centred"] .so-bw-stepbar-item,
    #so-booking-widget:where([data-cw~="560"])[data-template="tabbed"] .so-bw-stepbar-item,
    #so-booking-widget:where([data-cw~="560"])[data-template="agenda"] .so-bw-stepbar-item,
    #so-booking-widget:where([data-cw~="560"])[data-template="wide"] .so-bw-stepbar-item,
    #so-booking-widget:where([data-cw~="560"])[data-template="daypart"] .so-bw-stepbar-item,
    #so-booking-widget:where([data-cw~="560"])[data-template="dusk"] .so-bw-stepbar-item,
    #so-booking-widget:where([data-cw~="560"])[data-template="editorial"] .so-bw-stepbar-item{
        flex: 0 1 25%;
        min-width: 0;
        max-width: 25%;
    }
    #so-booking-widget:where([data-cw~="560"])[data-template="centred"] .so-bw-stepbar-item:not(:first-of-type)::before,
    #so-booking-widget:where([data-cw~="560"])[data-template="tabbed"] .so-bw-stepbar-item:not(:first-of-type)::before,
    #so-booking-widget:where([data-cw~="560"])[data-template="agenda"] .so-bw-stepbar-item:not(:first-of-type)::before,
    #so-booking-widget:where([data-cw~="560"])[data-template="wide"] .so-bw-stepbar-item:not(:first-of-type)::before,
    #so-booking-widget:where([data-cw~="560"])[data-template="daypart"] .so-bw-stepbar-item:not(:first-of-type)::before,
    #so-booking-widget:where([data-cw~="560"])[data-template="dusk"] .so-bw-stepbar-item:not(:first-of-type)::before,
    #so-booking-widget:where([data-cw~="560"])[data-template="editorial"] .so-bw-stepbar-item:not(:first-of-type)::before{
        display: none;
    }

    /* ── DUSK, in a narrow card: the map loses its labels rather than its rows ─
       The wrap above is right for a labelled bar that would otherwise run off
       the card, but it is expensive on the one template that pairs it with a
       DEEP header. MEASURED at 390px: Dusk's header came to 195px — 28 of
       padding, 33 for the "Step 1 of 6" line, and 137 for a two-row labelled
       step grid underneath it — against a 732px card, so 37% of the widget was
       chrome and the service list showed two and a half cards before the fold.
       The other six in that list run 98-133px and were left alone; this is not
       a push for equal headers, it is the one that had crossed a line.

       WHAT GOES IS THE LABEL, not the bar. The step map is worth keeping on a
       phone — it is the only thing showing how much of the booking is left —
       but its wording is already stated in full by `.so-bw-step-indicator`
       directly above it ("Step 1 of 6 · Choose a Service"), so at this width
       the labels are the redundant half, not the dots. Without them the six
       dots fit one row, which is what removes the second row rather than any
       tightening of the spacing.

       `flex: 1 1 auto` releases the `0 1 25%` the wrap sets, because that
       basis is what forces four-per-row; with the labels gone it would keep a
       six-step flow wrapped for no reason. Same technique the templates that
       hide their labels outright already use (Frost, Hero, Soft, Deck, Week
       Board) — this one just does it at a breakpoint instead of always. */
    #so-booking-widget:where([data-cw~="560"])[data-template="dusk"] .so-bw-stepbar-label{
        display: none;
    }
    #so-booking-widget:where([data-cw~="560"])[data-template="dusk"] .so-bw-stepbar-item{
        flex: 1 1 auto;
        max-width: none;
    }


/* ==========================================================================
   RESPONSIVE AUTO-HEIGHT — the card grows with its step, on all 19 templates
   ==========================================================================

   SUPERSEDES the bounded card. Three earlier blocks capped the widget and made
   .so-bw-body the scroll region:

     ~line 7095   the seven standard-flow architectures  max-height min(78vh,660px)
     ~line 8901   the eight standout architectures       ... the same, !important
     ~line 11093  the global bound                       max-height min(760px,88vh)

   The cap was written to keep the widget reading as a card rather than a long
   page. What it actually produced on a content-heavy step was a SECOND
   scrollbar inside the page's own: Review Booking (summary rows + policies +
   price breakdown + payment) is routinely twice the cap, so the customer had
   to scroll a ~5px inner bar to reach the total and the Confirm button — with
   the page itself not scrolled at all. A widget embedded in a page should
   grow; the page is the scroll region, and it always was.

   So the height is now content-driven everywhere, with a floor so the simple
   steps (one location, two staff) still read as a card and the widget doesn't
   visibly jump between step 1 and step 8.

   Specificity, following the CONTENT-ADAPTIVE GUARD above:
   `#so-booking-widget[data-template] .x` is (1,2,0), which ties with the
   standout-template block's (1,2,0) !important — and this section is LATER in
   the file, so it wins the tie. The duplicate (1,1,0) selector covers a widget
   with no Studio save (no data-template attribute). !important only where it
   must beat an existing !important.

   Deliberately NOT released — each bounds a popover or a secondary panel, not
   the step's primary content, and each stays reachable inside its own box:
     .so-bw-sp__body                the profile popup's scroll region (a real
                                    dialog, sized to the viewport not the page)
     .so-bw-tz-combo-list    200px  the timezone combobox dropdown
     .so-bw-recurring-preview-list  220px, a preview of generated dates
     [data-template="agenda"] .so-bw-agenda-list
                             460px  Agenda's proposition IS a scrolling list of
                                    openings across days; releasing it would run
                                    to hundreds of rows and erase the template.
   ────────────────────────────────────────────────────────────────────────── */

/* ── 1 · The card sizes to its content ─────────────────────────────────── */
#so-booking-widget[data-template] .so-bw-container,
#so-booking-widget .so-bw-container {
    max-height: none !important;
    /* Floor, not a height: simple steps keep a card-like presence, and the
       widget doesn't visibly collapse between a two-card step and Review.
       Viewport-relative so a short laptop window isn't handed a 520px card
       it has to scroll the page to see; the px bounds keep it sane on very
       tall and very short screens. */
    min-height: clamp(360px, 52vh, 560px);
}

/* The four grid architectures (Split, Accordion, Tabbed, Agenda) declare
   `display: grid` with `grid-template-rows: auto 1fr auto`. With the cap gone
   the 1fr middle row resolves to its content, which is exactly what's wanted —
   they need no special case. The three that set their own taller floor
   (split 560px, dusk 520px, agenda 520px) keep it: those are design minimums
   for a two-pane composition, and they are larger than the clamp above. */

/* ── 2 · The body no longer scrolls ────────────────────────────────────── */
/* overflow-x stays clipped: releasing the y axis must not let a wide child
   (a long unbroken service name, a wide summary row) push a horizontal
   scrollbar onto the page. `clip` rather than `hidden` so the y axis can stay
   visible — pairing `visible` with `hidden` silently computes to `auto`, which
   would put the inner scrollbar straight back. */
#so-booking-widget[data-template] .so-bw-body,
#so-booking-widget .so-bw-body {
    overflow-y: visible !important;
    overflow-x: clip;
    max-height: none !important;
    /* Was `min-height: 0`, which existed only to let the body shrink below its
       content so it could scroll. Nothing scrolls now, and the floor lives on
       the container above. */
    min-height: 0;
}

/* ── 3 · No inner scrollers on the step's own content ──────────────────── */
/* Each of these capped a list and grew its own bar inside the (already
   scrolling) body — the "small internal scrollbar" the brief is about. The
   lists are the step's primary content: on an auto-height card they simply
   run to their natural length and the page scrolls.

   .so-bw-card-grid--scroll / .so-bw-scroll-wrap--on are the service / staff /
   location lists (60vh), .so-bw-time-grid the legacy slot pill grid (320px),
   .so-bw-slot-list the current slot list. */
#so-booking-widget[data-template] .so-bw-card-grid,
#so-booking-widget[data-template] .so-bw-card-grid--scroll,
#so-booking-widget[data-template] .so-bw-scroll-wrap--on,
#so-booking-widget[data-template] .so-bw-slot-list,
#so-booking-widget[data-template] .so-bw-time-grid,
#so-booking-widget[data-template] .so-bw-datetime,
#so-booking-widget .so-bw-card-grid,
#so-booking-widget .so-bw-card-grid--scroll,
#so-booking-widget .so-bw-scroll-wrap--on,
#so-booking-widget .so-bw-slot-list,
#so-booking-widget .so-bw-time-grid,
#so-booking-widget .so-bw-datetime {
    max-height: none !important;
    overflow-y: visible;
    overflow-x: clip;
}

/* ── 4 · Phones: release the four templates pinned to the viewport ─────── */
/* Deck, Hero, Frost and Soft cap their ROOT at 100dvh on a phone and scroll
   .so-bw-body inside it (see SECTION 12b). Rule 2 above stops that body
   scrolling — so without this the container would grow past a root that is
   still capped, and everything below the fold would be clipped with no way to
   reach it. The root grows with the card instead; the page scrolls.
   Same 480px breakpoint as the block it releases. */
/* was @media (max-width: 480px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    #so-booking-widget:where([data-cw~="480"])[data-template="deck"],
    #so-booking-widget:where([data-cw~="480"])[data-template="hero"],
    #so-booking-widget:where([data-cw~="480"])[data-template="frost"],
    #so-booking-widget:where([data-cw~="480"])[data-template="soft"]{
        max-height: none;
    }
    /* A phone is narrow, so the floor drops: 52vh of a 780px viewport is a
       lot of empty card under a two-field step. */
    #so-booking-widget:where([data-cw~="480"])[data-template] .so-bw-container,
    #so-booking-widget:where([data-cw~="480"]) .so-bw-container{
        min-height: clamp(260px, 38vh, 420px);
    }


/* ==========================================================================
   REVIEW STEP — a compact recap, and a commit that stays reachable
   ==========================================================================

   Releasing the card's height (above) let Review render in full — and showed
   how tall "in full" is: ~25 label/value rows one per line, then the price
   breakdown, gift card, referral, policies and payment, with Confirm at the
   very bottom. Correct, but a lot of scrolling to reach the one button the
   step exists for.

   Three changes, none of which drop information:

     1. The two RECAP sections go two-up from 600px. Their rows are short
        pairs (Staff / Priya, Duration / 1h 15min); one per full-width line
        was mostly empty space. Rows whose value is free-form — address,
        service list, add-ons, notes, custom fields — carry
        `.so-bw-summary-row--wide` and keep the full line.

     2. Tighter vertical rhythm inside the recap only (the two --pad tokens
        and the section headers). The price rows keep their own spacing:
        money is what the customer checks most carefully.

     3. The commit bar sticks to the bottom of the viewport. Confirm is then
        one glance away at any scroll position, while everything above it
        stays exactly where it was. It is `sticky`, not `fixed`, so on a short
        step it simply sits in the flow as before.

   Scoped to `.so-bw-summary` and the nav, so no other step is affected. The
   (1,2,0) + (1,1,0) selector pair matches all nineteen templates plus the
   no-Studio-save case, per the CONTENT-ADAPTIVE GUARD convention.
   ────────────────────────────────────────────────────────────────────────── */

/* ── 1 · The commit bar stays reachable ────────────────────────────────── */
/* The nav is a sibling of .so-bw-body inside .so-bw-container, and nothing
   between it and the page establishes a scroll container, so `sticky` resolves
   against the page — which is exactly the intent. It already carries the
   template's own card background and top border (see .so-bw-nav), so the
   content scrolling under it is covered. */
#so-booking-widget[data-template] .so-bw-nav,
#so-booking-widget .so-bw-nav {
    position: sticky;
    bottom: 0;
    /* Above the summary panel and the policies block, below the profile
       popup's scrim (2147483000). */
    z-index: 6;
}

/* ── 2 · The recap goes two-up ─────────────────────────────────────────── */
/* was @media (min-width: 600px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    #so-booking-widget:where([data-cwmin~="600"])[data-template] .so-bw-summary-section,
    #so-booking-widget:where([data-cwmin~="600"]) .so-bw-summary-section{
        display: grid;
        grid-template-columns: 1fr 1fr;
        /* The rows draw their own 1px bottom border, so a gap here would
           leave the two columns' borders floating apart. */
        gap: 0;
    }
    /* The header strip spans both columns — it titles the whole section. */
    #so-booking-widget:where([data-cwmin~="600"])[data-template] .so-bw-summary-section-title,
    #so-booking-widget:where([data-cwmin~="600"]) .so-bw-summary-section-title{
        grid-column: 1 / -1;
    }
    /* Free-form values keep the full width. */
    #so-booking-widget:where([data-cwmin~="600"])[data-template] .so-bw-summary-row--wide,
    #so-booking-widget:where([data-cwmin~="600"]) .so-bw-summary-row--wide{
        grid-column: 1 / -1;
    }
    /* A hairline between the columns. Drawn on the row's inline-start edge for
       the SECOND column only, via the grid's own column position, so it stays
       correct no matter which rows are hidden by x-show — nth-child would
       count the hidden ones and stripe the wrong cells. */
    #so-booking-widget:where([data-cwmin~="600"])[data-template] .so-bw-summary-section > .so-bw-summary-row,
    #so-booking-widget:where([data-cwmin~="600"]) .so-bw-summary-section > .so-bw-summary-row{
        border-inline-start: 1px solid var(--so-bw-border);
        margin-inline-start: -1px;
    }
    #so-booking-widget:where([data-cwmin~="600"])[data-template] .so-bw-summary-section > .so-bw-summary-row--wide,
    #so-booking-widget:where([data-cwmin~="600"]) .so-bw-summary-section > .so-bw-summary-row--wide{
        border-inline-start: 0;
        margin-inline-start: 0;
    }

    /* ── THE COLUMNS HAD NO GUTTER ───────────────────────────────────────
       REPORTED from a live screenshot: "Main LocationService",
       "priyanka bDate", "Gowtham sEmail" — the left pair's value running
       straight into the right pair's label with nothing between them.
       Measured across the catalogue: 42 colliding pairs, gap 0px, on Dialog,
       Card Deck, Dual Month, Dusk, Frost, Inline and Coral Pop at every
       width from 600px up.

       `gap: 0` above is deliberate and stays — a real gap would break the
       rows' bottom borders into two floating segments. The gutter has to come
       from INSIDE the row, because padding sits within the row's border box
       and so leaves those borders meeting.

       The reason it was missing: most templates set `padding: Npx 0` on the
       row and take their horizontal inset from an ancestor instead — the
       panel on Card Deck and Coral Pop, the section on Frost, nothing at all
       on Dialog, Dusk, Inline and Dual Month. That is correct in ONE column,
       where the ancestor's inset is the only edge that exists. In two-up it
       leaves the inner edge — the one that matters — at zero.

       So in two-up mode the ROW owns the horizontal inset, and the section
       stops adding one. 16px each side gives a 32px channel either side of
       the divider and an even 16px outer inset, which is also the first time
       these eleven have agreed on that number.

       Not scoped to the templates that collided: all nineteen share this
       component and this media query, and a gutter that exists on some of
       them is the inconsistency this pass is for. */
    #so-booking-widget:where([data-cwmin~="600"])[data-template] .so-bw-summary-section,
    #so-booking-widget:where([data-cwmin~="600"]) .so-bw-summary-section{
        padding-inline: 0;
    }
    #so-booking-widget:where([data-cwmin~="600"])[data-template] .so-bw-summary-section > .so-bw-summary-row,
    #so-booking-widget:where([data-cwmin~="600"]) .so-bw-summary-section > .so-bw-summary-row,
    #so-booking-widget:where([data-cwmin~="600"])[data-template] .so-bw-summary-section > .so-bw-summary-section-title,
    #so-booking-widget:where([data-cwmin~="600"]) .so-bw-summary-section > .so-bw-summary-section-title{
        padding-inline: 16px;
    }


/* ── 3 · Tighter rhythm in the recap ───────────────────────────────────── */
/* Token-level, so every template's summary inherits it and none of them need
   a per-template override. The price rows below the sections set their own
   padding and are untouched. */
#so-booking-widget[data-template] .so-bw-summary,
#so-booking-widget .so-bw-summary {
    --so-bw-summary-pad-y: 9px;
}
#so-booking-widget[data-template] .so-bw-summary-section-title,
#so-booking-widget .so-bw-summary-section-title {
    padding-top: 7px;
    padding-bottom: 7px;
}
/* Policies are a reference block on this step, not the subject of it. */
#so-booking-widget[data-template] .so-bw-policies--review,
#so-booking-widget .so-bw-policies--review {
    padding: 10px 14px;
}

/* ==========================================================================
   CONTAINER BREATHING ROOM — nothing clipped at the card's edges
   ==========================================================================

   Two clipping faults, measured across all nineteen templates:

   1. THE STEPPER. `.so-bw-stepbar` sets `overflow-x: auto` so a long step list
      scrolls sideways. CSS then computes `overflow-y` to `auto` as well —
      `visible` is not allowed to pair with a scrolling axis — which turns the
      bar into a vertical scrollport too. Eight templates (Immersive Rose,
      Dialog Blue, Weekboard Amber, Chat Emerald, Pocket Violet, Ink Mono,
      Glass Frost, Coral Pop) style the bar shorter than its own dots — content
      14px inside a 5-11px box — so the circles were cut off at the top.

      Fixed with vertical padding rather than by touching the overflow: padding
      counts toward clientHeight, so the box grows past its content and the
      scrollport has nothing left to clip. Horizontal scrolling — the reason
      the property is there — is untouched.

   2. THE CARD'S OWN EDGES. The root carried no inline padding on fifteen of
      the nineteen, so `.so-bw-container` ran the full width of the root and
      its rounded corners and `box-shadow` had nowhere to paint. Inside any
      theme wrapper that clips (most do), both were shaved off at the page
      edges. The four templates that already set their own root padding
      (Dialog Blue, Pocket Violet, Glass Frost, and Immersive Rose's hero
      inset) declare it at (1,1,0) and keep it — the rule below is (1,0,0) on
      purpose so it is a floor, not an override.
   ────────────────────────────────────────────────────────────────────────── */

/* ── 1 · The stepper is never cut ──────────────────────────────────────── */
#so-booking-widget[data-template] .so-bw-stepbar,
#so-booking-widget .so-bw-stepbar {
    /* > half the tallest dot + its ring, so the box always outgrows its
       content and the computed overflow-y has nothing to scroll. */
    padding-top: 8px;
    padding-bottom: 8px;
    /* A sideways flick on the step list must not chain into the page. */
    overscroll-behavior-x: contain;
}

/* Hero, Frost and Ink Mono replace the dotted stepper with a row of thin
   segment bars and hide `.so-bw-stepbar-dot`. The connector line between dots
   is drawn as an absolutely-positioned ::before at `top: 12px` — well below a
   3-4px bar — so it was left painting outside the bar's box and was the whole
   of the residual overflow there. It has nothing to connect once the dots are
   gone. */
#so-booking-widget[data-template="hero"] .so-bw-stepbar-item:not(:first-of-type)::before,
#so-booking-widget[data-template="frost"] .so-bw-stepbar-item:not(:first-of-type)::before {
    display: none;
}

/* ── 1b · Clip the corners without becoming a scroll container ─────────── */
/* `.so-bw-container { overflow: hidden }` exists to clip children to the
   card's rounded corners. It also makes the container a scroll container,
   which silently disables `position: sticky` on anything inside it — that is
   why the sticky commit bar (REVIEW STEP section) never pinned.

   `clip` clips exactly the same way but does NOT create a scroll container, so
   the nav's sticky positioning resolves against the page as intended.
   Browsers without `overflow: clip` (Safari < 16) drop this declaration and
   keep the `hidden` above: corners still clip, the commit bar simply sits in
   the flow at the end of the step, which is where it was before. */
#so-booking-widget .so-bw-container {
    overflow: clip;
}

/* ── 2 · Room for the card's corners and shadow ────────────────────────── */
/* clamp so a phone gives up as little width as possible while a desktop gets
   a proper margin; the container is max-width + margin-inline: auto, so this
   only insets it — it never shifts the widget off-centre. */
#so-booking-widget {
    padding-inline: clamp(12px, calc(var(--so-bw-cw, 100vw) * 0.025), 24px);
    /* Top spacing so the header (and the stepper inside it) never sits flush
       against whatever the theme renders above the widget. The existing
       margin-bottom: 40px handles the other end. */
    padding-top: 12px;
}

/* ── 2b · …except at Full Width, where the gutter IS the gap ─────────────
   The clamp above is the last thing between the card and the window once the
   root is full-aligned: 24px a side on a desktop, 19 / 14 / 10 as the viewport
   narrows. Everything else had been removed (the theme's 50px global padding,
   our own 1100px cap) and this was what remained — a symmetric inset that reads
   as "Full Width isn't full width", because on this size that is exactly what
   it is.

   Only the INLINE axis, and only at this size. `padding-top` stays: the widget
   still must not sit flush against whatever the theme renders above it, and
   that is a vertical concern the choice says nothing about. Small / Medium /
   Large keep the clamp in full — they are centred cards with space around them
   by design, and their corners and shadow need the room this was written for.

   The trade is deliberate and worth stating: edge to edge means the card's
   rounded corners and drop shadow now meet the viewport edge, so on the sides
   there is nothing left to see them against. That is inherent to the request —
   a shadow you can still see all the way round is a card with a margin, which
   is what the other three sizes are for.

   Gated on `[data-position="inline"]` as well, because "edge" means the page
   edge and only an inline widget has one. A popup's card sits inside
   `.so-bw-popup__dialog` and a floating one inside `.so-bw-floating__panel`,
   where this gutter is the inset from the shell's own rounded frame rather than
   a gap in the page — so those two keep it. `(1,2,0)`, which also outranks the
   `:where([data-cw~="480"])` phone override at `(1,0,0)`, so it holds at every
   width without a companion rule. */
#so-booking-widget[data-position="inline"][data-size="full"] {
    padding-inline: 0;
}
/* was @media (max-width: 480px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    #so-booking-widget:where([data-cw~="480"]){
        padding-inline: 10px;
        padding-top: 8px;
    }


/* ==========================================================================
   UI CONSISTENCY PASS
   ==========================================================================

   Six unrelated defects found by walking all nineteen templates through all
   eight steps at desktop / tablet / phone and measuring the DOM (clipping
   against overflow ancestors, WCAG contrast of every text run, control
   widths, card-height variance). Each numbered part below fixes one of them
   and says what it measured.

   Sits before the popup section, which must stay last.
   ========================================================================== */

/* ── 1 · Readable "ink" tones ──────────────────────────────────────────────

   Two colour tokens were doing jobs they were not mixed for.

   (a) --so-bw-text-tertiary resolved to --so-bw-ds-n300 (#c1c7d0). The ramp's
       own comment says it is tuned so "500 on white and 700 on n50 clear AA" —
       n300 is a BORDER tone, and n300 on white measures 1.70:1. Every use of
       tertiary text was therefore failing AA by a wide margin: the staff
       time-zone line, the "no available times" message, the inactive step-bar
       labels, disabled calendar days. n400 is added for the job and tertiary
       binds to it: 4.9:1 on white, still clearly lighter than secondary.

   (b) --so-bw-primary is a BUTTON FILL. Used as small text on a light tint —
       price badges, the status pill, "View profile" — it measured 2.4-3.4:1
       depending on the template's hue. Rather than hand-pick nineteen text
       colours (which would also drift the moment a template's hue changes),
       --so-bw-primary-ink darkens the SAME colour just enough to read: OKLCH
       relative colour keeps hue and chroma exactly and caps lightness, so the
       badge is still recognisably the template's own colour.

       min() means an already-dark primary is left alone. Browsers without
       relative colour syntax (Safari < 16.4, Firefox < 128) drop the
       declaration and fall back to the plain var() on the line above, i.e.
       today's rendering — nothing breaks, it just is not improved. */
#so-booking-widget {
    --so-bw-ds-n400: #63697a;
}
#so-booking-widget .so-bw-container {
    --so-bw-text-tertiary: var(--so-bw-ds-n400);

    /* Text painted ON the brand fill — the confirmation banner, the avatar
       initials, the WhatsApp button. Its own token because "white" here is a
       function of the FILL, not of the page: binding it to --so-bw-card-bg (as
       a first pass at de-hardcoding did) turned the initials dark on the dark
       templates, where the card surface is dark but the brand fill is not.
       Matches .so-bw-btn--primary, which has always been white on primary. */
    --so-bw-on-primary: #ffffff;

    --so-bw-primary-ink: var(--so-bw-primary);
    --so-bw-primary-ink: oklch(from var(--so-bw-primary) min(l, 0.47) c h);
    --so-bw-success-ink: var(--so-bw-success);
    --so-bw-success-ink: oklch(from var(--so-bw-success) min(l, 0.47) c h);
    --so-bw-error-ink: var(--so-bw-error);
    --so-bw-error-ink: oklch(from var(--so-bw-error) min(l, 0.47) c h);
}
/* On a dark canvas the correction runs the other way: the same hue has to be
   LIGHTENED to clear its surface, and the neutral ramp's tertiary step has to
   come from the light end. Matches the existing dark-first selector pair so a
   template added to that list inherits this too. */
#so-booking-widget[data-theme="dark"] .so-bw-container,
#so-booking-widget[data-template="dark"] .so-bw-container,
#so-booking-widget[data-template-id="dusk-champagne"] .so-bw-container {
    --so-bw-text-tertiary: #8b90a3;
    --so-bw-primary-ink: oklch(from var(--so-bw-primary) max(l, 0.74) c h);
    --so-bw-success-ink: oklch(from var(--so-bw-success) max(l, 0.74) c h);
    --so-bw-error-ink: oklch(from var(--so-bw-error) max(l, 0.74) c h);
}

/* Every `color: var(--so-bw-primary)` in this stylesheet now reads
   `var(--so-bw-primary-ink, var(--so-bw-primary))` — 47 declarations, applied
   at the source rather than as an override list, so a rule added later cannot
   quietly reintroduce the unreadable version by not being on a list. Only
   `color` was rewritten: `background`, `border-color` and `fill` still take
   the undarkened brand hue, because a primary BUTTON must stay the primary
   button. */
#so-booking-widget .so-bw-badge--free,
#so-booking-widget[data-template] .so-bw-badge--free {
    color: var(--so-bw-success-ink, var(--so-bw-success));
}

/* Disabled and out-of-month day cells stacked a 0.42 / 0.55 opacity on top of
   the already-too-light tertiary, landing at 1.56-1.70:1 — not "de-emphasised"
   but genuinely unreadable, and a customer cannot tell an unavailable date
   from a rendering glitch. Unavailable dates are still obviously unavailable
   (tinted cell, no pointer, strikethrough for holidays); they are now legible
   while being so. */
#so-booking-widget .so-bw-calendar-day--other-month { opacity: 0.75; }
#so-booking-widget .so-bw-calendar-day--disabled { opacity: 0.62; }

/* ── 2 · Service card: three-line description + Read more ────────────────

   Card height was set by the longest description in the catalogue: a verbose
   service and a one-line service side by side measured 219px and 124px, and
   the grid row stretched to the taller one, so a two-service row was mostly
   whitespace.

   The clamp is back, but this time the text it hides is reachable — the card
   renders a Read more link that opens the full description in a modal. That is
   the piece the earlier removal was missing: the note on the base
   .so-bw-card-meta rule records that the clamp was dropped precisely because it
   destroyed content with no way back to it.

   Everything here needs !important because the "never truncate" block in the
   RESPONSIVE AUTO-HEIGHT section forces display / overflow / white-space /
   line-clamp with !important on .so-bw-card-meta. This is a deliberate, scoped
   exception to that rule, so it restates the same properties at equal
   specificity and later source order. The MODIFIER is what keeps the exception
   narrow: the location card's address still never truncates.

   THE STAFF CARD NOW CARRIES THE MODIFIER TOO. This note used to name it as an
   exemption — "the staff card's role line" — which described the wrong thing:
   that line renders `member.bio || member.title`, so it is a BIOGRAPHY first
   and the role only as a fallback. Unclamped, one verbose bio measured 626px
   on Wide Canvas, 459 on Hero, 417 on Agenda and 401 on Accordion, and
   grid-auto-rows below stretched every colleague's card to match it.

   The same condition applies as for services: the clamp is only allowed
   because the text it hides is reachable. View profile sits directly under the
   line and opens the whole bio, which is the staff card's Read more. */
#so-booking-widget .so-bw-card-meta--clamp,
#so-booking-widget[data-template] .so-bw-card-meta--clamp {
    display: -webkit-box !important;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2 !important;
    line-clamp: 2;
    overflow: hidden !important;
    text-overflow: ellipsis !important;
    line-height: 1.45;
    /* Reserve the full two lines so a one-line description does not sit a line
       shorter than the card beside it. The markup only renders this box at all
       when SOME service in the filtered list has a description, so a catalogue
       with no descriptions reserves nothing.

       Two lines rather than three: the clamp is a preview, and the whole text
       is one click away in the Read more modal, so the third line was buying
       about eight more words at the cost of ~19px on every card in the grid —
       which is 19px off how much of the catalogue fits on screen. */
    min-height: calc(2 * 1.45em);
}
/* The card line renders the author's INLINE markup (so a coloured description
   reads coloured here, not only in the Read more panel — see
   soDescInlineHtml). soDescInlineHtml already unwraps the block tags, because a
   block child inside `display: -webkit-box` becomes its own box and breaks the
   clamp outright. This is the belt to that braces: anything that still arrives
   as a block — a stray tag, a shape a future editor emits — is forced inline
   and stripped of the margins that would push the second line out of view.
   The author's own `color` is an inline style and therefore untouched. */
#so-booking-widget .so-bw-rt-inline * {
    display: inline !important;
    margin: 0 !important;
    padding: 0 !important;
    list-style: none !important;
    font-size: inherit !important;
    line-height: inherit !important;
}
/* Clamping the description bounds the runaway case, but it is not the whole of
   "same height": a two-line service NAME still made its row taller than the row
   below it (measured 193px against 171px for the same three services).
   grid-auto-rows levels every row in the grid to the tallest card, so the last
   row — the one with a single card and nothing beside it to stretch against —
   matches the rest instead of ending 22px short.

   Excluded while an add-ons panel is expanded: that card is deliberately taller
   for as long as it is open, and levelling to it would inflate every other card
   in the catalogue. :has() is the guard; a browser without it simply keeps
   today's per-row behaviour, which is the pre-existing rendering. */
#so-booking-widget .so-bw-card-grid:not(:has(.so-bw-card--has-extras-open)) {
    grid-auto-rows: 1fr;
}

/* Level cards are not the same as level CONTENT. With the rows equalised, a
   two-line service name still pushed its duration/price badges 23px below the
   badges on the card beside it, so the eye read a ragged baseline across a grid
   of same-height boxes. The body becomes a column and the badge strip takes the
   slack, which pins every card's badges to the same line.

   Scoped to service cards: staff and location cards have a different body and
   are not laid out in a comparable grid. */
#so-booking-widget .so-bw-card--service .so-bw-card-top {
    flex: 1 1 auto;
    align-items: stretch;
}
#so-booking-widget .so-bw-card--service .so-bw-card-body {
    display: flex;
    flex-direction: column;
}
#so-booking-widget .so-bw-card--service .so-bw-card-badges {
    margin-top: auto;
    padding-top: 6px;
}
/* The header band already gives the card a generous top edge, so the text row
   underneath does not need the full card padding above it as well. Two pixels
   a side, times every card in a scrolling grid, is a row. */
#so-booking-widget .so-bw-card--service .so-bw-card-top {
    padding-top: calc(var(--so-bw-card-pad) - 4px);
    padding-bottom: calc(var(--so-bw-card-pad) - 2px);
}
/* The row stretches now, so the avatar tile — which is a fixed square — would
   sit at the top of a tall card. Keep it optically on the title. */
#so-booking-widget .so-bw-card--service .so-bw-card-icon,
#so-booking-widget .so-bw-card--service .so-bw-card-check {
    align-self: center;
}

/* The link that gets the clamped text back. A plain text button rather than a
   second visual weight on the card — the card's own job is still to be picked.
   Sibling of the clamped box, never inside it, so it cannot be clipped by the
   very clamp it exists to undo. */
#so-booking-widget .so-bw-card-more {
    display: inline-block;
    /* The body is a flex column, so an inline-block child would be stretched
       and then centred by whatever text-align the template sets on the card.
       Pin it to the start so it lines up with the description above it. */
    align-self: flex-start;
    text-align: left;
    margin-top: 2px;
    margin-bottom: 2px;
    padding: 2px 0;
    background: none;
    border: 0;
    font: inherit;
    font-size: 12px;
    font-weight: 600;
    line-height: 1.3;
    text-decoration: underline;
    text-underline-offset: 2px;
    cursor: pointer;
    transition: opacity var(--so-bw-transition);
}
#so-booking-widget .so-bw-card-more:hover { opacity: 0.75; }
#so-booking-widget .so-bw-card-more:focus-visible {
    outline: 2px solid var(--so-bw-primary);
    outline-offset: 2px;
    border-radius: var(--so-bw-r-4);
}
/* Same trick as the reserved description box: when only SOME cards in the grid
   need the link, the others keep its height so the rows stay level. Hidden
   from the pointer and from assistive tech (the markup also sets
   tabindex="-1" and aria-hidden), so it is space and nothing else. */
#so-booking-widget .so-bw-card-more--ghost {
    visibility: hidden;
    pointer-events: none;
}

/* ── 3 · Phone row: the country picker only needs to hold a dial code ────

   Measured between 139px and 531px wide depending on the template, because the
   static width was being erased at runtime. Alpine's :style with a STRING
   value rewrites the whole style attribute, so the `width: 128px` sitting in
   style="" disappeared the first time the binding evaluated and the native
   select fell back to sizing itself to its widest option ("United Arab
   Emirates +971"). The markup now carries classes and the error state is a
   class too, so nothing can wipe the geometry.

   The width holds a flag, up to a four-digit dial code and the native chevron,
   and not one character more — every pixel beyond that comes off the number
   field, which is the input people actually type into. */
#so-booking-widget .so-bw-phone-row {
    display: flex;
    align-items: stretch;
    gap: 8px;
}
#so-booking-widget .so-bw-phone-dial {
    flex: 0 0 auto;
    width: 104px;
    min-width: 0;
    padding-inline: 10px 6px;
    cursor: pointer;
    /* Deliberately NOT .so-bw-select: that class paints a chevron as a
       background image and reserves 36px of right padding for it, which at
       104px would leave no room for the code. It also no longer HAS a native
       arrow to fall back on — the control is a <button> now, and the chevron
       is a real <svg> laid out by the flex rule below — so this line survives
       only to guard against a template painting one on .so-bw-input. */
    background-image: none;
    /* It is a <button> now, so none of the UA button defaults are wanted: the
       label must sit left with the chevron pushed to the right edge, the text
       must inherit the widget's font rather than the platform's, and the
       centred alignment a button applies by default would put the code in the
       middle of the pill. */
    display: flex;
    align-items: center;
    gap: 4px;
    text-align: left;
    font: inherit;
    line-height: 1.5;
    /* The wrapper is the positioning context for the panel, not this. */
    position: static;
}
/* The dial wrapper must not size itself: the width above belongs to the
   trigger, and a flex parent would otherwise stretch this box instead. */
#so-booking-widget .so-bw-combo--dial {
    flex: 0 0 auto;
}
#so-booking-widget .so-bw-combo--dial .so-bw-combo-value {
    /* The code is the content; a long ISO + 4-digit dial must clip rather than
       push the chevron out of the pill. */
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}
#so-booking-widget .so-bw-combo--dial .so-bw-combo-chevron {
    flex: 0 0 auto;
    opacity: 0.55;
    transition: transform var(--so-bw-transition);
}
#so-booking-widget .so-bw-combo--dial.so-bw-combo--open .so-bw-combo-chevron {
    transform: rotate(180deg);
}
#so-booking-widget .so-bw-phone-number {
    flex: 1 1 auto;
    /* Without this a flex item refuses to shrink below its content width and
       the row overflows the card on a narrow phone. */
    min-width: 0;
}
/* was @media (max-width: 380px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    #so-booking-widget:where([data-cw~="380"]) .so-bw-phone-dial{ width: 92px; }

/* ── 3b · The shared dropdown panel ──────────────────────────────────────

   `.so-bw-combo` is the widget's ONE menu surface. It exists because a native
   <select>'s popup is drawn by the operating system and cannot be styled at
   all: whatever a template does to its colours, type and corners stops at the
   closed control, and the open list arrives as a grey (or, in dark mode, black)
   system menu with the platform's own highlight blue. On a template that has
   just spent a whole section establishing a register, that is the one element
   that ignores it.

   MODELLED ON `.so-bw-tz-combo`, deliberately and closely — same 4px gap, same
   absolute panel, same flip-up, same capped scrolling list, same z-index of 30.
   The timezone picker was already this widget's answer to "a menu we control",
   and answering it a second way would give the product two dropdown languages.
   The two now share `_placeComboPanel()` in the JS for exactly that reason.
   (They stay separate class families because the tz picker sits inside a
   sticky, opaque slot-panel head and carries host-chain unlocks this one does
   not need; merging the selectors would drag those into the form.)

   EVERY PADDING/MARGIN RULE IS ID-QUALIFIED. `#so-booking-widget *` zeroes
   both at (1,0,0), so a plain `.so-bw-combo-search { padding }` at (0,1,0)
   loses and the search box collapses under its own magnifier — the exact bug
   the timezone block documents at ~2321. */
#so-booking-widget .so-bw-combo {
    position: relative;
    display: block;
}
/* ── The custom-field variant ────────────────────────────────────────────
   Same panel, no search box (an admin-authored field is three or four
   options), and a trigger that fills its form group rather than sitting at a
   fixed width like the dial does. */
#so-booking-widget .so-bw-combo--field {
    width: 100%;
}
#so-booking-widget .so-bw-combo--field .so-bw-select {
    display: flex;
    align-items: center;
    gap: 8px;
    width: 100%;
    text-align: left;
    font: inherit;
    line-height: 1.5;
    cursor: pointer;
    /* `.so-bw-select` paints a chevron as a BACKGROUND IMAGE and reserves
       36px of right padding for it. The trigger draws a real <svg> now — one
       that can rotate on open and take currentColor — so the painted one has
       to go or the control shows two. The reserved padding goes with it;
       the flex gap above spaces the real chevron instead. */
    background-image: none;
    padding-right: var(--so-bw-input-pad-x, 12px);
}
#so-booking-widget .so-bw-combo--field .so-bw-combo-value {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}
/* The placeholder row reads as a prompt, not as an answer. */
#so-booking-widget .so-bw-combo-value--empty {
    color: var(--so-bw-text-tertiary);
}
#so-booking-widget .so-bw-combo--field .so-bw-combo-chevron {
    flex: 0 0 auto;
    opacity: 0.55;
    transition: transform var(--so-bw-transition);
}
#so-booking-widget .so-bw-combo--field.so-bw-combo--open .so-bw-combo-chevron {
    transform: rotate(180deg);
}
/* A field panel matches its trigger's width instead of hugging its content:
   the options are sentences an admin wrote, and a menu narrower than the
   control it belongs to reads as a different element. */
#so-booking-widget .so-bw-combo--field .so-bw-combo-panel {
    width: 100%;
    max-width: none;
}
#so-booking-widget .so-bw-combo-panel {
    position: absolute;
    top: calc(100% + 4px);
    left: 0;
    right: auto;
    /* At least as wide as the trigger, but free to grow to its content — the
       dial trigger is 104px and "United Kingdom +44" is not going to fit in
       it. Capped so it cannot run off a phone. */
    min-width: 100%;
    width: max-content;
    max-width: min(320px, calc(100vw - 48px));
    border: 1px solid var(--so-bw-border);
    border-radius: min(var(--so-bw-ds-radius-ctl, 10px), 14px);
    background-color: var(--so-bw-card-bg);
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.14);
    overflow: hidden;
    /* 30 for the same reason the timezone panel takes 30, and the value is
       copied rather than re-derived so the two menus can never rank against
       each other: it must clear the commit bar's 6 and stay under the
       staff-profile scrim, which is a true modal. See the z-index inventory. */
    z-index: 30;
}
/* Flipped, when _placeComboPanel found more room above than below. */
#so-booking-widget .so-bw-combo--up .so-bw-combo-panel {
    top: auto;
    bottom: calc(100% + 4px);
}
#so-booking-widget .so-bw-combo-search-wrap {
    position: relative;
    padding: 8px 8px 6px;
}
#so-booking-widget .so-bw-combo-search-icon {
    position: absolute;
    left: 18px;
    top: 50%;
    transform: translateY(-45%);
    color: var(--so-bw-text-tertiary);
    pointer-events: none;
}
#so-booking-widget .so-bw-combo-search {
    width: 100%;
    padding: 7px 10px 7px 30px;
    border: 1px solid var(--so-bw-border);
    border-radius: var(--so-bw-r-8);
    background: var(--so-bw-card-bg);
    color: var(--so-bw-text);
    font-family: var(--so-bw-font);
    /* 16px on a phone or iOS zooms the whole page on focus, which on a menu
       this small throws the customer out of the field they just opened. The
       narrow tier below raises it for that reason, not for legibility. */
    font-size: 13px;
    outline: none;
}
#so-booking-widget .so-bw-combo-search:focus {
    border-color: var(--so-bw-border-focus);
    box-shadow: 0 0 0 3px rgba(var(--so-bw-primary-rgb), 0.15);
}
/* A <ul> regains the UA's 40px start indent and 16px block margin the moment
   it carries a class the reset skips, so it states both itself. The cascade
   suite enumerates every list that has to do this. */
#so-booking-widget .so-bw-combo-list {
    margin: 0;
    padding: 4px;
    list-style: none;
    /* The two literals are the design cap; the custom property only ever
       TIGHTENS it, and is written by _placeComboPanel from the room actually
       available inside the clipping container. */
    max-height: min(240px, 40vh, var(--so-bw-combo-list-max, 240px));
    overflow-y: auto;
    overscroll-behavior: contain;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: thin;
}
#so-booking-widget .so-bw-combo-option {
    display: flex;
    align-items: center;
    gap: 8px;
    width: 100%;
    padding: 7px 8px;
    border: 0;
    border-radius: var(--so-bw-r-6);
    background: transparent;
    color: var(--so-bw-text);
    font-family: var(--so-bw-font);
    font-size: 13px;
    line-height: 1.35;
    text-align: left;
    cursor: pointer;
    transition: background var(--so-bw-transition), color var(--so-bw-transition);
}
#so-booking-widget .so-bw-combo-option:hover {
    background: rgba(var(--so-bw-primary-rgb), 0.08);
}
#so-booking-widget .so-bw-combo-option--selected {
    background: rgba(var(--so-bw-primary-rgb), 0.12);
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
    font-weight: 600;
}
/* The three columns of a dial row: ISO, country name, code. The ISO is set in
   a smaller, quieter register because it is the disambiguator (US vs CA on the
   same +1), not the thing being read. */
#so-booking-widget .so-bw-combo-option-iso {
    flex: 0 0 auto;
    min-width: 22px;
    color: var(--so-bw-text-tertiary);
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.04em;
}
#so-booking-widget .so-bw-combo-option-label {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}
#so-booking-widget .so-bw-combo-option-dial {
    flex: 0 0 auto;
    color: var(--so-bw-text-secondary);
    font-variant-numeric: tabular-nums;
}
#so-booking-widget .so-bw-combo-option--selected .so-bw-combo-option-dial {
    color: inherit;
}
#so-booking-widget .so-bw-combo-check {
    flex: 0 0 auto;
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}
#so-booking-widget .so-bw-combo-empty {
    padding: 10px 8px;
    color: var(--so-bw-text-secondary);
    font-size: 13px;
}
/* was @media (max-width: 520px) — gated on CONTAINER width, like the rest of
   the file. The panel becomes a full-width sheet under the trigger, and the
   search font goes to 16px so iOS does not zoom the page on focus. */

    #so-booking-widget:where([data-cw~="520"]) .so-bw-combo-panel{
        left: 0;
        right: 0;
        width: auto;
        max-width: none;
    }
    #so-booking-widget:where([data-cw~="520"]) .so-bw-combo-search{ font-size: 16px; }

/* One error treatment for every field in the flow, replacing five separate
   inline border-color strings. */
#so-booking-widget .so-bw-input--invalid {
    border-color: var(--so-bw-error) !important;
}
#so-booking-widget .so-bw-input--invalid:focus {
    box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.18);
}

/* ── 4 · Textarea matches the inputs beside it ───────────────────────────

   The Notes and Service Address fields carry .so-bw-input, so they inherited
   its radius and padding — and on the pill templates that is exactly the
   problem. coral-pop and its family set --so-bw-input-radius to 999px, which
   is right for a 44px-tall single-line field and wrong for a 100px-tall box:
   the textarea rendered with bulging semicircular ends that pushed the first
   line of text inwards and swallowed the corners. It read as a different,
   broken control sitting under four tidy ones.

   min() is the whole fix: the textarea takes the template's radius, capped at
   the point where a corner stops being a corner. A square template stays
   square (0 caps to 0), a 10px template is untouched, and a pill template gets
   a rounded box instead of a lozenge.

   Three smaller things also set it apart from the inputs: a textarea is
   inline-level by default (a descender gap under the box put it a few pixels
   off the rhythm of the fields above), it does not inherit line-height the way
   the inputs do, and the native resize grabber paints over the rounded corner.

   resize stays available — a long note is exactly what the field is for — but
   vertical-only, so dragging can never push the textarea outside the card. */
#so-booking-widget .so-bw-textarea,
/* (1,2,0) so it also outranks the per-template rules that set a RAW pill
   radius on .so-bw-input rather than moving the token — coral-pop's
   `[data-template-id="coral-pop"] .so-bw-input { border-radius: 999px }` was
   beating the plain (1,1,0) rule and the textarea stayed a lozenge. Later in
   source, so it wins the tie against those at equal specificity. */
#so-booking-widget[data-template] .so-bw-textarea,
#so-booking-widget[data-template-id] .so-bw-textarea {
    display: block;
    width: 100%;
    box-sizing: border-box;
    resize: vertical;
    min-height: 88px;
    line-height: 1.5;
    font-family: var(--so-bw-font);
    font-size: var(--so-bw-input-font-size);
    border-radius: min(var(--so-bw-input-radius), 16px);
    /* Keeps the grabber off the corner the radius rounds. */
    background-clip: padding-box;
}
#so-booking-widget .so-bw-textarea::-webkit-resizer {
    background-color: transparent;
}

/* ── 5 · Gift-card / referral code row ───────────────────────────────────

   Was a hardcoded #4f46e5 button and a #d1d5db 6px-radius input, written
   inline, on all nineteen templates — indigo chrome in the middle of a coral
   or champagne review step, and unreadable on the dark one. It also carried
   the same Alpine :style bug as the phone picker: the disabled binding wiped
   the static style, so a disabled Apply button lost its background entirely
   and rendered white-on-white at 1.43:1. Both are classes now.  */
#so-booking-widget .so-bw-code-input {
    flex: 1 1 auto;
    min-width: 0;
    font-family: var(--so-bw-font-mono, ui-monospace, monospace);
    letter-spacing: 1px;
    text-transform: uppercase;
}
/* Uppercase + tracked-out monospace is a lot of width for a placeholder. On a
   phone the field is ~200px and "HAVE A REFERRAL CODE? (OPTIONAL)" was being
   cut mid-word; the tracking goes first, since it is decoration and the prompt
   is information. */
/* was @media (max-width: 480px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    #so-booking-widget:where([data-cw~="480"]) .so-bw-code-input{
        letter-spacing: 0.3px;
        font-size: 12px;
    }

#so-booking-widget .so-bw-code-apply {
    flex: 0 0 auto;
    padding: var(--so-bw-input-pad-y) 16px;
    border: 0;
    border-radius: var(--so-bw-input-radius);
    background: var(--so-bw-primary);
    color: var(--so-bw-on-primary, #fff);
    font-family: var(--so-bw-font);
    font-size: 13px;
    font-weight: 600;
    line-height: 1.5;
    cursor: pointer;
    transition: background-color var(--so-bw-transition), opacity var(--so-bw-transition);
}
#so-booking-widget .so-bw-code-apply:hover:not(:disabled) {
    background: var(--so-bw-primary-hover, var(--so-bw-primary));
}
#so-booking-widget .so-bw-code-apply:disabled {
    opacity: 0.5;
    cursor: not-allowed;
}

/* ── 6 · "Your local time" dropdown floats instead of shoving ────────────

   The panel was in normal flow directly under the trigger. Opening it grew the
   sticky header of the slot panel by ~250px, which pushed the entire slot list
   down the card; on nine of the nineteen templates the bottom of the list then
   sat below the viewport. Closing it snapped everything back. That is the
   "breaks the layout" part of the complaint, and no amount of z-index fixes it
   — an in-flow box takes space by definition.

   The original note said absolute positioning was avoided because the slot
   panel was overflow:scroll and would clip it. That is no longer true: the
   panel dropped its inner scroll when the card body became the single scroll
   region, so there is nothing between the combobox and the card to clip
   against. Verified by measurement, not by reading — the audit walks every
   overflow ancestor of the open panel on all nineteen templates and reports a
   clip.

   z-index has to clear the sticky slot-panel header (2) and the sticky review
   commit bar (6); 30 leaves room and still sits under the staff-profile
   overlay, which is a true modal. */
#so-booking-widget .so-bw-tz-combo-panel {
    position: absolute;
    top: calc(100% + 4px);
    /* Pinned to the trigger's RIGHT edge: the combobox sits at the right of the
       slot-panel header on wide layouts, so growing leftwards keeps the panel
       inside the card. inset-inline would grow it rightwards, off the edge. */
    right: 0;
    left: auto;
    /* At least as wide as the trigger, wide enough for a long IANA name, never
       wider than the card. */
    min-width: 100%;
    width: max-content;
    max-width: min(320px, calc(100vw - 48px));
    margin-top: 0;
    /* CAPPED, not simply inherited. The soft / coral-pop family sets
       --so-bw-ds-radius-ctl to 999px for its pill buttons; taking that token
       neat rounded a 260x292 menu into a lozenge with its first and last
       options sliced off. min() keeps a square template square (0 stays 0) and
       a rounded one rounded, while never letting a control radius meant for a
       40px-tall pill apply to a 290px-tall panel. */
    border-radius: min(var(--so-bw-ds-radius-ctl, 10px), 14px);
    z-index: 30;
    /* The list inside scrolls; the panel itself must not, or the search box
       scrolls away with it. */
    overflow: hidden;
}
/* The list is the scroll region, and it gets momentum + contained overscroll so
   reaching its end does not start scrolling the card behind it. */
#so-booking-widget .so-bw-tz-combo-list {
    /* --so-bw-tz-list-max is written by _placeTzPanel() on open: the space the
       clipping container actually leaves in the direction the panel opened. The
       two literals are the design cap; the variable only ever tightens it. */
    max-height: min(240px, 40vh, var(--so-bw-tz-list-max, 240px));
    overflow-y: auto;
    overscroll-behavior: contain;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: thin;
}
/* The combobox is inside a sticky, opaque header. Once the panel is absolute it
   paints out of that header's box, so nothing on the way up may clip it. */
#so-booking-widget .so-bw-slot-panel-head,
#so-booking-widget .so-bw-tz-picker,
#so-booking-widget .so-bw-slot-panel {
    overflow: visible;
}
/* …and the header's own z-index has to clear the sticky nav (5) and the review
   commit bar (6). The panel's z-index alone cannot: `position: sticky` plus a
   z-index makes the header a STACKING CONTEXT, so 30 on the panel only ranks it
   against the header's other children — against the nav it inherits the
   header's 2 and loses. On a phone that showed as the nav, and a template's
   decorative blob, painting straight over an open menu. Raised only while the
   menu is open, so the header goes back to competing at 2 the rest of the
   time. */
#so-booking-widget .so-bw-slot-panel-head--menu {
    z-index: 40;
}
/* Not enough room below the trigger — _placeTzPanel() measured it against the
   container that does the clipping and flipped the panel above instead. The
   container clips to its own rounded corners, so a panel running past the
   bottom edge is not merely awkward, it is invisible. */
#so-booking-widget .so-bw-tz-combo--up .so-bw-tz-combo-panel {
    top: auto;
    bottom: calc(100% + 4px);
}
/* Full-width sheet under the trigger on a phone, where a 320px cap next to a
   right-aligned trigger would leave it hugging one edge. */
/* was @media (max-width: 520px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    #so-booking-widget:where([data-cw~="520"]) .so-bw-tz-combo-panel{
        right: 0;
        left: 0;
        width: auto;
        max-width: none;
    }


/* ==========================================================================
   ONE SERVICE CARD, AND LIST HEIGHT BUDGETS
   ==========================================================================

   Two related problems, both about the flow's height.

   1. A catalogue with photos on some services and not others rendered as TWO
      card designs side by side: a photo card led with a full-width image band
      and put its title underneath, a photo-less card led with a small inline
      avatar and put its title beside it. Different heights, different left
      edges, different baselines, in the same grid.

   2. Nothing bounded the pick lists. A thirty-service catalogue or a forty-slot
      day grew the card until the step indicator was off the top of the screen
      and Continue was off the bottom — the two controls the customer needs to
      make any progress at all.

   The card keeps height: auto; the LISTS inside it are what get a ceiling.
   ========================================================================== */

/* ── 1 · The header band is always there ─────────────────────────────────

   Same element, same height, whichever it holds. Everything below it —
   title, description, duration, price — therefore starts at one y-position
   across the whole catalogue, which is the entire point. The band keeps its
   16:10 ratio (3:2 on a phone), so within a grid every column is the same
   width and every band is the same height by construction. */
/* Shorter than the 16:10 the strip used to run at, and the same ratio on a
   phone (it went to 3:2 there, which is TALLER — a single-column grid makes
   each card much wider, so the ratio was working against the layout at exactly
   the size with the least height to spend). At a ~340px column this is a 170px
   header instead of 250px, which is most of the card-height reduction. Still a
   full-width photo header; the crop is just less deep. */
#so-booking-widget .so-bw-card-media,
#so-booking-widget[data-template] .so-bw-card-media {
    aspect-ratio: 2 / 1;
    /* A ratio alone works against the layouts that need it most: a
       single-column grid (inline, soft, and every template on a phone) makes
       each card much WIDER, so the same ratio produces a much TALLER header —
       a 500px-wide card was getting a 250px band. The cap turns the ratio into
       "up to this deep", so a narrow card keeps its proportions and a wide one
       gets a header rather than a hero. object-fit: cover means the wide case
       simply crops a little more. */
    max-height: 176px;
}
/* was @media (max-width: 480px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    #so-booking-widget:where([data-cw~="480"]) .so-bw-card-media,
    #so-booking-widget:where([data-cw~="480"])[data-template] .so-bw-card-media{
        max-height: 148px;
    }

    /* CARD DECK used to take a tighter cap here, to stop its phone tile — wider
       than its desktop tile, because the grid drops to one column — from
       getting a DEEPER band on the smaller screen. That cap has moved to §17.16
       beside Deck's aspect-ratio and applies at EVERY width, not just below
       480px, because the same drift runs the other way too: the band grew to
       176px on a wide embed. One depth, declared once, next to the ratio it
       belongs to. Chasing it with a second number here is what let the two
       disagree in the first place. */

#so-booking-widget .so-bw-card-media--placeholder {
    display: flex;
    align-items: center;
    justify-content: center;
    /* A quiet wash rather than the photo placeholder's flat grey: the band is
       standing in for an image, and should not read as one that failed to
       load. Falls back to the neutral surface where color-mix is unavailable. */
    background: var(--so-bw-bg-secondary);
    background: color-mix(in srgb, var(--so-bw-primary) 7%, var(--so-bw-card-bg));
}
/* The avatar keeps .so-bw-card-icon--avatar, so it stays the active template's
   avatar primitive and inherits its shape and typography. Only the SIZE is
   overridden here, and via the token rather than width/height: the primitive
   pins all six of width/height/min-/max- to --so-bw-icon-size, so setting
   height alone gets clamped straight back to 48px. */
#so-booking-widget .so-bw-card-media-avatar {
    --so-bw-icon-size: 68px;
    --so-bw-icon-svg-size: 34px;
    --so-bw-icon-text-size: 25px;
    flex: 0 0 auto;
    /* Circular regardless of the template's card radius — this is an avatar,
       not a tile, and a squircle here reads as a cropped photo. */
    border-radius: 50%;
    box-shadow: 0 1px 2px rgba(16, 18, 26, 0.12);
}
/* was @media (max-width: 480px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    #so-booking-widget:where([data-cw~="480"]) .so-bw-card-media-avatar{
        --so-bw-icon-size: 60px;
        --so-bw-icon-svg-size: 30px;
        --so-bw-icon-text-size: 22px;
    }


/* ── 2 · …and everything under it lines up ──────────────────────────────

   With the avatar moved into the band, the text row is text + check on every
   card. The title is the last variable left: a two-line name pushed its
   description, badges and card bottom down relative to the card beside it.
   Two lines are reserved and a third is clamped away — the full name is in the
   title attribute and in the Review step, and a service whose name does not
   fit two lines at 15px has a naming problem rather than a layout one. */
#so-booking-widget .so-bw-card--service .so-bw-card-title,
#so-booking-widget[data-template] .so-bw-card--service .so-bw-card-title {
    display: -webkit-box !important;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2 !important;
    line-clamp: 2;
    overflow: hidden !important;
    text-overflow: ellipsis !important;
    line-height: 1.3;
    min-height: calc(2 * 1.3em);
}

/* ── 3 · List height budgets ─────────────────────────────────────────────

   Each cap is min(what N items need, a share of the viewport). The first term
   is the promise — about nine services, seven staff, a dozen times before a
   scrollbar appears. The second is the guard: on a short window the list gives
   up height rather than pushing the nav off the screen, because a list you
   scroll is usable and a Continue button you cannot reach is not.

   These override the "no inner scrollers" block in the RESPONSIVE AUTO-HEIGHT
   section, which released every list with !important. That release was right
   for the REVIEW step — a recap is read top to bottom and must not be boxed —
   and wrong for the pick lists, which are exactly the case where an internal
   scroll is what keeps the flow's controls on screen. Review keeps its
   release; the four lists below get their ceiling back. Same specificity as
   the release, later in source, so these win. */
/* THE LIST IS SIZED SO THE WIDGET FITS ONE VIEWPORT.
   This was `min(1180px, 72vh)`, and 72vh is what produced the second
   scrollbar. Nothing else was wrong: the container is already
   `max-height: none`, the body is already `overflow-y: visible`, the nav is
   already `position: sticky; bottom: 0`, and the search field is a SIBLING of
   this box rather than a child, so it never scrolled. The fault was pure
   arithmetic — a list allowed 72vh, plus a header, a step description, a search
   field and a nav, does not fit in 100vh, so the DOCUMENT scrolled underneath a
   list that was also scrolling.

   `calc(100dvh - 380px)` states the intent the vh figure only approximated:
   one viewport, less the chrome above and below. The 380 counts, in order:
   header ~90, step description ~30, search ~50, the step's own
   `padding-bottom: clamp(72px, 10vw, 96px)` ~80, the nav ~70, container
   gutters ~30 — about 350, plus ~30 of slack so a template that runs a little
   taller than the median does not put the scrollbar straight back.

   The step padding AND the nav both count. A `position: sticky` element still
   occupies flow, and that padding exists precisely so the last card is not
   hidden beneath it — so the space below the list is nav plus padding, not nav
   alone. Counting the nav only is what would leave this ~80px short.

   `dvh` not `vh` so a phone's collapsing URL bar does not leave the list
   permanently overhanging. The 720px arm keeps a very tall monitor from
   handing over a list nobody wants to read in one column.

   min-height is the floor, and it deliberately WINS over the max-height above
   it (CSS resolves min-height last): on a viewport too short for the chrome the
   calc would otherwise collapse the list to nothing. There the page scrolls
   instead, which is the honest outcome — a 180px list is still usable, an empty
   one is not. */
#so-booking-widget[data-template] .so-bw-scroll-wrap,
#so-booking-widget .so-bw-scroll-wrap {
    max-height: none !important;
    overflow-y: visible;
    overflow-x: clip;
}
#so-booking-widget[data-template] .so-bw-card-grid--scroll,
#so-booking-widget .so-bw-card-grid--scroll {
    max-height: none !important;
    overflow-y: visible;
    overflow-x: clip;
}
#so-booking-widget[data-template] .so-bw-slot-scroll,
#so-booking-widget .so-bw-slot-scroll {
    max-height: none !important;
    overflow-y: visible;
    overflow-x: clip;
    /* Structural, not scroll-related: the wrapper is a new box between the
       panel and its groups, and without this they lose the panel's column
       gap. Stays. */
    display: flex;
    flex-direction: column;
    gap: 10px;
}
/* The scrollbar cosmetics that stood here are gone with the scrollbars. */
/* A phone has far less height to spend, and the nav is the thing that must
   survive. The lists tighten; the counts drop by two or three, which is the
   right trade on a screen you scroll with your thumb anyway. */
/* The phone tier that tightened these three ceilings is gone with them. One
   scroll region cannot be tightened — the page simply runs longer, which is
   the behaviour a phone is built around. */

/* Four architectures cap their ROOT at 100dvh on a phone and scroll
   .so-bw-body inside it. A second ceiling inside that one double-bounds the
   list and can leave the card mostly empty, so there the body stays the single
   scroll region — which is the behaviour those layouts are designed around. */
/* was @media (max-width: 480px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    #so-booking-widget:where([data-cw~="480"])[data-template="deck"] .so-bw-scroll-wrap,
    #so-booking-widget:where([data-cw~="480"])[data-template="hero"] .so-bw-scroll-wrap,
    #so-booking-widget:where([data-cw~="480"])[data-template="frost"] .so-bw-scroll-wrap,
    #so-booking-widget:where([data-cw~="480"])[data-template="soft"] .so-bw-scroll-wrap,
    #so-booking-widget:where([data-cw~="480"])[data-template="deck"] .so-bw-card-grid--scroll,
    #so-booking-widget:where([data-cw~="480"])[data-template="hero"] .so-bw-card-grid--scroll,
    #so-booking-widget:where([data-cw~="480"])[data-template="frost"] .so-bw-card-grid--scroll,
    #so-booking-widget:where([data-cw~="480"])[data-template="soft"] .so-bw-card-grid--scroll{
        max-height: none !important;
        overflow-y: visible;
    }


/* ── 4 · The two controls that must never leave ──────────────────────────

   With the lists bounded, the card stops growing and the step indicator at the
   top and the nav at the bottom both stay put.

   There used to be a `#so-booking-widget .so-bw-nav { z-index: 5 }` here,
   described as belt and braces so a list with its own stacking context could
   not paint over the commit bar. It was dead: the (1,2,0) pair in the REVIEW
   STEP block earlier in this file sets the nav to 6, and this (1,1,0) rule
   could never beat it. Removed rather than corrected, because it was also the
   file's own reference for "what rank is the nav", and it had the wrong
   number in it.

   THE ACTUAL SCALE, since three separate blocks have now guessed at it:

     2   card media carousel arrows and dots   (card-local — see section 5)
     4   .so-bw-search, sticky top of the scrollport
     4   the card's selection / hover ring     (card-local — see section 5)
     6   .so-bw-nav, the commit bar
    30   .so-bw-tz-combo-panel, the open timezone menu
    40   .so-bw-slot-panel-head--menu, its host lifted while that menu is open
    50   .so-bw-loading, the full-container overlay

   The two 4s do not collide, and that is not luck: the ring's 4 is measured
   inside the card's own stacking context (`isolation: isolate`, section 5),
   so it ranks against the 2s above it and never meets the search field's. */

/* ── 5 · The selection ring lives INSIDE the card ────────────────────────

   Selection was marked with `box-shadow: 0 0 0 2px accent`, which paints a
   ring 2px OUTSIDE the border box. Once the lists above became scroll
   containers (overflow-y: auto / overflow-x: clip), that ring sat in territory
   the scroller clips: the leftmost card's ring was sliced off flat against the
   scroll port's left edge, the rightmost card's ran into the scrollbar, and
   the top row's was cut by the port's top edge. A selected card read as
   cropped — square where it should be round — which is exactly the state the
   customer looks at most, since selecting is the point of the step.

   The frame is an absolutely positioned ::after overlay pinned to the card's
   padding box. Two other primitives were tried first and both fail on this
   markup:

     · An INSET BOX-SHADOW paints above the element's background but below its
       children, and the service card zeroes its own padding so the header band
       (photo or avatar) covers the top, left and right edges edge to edge. The
       ring simply disappears behind it.
     · An INSET OUTLINE fails the same way in practice. The spec paints
       outlines last, but Chrome paints them in the element's own paint phase,
       so the same children cover it — measured at 4x zoom, a card that should
       have shown a 3px accent edge painted 5 device pixels at the top, 4 at
       the sides and 3 at the bottom: the 1px border alone, unevenly
       anti-aliased against different child backgrounds. That unevenness is
       the defect this block exists to fix.

   An overlay is a real box in the paint order, so a z-index above the card's
   own children (the carousel arrows and dots sit at 2) keeps all four sides
   visible over a photo, over the header band, over anything.
   `border-radius: inherit` traces the card's own corners on all nineteen
   templates, from 12px to coral-pop's 22px, and `pointer-events: none` keeps
   the whole card clickable.

   The negative inset is the part that is easy to get wrong. `inset: 0`
   resolves against the PADDING box, one border-width in — and `inherit` hands
   the overlay the card's full 12px radius, while the border's inner edge
   curves at 12px minus that border. On the straight edges the two abut
   exactly; on the curve they diverge, and the card's own background shows
   through as a thin white arc at each corner. Pulling the overlay out by the
   border width lands it on the BORDER box, where the inherited radius is the
   radius it was measured for: the two curves are then concentric, the
   overlay's border covers the card's own, and there is nothing between them
   to show through. Safe on every template — all nineteen render the card
   overflow: visible, so nothing clips the pulled-out pixel.

   Nothing can clip it either: it lives inside the border box, so the scroll
   containers the lists now use have nothing of it to cut.

   Specificity mirrors the SELECTION FEEDBACK block earlier in this file
   ((1,3,0) + !important, which is what beats the per-template-id card rules);
   later source order carries the tie. */

/* …AND THE Z-INDEX HAS TO STAY INSIDE THE CARD, which is the part the first
   version of this block got wrong.

   The ring's `z-index: 4` below was chosen to clear the card's OWN children —
   the carousel arrows and dots sit at 2. But `.so-bw-card` is `position:
   relative` with `z-index: auto`, and that is not a stacking context, so the 4
   never stayed card-local: it escaped into the step's context and competed
   with the widget's chrome. There it ties `.so-bw-search` — sticky, top: 0,
   `z-index: 4` (section 23 rule 3) — and an equal z-index is broken by tree
   order, which the card grid wins because it comes after the search field.

   The result was a 2px accent line ruled straight through the pinned
   "Search services…" band on 18 of the 19 templates, on the service, staff and
   location steps, for the SELECTED card and for any card merely under the
   pointer (the hover ring below carries the same 4). Reported on Accordion.

   `isolation: isolate` is the whole fix: it makes the card a stacking context
   so the 4 is measured against the card's own children again — which is what
   the paragraph above always claimed it meant. It is the one property that
   does this WITHOUT side effects. `transform`, `filter`, `will-change` and
   `contain: paint` would also create a context, but they establish a
   containing block and/or a clip, and either would break the ring's
   `inset: calc(-1 * border-width)` pull-out onto the border box; `contain`
   and `filter` would also trap the `position: fixed` staff-profile popup that
   is rendered from inside a card.

   On `.so-bw-card` rather than on `.so-bw-card--selected`, because the hover
   ring's selector is `:hover:not(.so-bw-card--selected)` — a selected-only
   rule cannot reach the element that owns it, and the hover ring is the one
   that fires on every card the pointer crosses. */
#so-booking-widget[data-template] .so-bw-card,
#so-booking-widget .so-bw-card {
    isolation: isolate;
}

#so-booking-widget[data-template] .so-bw-card.so-bw-card--selected,
#so-booking-widget .so-bw-card.so-bw-card--selected {
    box-shadow: none !important;
    outline: none;
    /* The overlay's containing block. Set here as well as on the base card
       rule so a template that switches the card to static can't detach it. */
    position: relative;
}
#so-booking-widget[data-template] .so-bw-card.so-bw-card--selected::after,
#so-booking-widget .so-bw-card.so-bw-card--selected::after {
    content: '';
    position: absolute;
    inset: calc(-1 * var(--so-bw-card-border-width, 1px));
    border: 2px solid var(--so-bw-primary);
    border-radius: inherit;
    pointer-events: none;
    z-index: 4;
}
/* The slot list is a scroll container too (.so-bw-slot-scroll), so its
   selected row had the same sliced ring, and measured the same uneven
   5/5/3/4 device pixels once the ring moved inside.

   It gets a different mechanism, for a reason: the timeline template already
   uses .so-bw-slot-row::after for the tick joining a row to its node on the
   axis (and ::before for the node itself), so an overlay here would erase
   timeline's connector on exactly the row the customer just picked. A row is
   box-sizing: border-box and its content is centred text, so thickening its
   OWN border costs no layout — the row's outer size does not change and the
   text shifts by a pixel nobody can see. And a border is box decoration: no
   child can paint over it, which is the whole problem being fixed here. */
#so-booking-widget[data-template] .so-bw-slot-row.so-bw-slot-row--selected,
#so-booking-widget[data-template] .so-bw-slot-row.so-bw-slot-row--selected:hover,
#so-booking-widget .so-bw-slot-row.so-bw-slot-row--selected,
#so-booking-widget .so-bw-slot-row.so-bw-slot-row--selected:hover {
    box-shadow: none !important;
    outline: none;
    border-width: 2px !important;
}
/* Hover's 1px accent ring was outside the box for the same reason, and would
   be covered by the same children if it moved inside as an outline. It gets
   the same overlay, one pixel thick, so hover and selection read as the same
   frame at two weights instead of two different treatments. The soft lift
   stays a box-shadow: a blurred drop shadow reads fine at the edges, and the
   gutters added below give it room. */
#so-booking-widget[data-template] .so-bw-card:hover:not(.so-bw-card--selected),
#so-booking-widget .so-bw-card:hover:not(.so-bw-card--selected) {
    box-shadow: inset 0 0 0 999px color-mix(in srgb, var(--so-bw-primary) 8%, transparent),
                0 6px 16px -8px rgba(0, 0, 0, 0.28) !important;
    outline: none;
    position: relative;
}
#so-booking-widget[data-template] .so-bw-card:hover:not(.so-bw-card--selected)::after,
#so-booking-widget .so-bw-card:hover:not(.so-bw-card--selected)::after {
    content: '';
    position: absolute;
    inset: calc(-1 * var(--so-bw-card-border-width, 1px));
    border: 1px solid var(--so-bw-primary);
    border-radius: inherit;
    pointer-events: none;
    z-index: 4;
}

/* ── 6 · Gutters inside every clipping box ───────────────────────────────

   Room for the ring is only half of it: even with the frame inside the card,
   a card whose edge is flush against the scroll port (and, on the right,
   against the scrollbar) reads as cramped and half-cropped.

   The padding widens the CLIPPER; the matching negative margin pulls the box
   back out so the cards' left edge still lines up with the search field and
   the step heading above them. Without that compensation the whole list would
   step inward by the gutter and trade one alignment problem for another. The
   right gutter is larger because the scrollbar is painted in it.

   ── …and BOTH margins have to match their own padding ───────────────────
   The right gutter being the larger one is the point; the pull-out being the
   SAME on both sides was the bug. Left compensated 8 against 8 and came out
   flush, right compensated 8 against 12 and came out 4px short — so a card
   row's right edge sat 4px inside the step heading above it while its left
   edge lined up perfectly. MEASURED at 420: heading right inset 16, last card
   20; the same 4px at every width, on every architecture that uses one of
   these clipping boxes.

   The comment above already promised this ("the matching negative margin
   pulls the box back out"). It just was not matching on the right.

   It is worse than 4px wherever the list is CENTRED inside this box rather
   than stretched across it: an asymmetric content box moves its own midpoint,
   so a `margin-inline: auto` grid centres on a line 2px left of the one the
   step's other content centres on. That is what put Centred's service list 2px
   off the column its form, review and confirmation share — on the one template
   whose stated identity is that the left edge never moves. */
#so-booking-widget[data-template] .so-bw-scroll-wrap,
#so-booking-widget .so-bw-scroll-wrap,
#so-booking-widget[data-template] .so-bw-card-grid--scroll,
#so-booking-widget .so-bw-card-grid--scroll {
    padding: 4px 12px 6px 8px;
    margin-left: -8px;
    margin-right: -12px;
}
/* Same mirroring for the slot scroller: 10px of right padding needs 10px of
   pull-back, not 6px, or the time chips sit 4px inside the panel that frames
   them exactly as the cards did. */
#so-booking-widget[data-template] .so-bw-slot-scroll,
#so-booking-widget .so-bw-slot-scroll {
    padding: 4px 10px 6px 6px;
    margin-left: -6px;
    margin-right: -10px;
}
/* Phones have less width to give away, and the body is already the scroll
   region on several architectures. Halve the gutter rather than drop it. */
/* was @media (max-width: 480px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    /* Halved gutter, and the SAME mirroring the full-size rule needs: 8px of
       right padding wants 8px of pull-back, not 4px. The phone tier carried an
       identical copy of the slip, so a 390px embed still had its cards sitting
       4px inside the search field after the desktop rule was corrected. */
    #so-booking-widget:where([data-cw~="480"])[data-template] .so-bw-scroll-wrap,
    #so-booking-widget:where([data-cw~="480"]) .so-bw-scroll-wrap,
    #so-booking-widget:where([data-cw~="480"])[data-template] .so-bw-card-grid--scroll,
    #so-booking-widget:where([data-cw~="480"]) .so-bw-card-grid--scroll{
        padding: 4px 8px 6px 4px;
        margin-left: -4px;
        margin-right: -8px;
    }


/* ── 7 · Space between the cards ─────────────────────────────────────────

   12px base, and as little as 4px on the dense templates, put two selected
   cards' accent edges 4px apart — close enough that a row of them read as one
   block rather than three chosen things. Widen it.

   Three declarations at the same specificity, narrowing in source order: the
   floor, then the dense family, then classic. The dense family keeps a smaller
   gap than the rest so it stays recognisably dense — the point is separation,
   not uniformity — and classic is put back to zero because it renders a JOINED
   list (shared borders, the register of a paper form), where a gap does not
   loosen the design, it dismantles it. */
#so-booking-widget[data-template] .so-bw-card-grid,
#so-booking-widget .so-bw-card-grid {
    gap: 14px !important;
}
/* Tabbed Rail joins the dense family rather than restating 10px in section
   17.4: the floor above is `!important`, so a gap declared out there loses
   whatever it says, and a second 10px in a second place is a second thing to
   keep in step. It qualifies on the same grounds as the rest of the list — its
   pane is two thirds of a 1040px shell because the rail has the other third,
   so its tiles are the narrowest in the catalogue. */
#so-booking-widget[data-template="compact"] .so-bw-card-grid,
#so-booking-widget[data-template="minimal"] .so-bw-card-grid,
#so-booking-widget[data-template="studio"] .so-bw-card-grid,
#so-booking-widget[data-template="dark"] .so-bw-card-grid,
#so-booking-widget[data-template="gradient"] .so-bw-card-grid,
#so-booking-widget[data-template="tabbed"] .so-bw-card-grid,
#so-booking-widget[data-template="wheel"] .so-bw-card-grid {
    gap: 10px !important;
}
#so-booking-widget[data-template="classic"] .so-bw-card-grid {
    gap: 0 !important;
}
/* ── ...and Agenda, for exactly the reason classic is exempt ──────────────
   REPORTED on Agenda's service step: "the service boxes are not fixed
   properly — no height, no border".

   Agenda's list (17.6 · C11) is the other JOINED list in the catalogue. Its
   rows carry `border: 0; background: transparent` and draw the separator as
   the row's OWN `border-bottom`, with a matching `border-top` on the grid, so
   the hairlines ARE the list — there is no card outline to space apart.

   The `gap: 14px !important` floor above is (1,1,0)+!important and C11's
   `gap: 0` is (1,1,0) plain, so the floor won and put 14px of blank between
   every hairline and the row it was meant to introduce. Measured at 1280px:
   a 72px row, then its hairline, then 14px of nothing, then the next row —
   so each line sat 14px from the row BELOW it and hard against the row above.
   Rows stopped reading as a list of equal-height items and started reading as
   detached rectangles with a stray rule under each, which is the report:
   nothing is "fixed" to anything, and the separators look like they belong to
   the wrong row. The selected row's 3px accent bar floated the same way.

   Zero, and the hairlines close the rows back up. Stated with !important
   because only that beats the floor; stated HERE, next to classic, because
   this is the block that decides card spacing for the whole catalogue and the
   exception has to be visible from it.

   The other five joined lists — Dialog, Inline, Timeline, Editorial and Wide
   Canvas — declare `gap: 0` plain and lose the same argument. They are left
   alone deliberately: this fix is scoped to the template that was reported. */
#so-booking-widget[data-template="agenda"] .so-bw-card-grid {
    gap: 0 !important;
}

/* ==========================================================================
   16. PILOT PRIMITIVES  —  four templates, whole journey
   --------------------------------------------------------------------------
   Sections 13–15 differentiate the templates at ONE step (choosing a date) and
   leave the other eight sharing a single card grid, a single form, a single
   review and a single confirmation. This section is the first instalment of
   the combinatorial redesign that fixes that, built for four pilot templates
   so the direction can be reviewed before the remaining fourteen follow.

   (Conversation was a fifth pilot. Its chips, day rail, replies-as-fields
   and soft recap went with the template when it was withdrawn from the
   catalogue. The one primitive that outlived it is D9's suggested-times
   chip, which Chat Booking reuses — see that block below.)

     Editorial      index · time index · label column · order of service · dateline
     Hero           photo-led cards · time gallery · stacked · card · celebration
     Week Board     grid · week board · two column · line items · calendar
     Wide Canvas    table · month + dense times · two column · two pane · mark

   -- The rule this section is built on -------------------------------------

       A PRIMITIVE owns its layout.
       A TEMPLATE selects primitives, and owns palette and character.
       A TEMPLATE never restyles a primitive's internals.

   -- Three things this section must not break ------------------------------

   1. THE DESCRIPTION AND ITS "READ MORE".
      A service card shows a two-line preview and, when the full text is
      longer, a Read more that opens it. `.so-bw-card-meta--clamp` reserves
      both lines so cards stay level, and `.so-bw-card-more--ghost` keeps the
      button's space on cards whose text is short. That mechanism is
      deliberate and it is NOT a per-template decision — every card-based
      primitive here keeps it.

      An earlier draft hid it on Hero, reasoning that a description does not
      survive over a photograph. Two things were wrong with that: the clamp
      rule is declared `!important` (it has to be, to beat the never-truncate
      block), so the description rendered anyway; and the selectors ended up
      merged into the badge rule, so every description inherited the badge's
      translucent fill and blur. The visible result was a grey box behind the
      text of every card. The lesson is in the C3 block below: a photo-led
      card leads with the photograph, it does not print over it.

   2. THE TIME STEP MUST DIFFER TOO.
      Picking a DAY was already differentiated (month, week board, agenda,
      suggested…). Picking a TIME was not: every template fell through to the
      same pill list, so three of the five pilots looked identical the moment
      a date was chosen. Each pilot now has its own slot treatment — see the
      four `· TIMES` blocks.

   3. NOTHING OUTSIDE THE PILOTS.
      Every selector here names editorial / hero / weekboard / wide.
      Assignment for the other fourteen waits for review.

   4. `.so-bw-card` IS NOT ONLY A CARD.
      The staff-profile dialog's panel is markup `<div class="so-bw-card
      so-bw-sp__panel">` — deliberately, so it inherits whatever surface the
      active template gives a card. Every bare `.so-bw-card` rule in this file
      therefore carries `:not(.so-bw-sp__panel)`, and the rules in this section
      must too.

      This is not hypothetical. C7 below turns a card into a pill with
      `border-radius: 999px`, and without the guard that reached the dialog:
      a roughly square panel with a 999px radius is a CIRCLE, and the profile
      opened as a disc with its own Close and Select Staff buttons clipped off
      the curve. Shipped and reported from a real install.

      If a rule is about the choice cards, say so — the guard costs nothing
      and the failure it prevents is total.
   ========================================================================== */

/* ──────────────────────────────────────────────────────────────────────────
   D9 · SUGGESTED FIRST                                  Chat Booking
   Real openings answer "when suits?" directly, as chips rather than a grid.

   Written for Conversation, which opened its date step this way. That
   template is withdrawn, and Chat Booking is now the only consumer: section
   36 reveals `.so-bw-chatalt .so-bw-suggest` when the chosen day is full and
   offers the nearest openings instead of a dead end. The chip itself is
   unchanged, which is the point of it living out here rather than inside
   either template's block.

   Hidden by default, so a layout that has not opted in renders nothing.
   ────────────────────────────────────────────────────────────────────────── */
#so-booking-widget .so-bw-suggest { display: none; }
#so-booking-widget .so-bw-suggest-opt {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    width: 100%;
    min-height: 56px;
    padding: 12px 16px;
    border: 1px solid var(--so-bw-border);
    border-radius: var(--so-bw-ds-radius-card);
    background: var(--so-bw-card-bg);
    color: var(--so-bw-text);
    font-family: inherit;
    text-align: left;
    cursor: pointer;
    transition: all var(--so-bw-ds-motion);
}
#so-booking-widget .so-bw-suggest-opt:hover { border-color: var(--so-bw-primary); }
#so-booking-widget .so-bw-suggest-when {
    display: flex;
    flex-direction: column;
    gap: 2px;
}
#so-booking-widget .so-bw-suggest-when strong { font-size: 14px; font-weight: 600; }
#so-booking-widget .so-bw-suggest-when span {
    font-family: var(--so-bw-mono);
    font-size: 11.5px;
    opacity: .75;
}
#so-booking-widget .so-bw-suggest-time {
    font-family: var(--so-bw-mono);
    font-variant-numeric: tabular-nums;
    font-size: 17px;
    font-weight: 600;
    white-space: nowrap;
    flex: 0 0 auto;
}

/* ──────────────────────────────────────────────────────────────────────────
   C2 · INDEX                                            Editorial
   Rules, not boxes: a list of choices reads as an index. This treatment was
   written years ago against `.so-bw-service-card` / `-grid` — classes the
   widget has never rendered — so Editorial's choice steps always fell back to
   the standard card grid. Retargeted onto the live classes.

   The description and its Read more stay exactly as they are elsewhere: an
   index line is still a service, and two lines plus "Read more" is what the
   catalogue promises.
   ────────────────────────────────────────────────────────────────────────── */
#so-booking-widget[data-template="editorial"] .so-bw-card-grid {
    grid-template-columns: 1fr;
    gap: 0;
}
#so-booking-widget[data-template="editorial"] .so-bw-card:not(.so-bw-sp__panel) {
    border: 0;
    border-bottom: 1px solid var(--so-bw-border);
    border-radius: 0;
    background: none;
    padding: 16px 2px;
    box-shadow: none;
}
/* The same opt-out C7 documents in full. Editorial puts the row's padding on
   the CARD (it has no boxed body to carry it), so `padding: 0 !important` on
   .so-bw-card--service collapsed the index to a wall of touching lines.
   Only the padding needs restating here: Editorial hides the media tile, so
   the forced column direction has nothing to stack. */
#so-booking-widget[data-template="editorial"] .so-bw-card--service:not(.so-bw-sp__panel) {
    padding: 16px 2px !important;
}
#so-booking-widget[data-template="editorial"] .so-bw-card:hover {
    background: color-mix(in srgb, var(--so-bw-primary) 4%, transparent);
}
/* `background: none` used to sit here and never applied — see the note in C7
   above: selection is painted by a (1,3,0) !important rule and a per-template
   background at (1,2,0) cannot reach it. Editorial keeps the platform's 15%
   wash like everything else, and marks the chosen line the way a printed
   listing would, with a rule down its left edge. */
#so-booking-widget[data-template="editorial"] .so-bw-card--selected {
    border-bottom-color: var(--so-bw-primary);
    box-shadow: inset 3px 0 0 var(--so-bw-primary);
    padding-left: 14px;
}
#so-booking-widget[data-template="editorial"] .so-bw-card-media { display: none; }
#so-booking-widget[data-template="editorial"] .so-bw-card-title {
    font-family: 'Newsreader', Georgia, serif;
    font-size: 19px;
    font-weight: 500;
    letter-spacing: -.01em;
}
#so-booking-widget[data-template="editorial"] .so-bw-card-meta { font-size: 13px; }
#so-booking-widget[data-template="editorial"] .so-bw-card-badges { gap: 12px; }
#so-booking-widget[data-template="editorial"] .so-bw-badge {
    background: none;
    border: 0;
    padding: 0;
    font-family: var(--so-bw-mono);
    font-size: 11px;
    color: var(--so-bw-text-secondary);
}

/* ── Editorial · DATE & TIME ─────────────────────────────────────────────
   THE DIARY PAGE.  Set, not drawn.

   Every other template in this file draws the month: a box, seven columns of
   boxes, a filled box for the day you chose. Editorial sets it. The whole
   apparatus of cell borders comes off and the month becomes a page of type
   held together by horizontal rules — the masthead rule under the month name,
   a hairline under each week, and nothing vertical anywhere. Numerals hang to
   the top-right of their cell the way a printed diary sets a date.

   The typography is the identity here, and it is doing the differentiating
   that colour alone was doing before: Newsreader for every numeral, and the
   weekday header in letterspaced mono capitals at 9.5px — a caption, not a
   table heading.
   ────────────────────────────────────────────────────────────────────────── */
#so-booking-widget[data-template="editorial"] .so-bw-calendar {
    border: 0;
    background: none;
    padding: 0;
}
/* The masthead rule. Heavier than every other line on the page, so the month
   name reads as the title of what follows rather than as a row of controls. */
#so-booking-widget[data-template="editorial"] .so-bw-calendar-header {
    padding-bottom: 8px;
    margin-bottom: 0;
    border-bottom: 2px solid var(--so-bw-text);
}
#so-booking-widget[data-template="editorial"] .so-bw-calendar-month {
    order: -1;
    flex: 1 1 auto;
    text-align: left;
    font-family: 'Newsreader', Georgia, serif;
    font-size: clamp(19px, calc(var(--so-bw-cw, 100vw) * 0.023), 25px);
    font-weight: 500;
    letter-spacing: -.015em;
}
/* Square, ruled, sitting as a pair at the right of the masthead — the arrows
   of a printed page, not the round chrome of an app. */
#so-booking-widget[data-template="editorial"] .so-bw-calendar-nav {
    border: 1px solid var(--so-bw-border);
    border-radius: 0;
}
#so-booking-widget[data-template="editorial"] .so-bw-calendar-weekdays {
    border: 0;
    border-radius: 0;
}
#so-booking-widget[data-template="editorial"] .so-bw-calendar-weekdays span {
    border-right: 0;
    background: none;
    padding: 9px 0 7px;
    font-family: var(--so-bw-mono);
    font-size: 9.5px;
    font-weight: 500;
    letter-spacing: .14em;
    text-transform: uppercase;
}
#so-booking-widget[data-template="editorial"] .so-bw-calendar-grid {
    border: 0;
    border-radius: 0;
    overflow: visible;
}
/* No vertical rules, no box: a week is one line of type with a hairline under
   it. The numeral hangs top-right, where a diary sets it. */
#so-booking-widget[data-template="editorial"] .so-bw-calendar-day {
    align-items: flex-start;
    justify-content: flex-end;
    aspect-ratio: auto;
    min-height: 44px;
    padding: 7px 7px 0 0;
    border: 0;
    border-bottom: 1px solid var(--so-bw-ds-n100);
    border-radius: 0;
    background: none;
    font-family: 'Newsreader', Georgia, serif;
    font-size: 17px;
    font-weight: 500;
}
#so-booking-widget[data-template="editorial"] .so-bw-calendar-day:nth-child(7n) {
    border-right: 0;
}
/* An available day is marked by a rule under its numeral, the way a printed
   listing marks a live entry — not by a tint, which this page has none of. */
#so-booking-widget[data-template="editorial"] .so-bw-calendar-day--available .so-bw-calendar-day-num {
    box-shadow: 0 2px 0 var(--so-bw-primary);
}
#so-booking-widget[data-template="editorial"] .so-bw-calendar-day--available:hover {
    background: var(--so-bw-ds-n25);
}
#so-booking-widget[data-template="editorial"] .so-bw-calendar-day--other-month,
#so-booking-widget[data-template="editorial"] .so-bw-calendar-day--disabled {
    background: none;
}
#so-booking-widget[data-template="editorial"] .so-bw-calendar-day--holiday {
    background: none;
    color: #e11d48;
}
/* Selected is a block of ink, set hard against the rules — the only solid on
   the page, so it cannot be mistaken for anything else. */
#so-booking-widget[data-template="editorial"] .so-bw-calendar-day--selected,
#so-booking-widget[data-template="editorial"] .so-bw-calendar-day--selected:hover {
    align-items: center;
    justify-content: center;
    padding: 0;
}
#so-booking-widget[data-template="editorial"] .so-bw-calendar-day--selected .so-bw-calendar-day-num {
    box-shadow: none;
}

/* ── Editorial · TIMES ───────────────────────────────────────────────────
   The day set as a column of type. Times are the leading figure of each line
   with a hairline under it — a listing, not a keypad. */
#so-booking-widget[data-template="editorial"] .so-bw-slot-panel {
    border: 0;
    background: none;
    padding: 0;
}
#so-booking-widget[data-template="editorial"] .so-bw-slot-list {
    display: block;
}
#so-booking-widget[data-template="editorial"] .so-bw-slot-row {
    width: 100%;
    justify-content: flex-start;
    gap: 14px;
    padding: 13px 4px;
    border: 0;
    border-bottom: 1px solid var(--so-bw-ds-n100);
    border-radius: 0;
    background: none;
    font-family: 'Newsreader', Georgia, serif;
    font-size: 18px;
    font-weight: 500;
}
#so-booking-widget[data-template="editorial"] .so-bw-slot-row--selected {
    background: none;
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
    box-shadow: inset 3px 0 0 var(--so-bw-primary);
    padding-left: 14px;
}
#so-booking-widget[data-template="editorial"] .so-bw-slot-group-title {
    margin: 18px 0 2px;
    padding-bottom: 6px;
    border-bottom: 1.5px solid var(--so-bw-primary);
    color: var(--so-bw-text);
}


/* ──────────────────────────────────────────────────────────────────────────
   F6 · LABEL COLUMN  /  R7 · ORDER OF SERVICE  /  N7 · DATELINE   Editorial
   ────────────────────────────────────────────────────────────────────────── */
#so-booking-widget[data-template="editorial"] .so-bw-form { display: block; }
#so-booking-widget[data-template="editorial"] .so-bw-form-group {
    display: grid;
    grid-template-columns: 148px minmax(0, 1fr);
    align-items: baseline;
    gap: 8px 20px;
    padding: 14px 0;
    border-bottom: 1px solid var(--so-bw-ds-n100);
}
#so-booking-widget[data-template="editorial"] .so-bw-label {
    font-family: 'Newsreader', Georgia, serif;
    font-size: 15px;
    font-weight: 500;
    text-align: right;
    margin: 0;
}
#so-booking-widget[data-template="editorial"] .so-bw-input,
#so-booking-widget[data-template="editorial"] .so-bw-select,
#so-booking-widget[data-template="editorial"] .so-bw-textarea {
    border-width: 0 0 1px 0;
    border-radius: 0;
    background: none;
    padding-left: 0;
    padding-right: 0;
}
#so-booking-widget[data-template="editorial"] .so-bw-input:focus,
#so-booking-widget[data-template="editorial"] .so-bw-select:focus,
#so-booking-widget[data-template="editorial"] .so-bw-textarea:focus {
    box-shadow: none;
    border-bottom-color: var(--so-bw-primary);
}
/* BOOKING BLOCKER — the First Name field was 0px wide.
   Found by walking the flow with agent-browser: Editorial was the one template
   of nineteen that never reached Review, because Continue stayed disabled and
   a REQUIRED field could not be filled. Measured: so-cust-fname req=true
   w=0 h=44, so-cust-lname w=280.

   `.so-bw-form-row` is not a label-and-field pair. It is a container holding
   TWO `.so-bw-form-group`s side by side — First Name and Last Name — and the
   base sets it `1fr 1fr` for exactly that reason. This block redefined it as
   `148px minmax(0, 1fr)`, the same shape used for a group's label column, so:

     · the First Name GROUP was placed in the 148px track;
     · that group is itself `148px minmax(0, 1fr)`, so its own label consumed
       the whole 148px and its input resolved to zero;
     · the Last Name group took the 1fr track and looked perfectly fine, which
       is why a glance at the form showed one field and read as a design
       choice rather than a fault.

   The row becomes a single column, so First Name and Last Name each get a
   full-width row with the same right-aligned 148px label as Email, Phone and
   Notes. That is what this template's form already is everywhere else — an
   index of labelled rows — and it matches what its own <=640px tier has
   always done. Nothing here is new design; the side-by-side pair was simply
   never viable inside a 148px label column.

   The layout audit could not catch this: its visibility filter requires
   width > 0, so a zero-width control was treated as "not rendered" and
   skipped by every check including the tap-target floor. That gap is now
   closed by the zero-size check in pilot-layout-audit.mjs. */
#so-booking-widget[data-template="editorial"] .so-bw-form-row {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: 0;
    align-items: baseline;
}
#so-booking-widget[data-template="editorial"] .so-bw-summary {
    border: 0;
    background: none;
    padding: 0;
    counter-reset: so-bw-order;
}
#so-booking-widget[data-template="editorial"] .so-bw-summary-section {
    border: 0;
    padding: 0;
}
#so-booking-widget[data-template="editorial"] .so-bw-summary-section-title {
    font-family: 'Newsreader', Georgia, serif;
    font-size: 17px;
    font-weight: 500;
    text-transform: none;
    letter-spacing: -.01em;
    color: var(--so-bw-text);
    border-bottom: 1.5px solid var(--so-bw-primary);
    padding-bottom: 6px;
    margin-bottom: 4px;
}
#so-booking-widget[data-template="editorial"] .so-bw-summary-row {
    counter-increment: so-bw-order;
    display: grid;
    grid-template-columns: 22px minmax(0, 1fr) auto;
    align-items: baseline;
    gap: 12px;
    padding: var(--so-bw-row-pad-y) 0;
    border-bottom: 1px solid var(--so-bw-ds-n100);
}
#so-booking-widget[data-template="editorial"] .so-bw-summary-row::before {
    content: counter(so-bw-order, decimal-leading-zero);
    font-family: var(--so-bw-mono);
    font-size: 10.5px;
    letter-spacing: .06em;
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}
#so-booking-widget[data-template="editorial"] .so-bw-summary-label {
    font-size: 14px;
    color: var(--so-bw-text-secondary);
}
#so-booking-widget[data-template="editorial"] .so-bw-summary-value {
    font-family: 'Newsreader', Georgia, serif;
    font-size: 16px;
    text-align: right;
}
#so-booking-widget[data-template="editorial"] .so-bw-summary-row--total {
    border-bottom: 0;
    border-top: 1.5px solid var(--so-bw-primary);
    margin-top: 4px;
    padding-top: 14px;
}
#so-booking-widget[data-template="editorial"] .so-bw-summary-row--total .so-bw-summary-value {
    font-size: 22px;
}
#so-booking-widget[data-template="editorial"] .so-bw-summary-row--wide {
    grid-template-columns: 1fr;
}
#so-booking-widget[data-template="editorial"] .so-bw-summary-row--wide::before { content: none; }
#so-booking-widget[data-template="editorial"] .so-bw-confirmation { text-align: left; }
#so-booking-widget[data-template="editorial"] .so-bw-confirmation-icon { display: none; }
#so-booking-widget[data-template="editorial"] .so-bw-confirmation-title {
    font-family: 'Newsreader', Georgia, serif;
    font-size: clamp(28px, calc(var(--so-bw-cw, 100vw) * 0.05), 40px);
    font-weight: 500;
    line-height: 1.08;
    letter-spacing: -.02em;
    padding-bottom: 14px;
    border-bottom: 2px solid var(--so-bw-primary);
}
#so-booking-widget[data-template="editorial"] .so-bw-confirmation-text {
    font-family: var(--so-bw-mono);
    font-size: 11px;
    letter-spacing: .16em;
    text-transform: uppercase;
    color: var(--so-bw-text-secondary);
    margin-top: 12px;
}
#so-booking-widget[data-template="editorial"] .so-bw-confirmation-details {
    border: 0;
    border-top: 1px solid var(--so-bw-ds-n100);
    border-radius: 0;
    background: none;
    padding: 14px 0 0;
}
#so-booking-widget[data-template="editorial"] .so-bw-confirmation-detail-value {
    font-family: 'Newsreader', Georgia, serif;
    font-size: 16px;
}


/* ──────────────────────────────────────────────────────────────────────────
   C3 · PHOTO-LED CARD                                   Hero
   Imagery leads the choice: the media band takes the top of the card and is
   the first thing read. Everything below it sits on the card's own surface.

   NOT an overlay. The first draft made the photograph the whole card and
   printed the type over a scrim, which (a) fought the description clamp,
   (b) put the placeholder avatar behind the text on any service without a
   photo, and (c) meant a catalogue where only some services have imagery
   rendered as two different designs side by side. Leading with the band gets
   the same "brand first" reading and keeps the text legible on every card.
   ────────────────────────────────────────────────────────────────────────── */
#so-booking-widget[data-template="hero"] .so-bw-card-grid {
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr));
    gap: 14px;
}
#so-booking-widget[data-template="hero"] .so-bw-card:not(.so-bw-sp__panel) {
    padding: 0;
    overflow: hidden;
}
/* …BUT `padding: 0` is for the PHOTO-FIRST card, and only the service card is
   one. It exists so the media strip bleeds to the card edge; the location and
   staff cards carry no media at all — they are an avatar beside text — and on
   those the same rule left the avatar jammed against the card border while the
   text kept the 12/14 inset from .so-bw-card-body. Measured on the live
   install: card padding 0, body padding 12px 14px 14px.
   The inset moves to the CARD, so icon and text share one edge.

   The second half is the vertical story. The card is a flex row inheriting
   `align-items: center`, and these grids level every card to the tallest, so
   the avatar was centred against a card height it had nothing to do with
   while the title sat at the top. Locations measured: icon top 80 on every
   card, title top 22 / 22 / 89 — the pin landed beside the name on the short
   card and beside the middle of the address on the long ones, which is the
   raggedness in the report. Aligning to the start puts every avatar next to
   its name, whatever the card was stretched to. */
#so-booking-widget[data-template="hero"] .so-bw-card:not(.so-bw-sp__panel):not(.so-bw-card--service) {
    padding: 12px 14px;
    align-items: flex-start;
}
#so-booking-widget[data-template="hero"] .so-bw-card:not(.so-bw-sp__panel):not(.so-bw-card--service) .so-bw-card-body {
    padding: 0;
}
/* The tick is a selection mark, not content — it stays on the optical centre
   of the row it marks rather than riding up with the title. */
#so-booking-widget[data-template="hero"] .so-bw-card:not(.so-bw-sp__panel):not(.so-bw-card--service) .so-bw-card-check {
    align-self: center;
}
/* AND EVERY ROW STOPS PAYING FOR THE TALLEST ONE.
   The shared grid sets `grid-auto-rows: 1fr`, which sizes EVERY row to the
   tallest card in the whole grid — not the tallest in its own row. With five
   locations on a four-up grid that made the second row 163px tall to hold a
   single card with 28px of content, so a name sat alone above ~130px of empty
   card. Measured: gridTemplateRows 163.375px 163.375px, content heights
   72 / 137 / 137 / 119 / 28.

   `auto` still stretches the cards WITHIN a row to match each other, which is
   the levelling that actually matters when they sit side by side; it just
   stops row two from inheriting row one's height.

   Held off the service grid, where the uniform tile IS the poster layout —
   same `:has()` shape this file already uses for Wide Canvas's extras. */
#so-booking-widget[data-template="hero"] .so-bw-card-grid:not(:has(.so-bw-card--service)) {
    grid-auto-rows: auto;
}
/* Taller than the shared 16/10 band — the photograph is the lead here, and
   the extra height is what makes this read as photo-first rather than as a
   card that happens to have a picture. */
#so-booking-widget[data-template="hero"] .so-bw-card-media {
    aspect-ratio: 3 / 2;
}
#so-booking-widget[data-template="hero"] .so-bw-card-top,
#so-booking-widget[data-template="hero"] .so-bw-card-body {
    padding: 12px 14px 14px;
}
#so-booking-widget[data-template="hero"] .so-bw-card-title { font-size: 16px; }

/* ── Hero · DATE & TIME ──────────────────────────────────────────────────
   THE POSTER MONTH.  The same scale the imagery sets.

   Hero's whole argument is that things should be big and few. A month drawn
   as a hairline table contradicts that on the one step where the customer
   looks longest, so the table is dissolved into detached TILES: no shared
   borders, a real gap between them, generous corners, and a month name set
   at display size to lead the step the way a service photograph leads the
   card.

   The tiles carry state in FILL rather than in outline, because at this size
   an outline is invisible: a bookable day is tinted with the accent, an
   unavailable one is flat, and the chosen one is solid.
   ────────────────────────────────────────────────────────────────────────── */
#so-booking-widget[data-template="hero"] .so-bw-calendar {
    border: 0;
    background: none;
    padding: 0;
}
#so-booking-widget[data-template="hero"] .so-bw-calendar-header {
    margin-bottom: 14px;
}
#so-booking-widget[data-template="hero"] .so-bw-calendar-month {
    order: -1;
    flex: 1 1 auto;
    text-align: left;
    font-size: clamp(21px, calc(var(--so-bw-cw, 100vw) * 0.032), 29px);
    font-weight: 800;
    letter-spacing: -.03em;
}
/* At poster scale a 28px round arrow reads as a speck. These are buttons. */
#so-booking-widget[data-template="hero"] .so-bw-calendar-nav {
    width: 44px;
    height: 44px;
    border: 1px solid var(--so-bw-border);
    border-radius: var(--so-bw-r-14);
    background: var(--so-bw-card-bg);
}
#so-booking-widget[data-template="hero"] .so-bw-calendar-weekdays {
    border: 0;
    border-radius: 0;
    margin-bottom: 6px;
}
#so-booking-widget[data-template="hero"] .so-bw-calendar-weekdays span {
    border-right: 0;
    background: none;
    padding: 0 0 4px;
    font-size: 10px;
    font-weight: 700;
    letter-spacing: .09em;
    text-transform: uppercase;
}
/* Detached tiles. `overflow: visible` because the base grid clips to its own
   rounded corners, which would shave the outer tiles once they have their
   own radius. */
#so-booking-widget[data-template="hero"] .so-bw-calendar-grid {
    gap: 5px;
    border: 0;
    border-radius: 0;
    overflow: visible;
}
#so-booking-widget[data-template="hero"] .so-bw-calendar-day {
    /* Square cells plus a 5px gap leaves ~34px tiles in a 300px column, which
       is under the tap floor. Sizing on height instead keeps every tile at
       least 46px tall however narrow the column gets. */
    aspect-ratio: auto;
    min-height: 46px;
    border: 0;
    border-radius: var(--so-bw-r-14);
    background: var(--so-bw-ds-n25);
    font-size: 15px;
    font-weight: 600;
}
#so-booking-widget[data-template="hero"] .so-bw-calendar-day--available {
    background: var(--so-bw-primary-light);
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}
#so-booking-widget[data-template="hero"] .so-bw-calendar-day--available:hover {
    background: var(--so-bw-primary);
    color: #fff;
}
#so-booking-widget[data-template="hero"] .so-bw-calendar-day--other-month {
    background: none;
}
#so-booking-widget[data-template="hero"] .so-bw-calendar-day--disabled {
    background: var(--so-bw-ds-n25);
}
#so-booking-widget[data-template="hero"] .so-bw-calendar-day--holiday {
    background: #fff1f2;
}
/* The dot the base rule pins 5px off the bottom lands mid-tile at this size;
   a ring on the tile itself is the same statement at the right scale. */
#so-booking-widget[data-template="hero"] .so-bw-calendar-day--today::after {
    display: none;
}
#so-booking-widget[data-template="hero"] .so-bw-calendar-day--today {
    box-shadow: inset 0 0 0 2px var(--so-bw-primary);
}

/* ── Hero · TIMES ────────────────────────────────────────────────────────
   A gallery of times: large, generously spaced, three across — the same
   unhurried scale the imagery sets, rather than a tight keypad. */
#so-booking-widget[data-template="hero"] .so-bw-slot-list {
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 118px), 1fr));
    gap: 10px;
}
#so-booking-widget[data-template="hero"] .so-bw-slot-row {
    justify-content: center;
    min-height: 54px;
    font-size: 15px;
    font-weight: 600;
    border-radius: var(--so-bw-ds-radius-card);
}
#so-booking-widget[data-template="hero"] .so-bw-slot-group-title {
    font-size: 11px;
    margin-bottom: 4px;
}


/* ──────────────────────────────────────────────────────────────────────────
   N3 · CELEBRATION                                      Hero
   ────────────────────────────────────────────────────────────────────────── */
#so-booking-widget[data-template="hero"] .so-bw-confirmation-icon {
    width: 76px;
    height: 76px;
}
#so-booking-widget[data-template="hero"] .so-bw-confirmation-title {
    font-size: clamp(26px, calc(var(--so-bw-cw, 100vw) * 0.044000000000000004), 36px);
    line-height: 1.1;
    letter-spacing: -.02em;
}


/* ──────────────────────────────────────────────────────────────────────────
   F2 · TWO COLUMN                                Week Board, Wide Canvas
   Desktop-dense templates should not ask for eight scrolls of single-column
   form.
   ────────────────────────────────────────────────────────────────────────── */
#so-booking-widget[data-template="weekboard"] .so-bw-form,
#so-booking-widget[data-template="wide"] .so-bw-form {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 14px 18px;
    align-items: start;
}
#so-booking-widget[data-template="weekboard"] .so-bw-form-group,
#so-booking-widget[data-template="wide"] .so-bw-form-group {
    grid-column: auto;
}
/* .so-bw-form-row is a 2-up grid spanning 1/-1 of a 12-column parent. Inside
   a 2-column parent that span is wrong twice over, so the row flattens into
   two ordinary cells of the outer grid instead of nesting a grid in a grid —
   which also makes the one-column fallback below need no reset. */
#so-booking-widget[data-template="weekboard"] .so-bw-form-row,
#so-booking-widget[data-template="wide"] .so-bw-form-row {
    display: contents;
}
#so-booking-widget[data-template="weekboard"] .so-bw-form-group:has(.so-bw-textarea),
#so-booking-widget[data-template="wide"] .so-bw-form-group:has(.so-bw-textarea) {
    grid-column: 1 / -1;
}
/* was @media (max-width:700px) — now gated on the CONTAINER width
   via data-cw (see booking-widget.js _wireContainerWidth). :where()
   keeps specificity identical to the media query it replaces. */

    #so-booking-widget:where([data-cw~="700"])[data-template="weekboard"] .so-bw-form,
    #so-booking-widget:where([data-cw~="700"])[data-template="wide"] .so-bw-form{
        grid-template-columns: 1fr;
    }



/* ──────────────────────────────────────────────────────────────────────────
   Week Board · DATE & TIME                              Week Board
   THE TIMETABLE.

   The board is the one date treatment that was already structurally its own
   (seven columns of real times, so day and time are ONE decision). What it
   was not was its own to LOOK at: seven bare columns of pills on a white
   field read as a fragment of some other template's slot list.

   A timetable has ruled columns and a headed top rail, so this gives it both:
   each day is a panel with a tinted head, the columns are separated by real
   rules, and the whole board sits in a frame. Today's column is picked out by
   the accent on its head rather than by a badge, because on a board the
   column IS the unit.
   ────────────────────────────────────────────────────────────────────────── */
#so-booking-widget[data-template="weekboard"] .so-bw-week-pager {
    padding-bottom: 10px;
    border-bottom: 2px solid var(--so-bw-text);
}
#so-booking-widget[data-template="weekboard"] .so-bw-week-range {
    font-size: 14px;
    letter-spacing: .01em;
}
#so-booking-widget[data-template="weekboard"] .so-bw-week {
    gap: 0;
    margin-top: 14px;
    border: 1px solid var(--so-bw-border);
    border-radius: var(--so-bw-ds-radius-card);
    overflow: hidden;
}
/* The rule between days. Applied as a left border on every column but the
   first, so seven columns produce six rules and the frame supplies the
   outer two — no doubling at the edges. */
#so-booking-widget[data-template="weekboard"] .so-bw-week-col {
    gap: 5px;
    padding: 0 6px 10px;
    border-left: 1px solid var(--so-bw-ds-n100);
}
#so-booking-widget[data-template="weekboard"] .so-bw-week-col:first-child {
    border-left: 0;
}
#so-booking-widget[data-template="weekboard"] .so-bw-week-head {
    margin: 0 -6px 4px;
    padding: 9px 4px;
    background: var(--so-bw-ds-n25);
    border-bottom: 1px solid var(--so-bw-ds-n100);
}
#so-booking-widget[data-template="weekboard"] .so-bw-week-col--active .so-bw-week-head {
    background: var(--so-bw-primary-light);
    border-bottom-color: var(--so-bw-primary);
}
#so-booking-widget[data-template="weekboard"] .so-bw-week-col--active .so-bw-week-num {
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}
#so-booking-widget[data-template="weekboard"] .so-bw-week-dow {
    font-size: 10.5px;
    letter-spacing: .08em;
    text-transform: uppercase;
}
#so-booking-widget[data-template="weekboard"] .so-bw-week-num {
    font-size: 17px;
}
/* Below the collapse the columns become stacked sections, where a frame and
   six vertical rules describe a shape that is no longer there. */
/* was @media (max-width: 640px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    #so-booking-widget:where([data-cw~="640"])[data-template="weekboard"] .so-bw-week{
        border: 0;
        border-radius: 0;
        overflow: visible;
        margin-top: 12px;
    }
    #so-booking-widget:where([data-cw~="640"])[data-template="weekboard"] .so-bw-week-col{
        border-left: 0;
        padding: 0 0 12px;
    }
    #so-booking-widget:where([data-cw~="640"])[data-template="weekboard"] .so-bw-week-head{
        margin: 0 0 2px;
        padding: 0;
        background: none;
        border-bottom: 0;
    }
    #so-booking-widget:where([data-cw~="640"])[data-template="weekboard"] .so-bw-week-col--active .so-bw-week-head{
        background: none;
    }


/* The panel below the board carries the chosen day in full — the board caps
   at eight times per column, so "+N more" has to lead somewhere real. Kept as
   a compact strip so the two read as one schedule rather than as a board
   followed by an unrelated list. */
#so-booking-widget[data-template="weekboard"] .so-bw-slot-panel {
    margin-top: 18px;
    border-top: 1px solid var(--so-bw-border);
    border-radius: 0;
    background: none;
    padding-top: 14px;
}
#so-booking-widget[data-template="weekboard"] .so-bw-slot-list {
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 88px), 1fr));
    gap: 6px;
}
#so-booking-widget[data-template="weekboard"] .so-bw-slot-row {
    justify-content: center;
    min-height: 40px;
    padding: 8px 6px;
    font-size: 12.5px;
    border-radius: var(--so-bw-ds-radius-chip);
}


/* ──────────────────────────────────────────────────────────────────────────
   R3 · LINE ITEMS  /  N5 · CALENDAR CARD                Week Board
   ────────────────────────────────────────────────────────────────────────── */
#so-booking-widget[data-template="weekboard"] .so-bw-summary {
    border: 1px solid var(--so-bw-border);
    border-radius: var(--so-bw-ds-radius-card);
    overflow: hidden;
    padding: 0;
}
#so-booking-widget[data-template="weekboard"] .so-bw-summary-section {
    border: 0;
    padding: 0;
}
#so-booking-widget[data-template="weekboard"] .so-bw-summary-section-title {
    margin: 0;
    padding: 10px 14px;
    background: var(--so-bw-ds-n25);
    border-bottom: 1px solid var(--so-bw-border);
}
#so-booking-widget[data-template="weekboard"] .so-bw-summary-row {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, auto);
    gap: 14px;
    align-items: baseline;
    padding: 12px 14px;
    border-bottom: 1px solid var(--so-bw-ds-n100);
}
#so-booking-widget[data-template="weekboard"] .so-bw-summary-row:last-child { border-bottom: 0; }
#so-booking-widget[data-template="weekboard"] .so-bw-summary-value {
    font-family: var(--so-bw-mono);
    font-variant-numeric: tabular-nums;
    text-align: right;
}
#so-booking-widget[data-template="weekboard"] .so-bw-summary-row--total {
    background: var(--so-bw-ds-n25);
    border-top: 1px solid var(--so-bw-border);
}
#so-booking-widget[data-template="weekboard"] .so-bw-summary-row--wide {
    grid-template-columns: 1fr;
}
#so-booking-widget[data-template="weekboard"] .so-bw-confirmation-details {
    max-width: 360px;
    margin-inline: auto;
    border: 1px solid var(--so-bw-border);
    border-top: 5px solid var(--so-bw-primary);
    border-radius: var(--so-bw-ds-radius-card);
    overflow: hidden;
    padding: 0;
}
#so-booking-widget[data-template="weekboard"] .so-bw-confirmation-detail {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, auto);
    gap: 12px;
    padding: 11px 14px;
    border-bottom: 1px solid var(--so-bw-ds-n100);
}
#so-booking-widget[data-template="weekboard"] .so-bw-confirmation-detail:last-child { border-bottom: 0; }
#so-booking-widget[data-template="weekboard"] .so-bw-confirmation-detail-value {
    font-family: var(--so-bw-mono);
    font-variant-numeric: tabular-nums;
    text-align: right;
}


/* ──────────────────────────────────────────────────────────────────────────
   C9 · COMPARISON TABLE                                 Wide Canvas
   The densest choice step in the catalogue: service against duration and
   price, scannable in one pass. Built on the card grid rather than a <table>
   because the markup is shared with eighteen other templates.

   The description clamps to ONE line here rather than two — a table row is a
   comparison, not a reading experience — and Read more still opens the full
   text, so nothing is lost.
   ────────────────────────────────────────────────────────────────────────── */
#so-booking-widget[data-template="wide"] .so-bw-card-grid {
    grid-template-columns: 1fr;
    gap: 0;
    border: 1px solid var(--so-bw-border);
    border-radius: var(--so-bw-ds-radius-card);
    overflow: hidden;
}
/* ...BUT `overflow: hidden` above is a table-frame clipper, and the staff and
   location lists are ALSO the step's scroll container (.so-bw-card-grid--scroll,
   capped at min(430px, 52vh) further up this file). Those two jobs collide on
   one element: SECTION 12c gives the scroller `overflow-y: auto`, this rule
   overwrites it with `hidden`, and the list is left capped but unscrollable.

   Both selectors score (1,2,0) — `#id[attr] .class` against `#id[attr] .class`
   — so the tie broke on source order, and this section is later. Measured on
   the live install at 1280x800: max-height 416px, content 438px, computed
   overflow-y `hidden`. The fifth staff member was 24px below a hard edge with
   no way to reach them. Reported as "not scroll the staffs".

   Restoring the axis explicitly rather than dropping the clipper: `hidden` on
   X still clips the rows to the rounded frame, `auto` on Y gives the list back
   its scrollbar. Same (1,2,0), later in source, so it wins the same tie. */
#so-booking-widget[data-template="wide"] .so-bw-card-grid--scroll {
    overflow-x: clip;
    overflow-y: visible;
    max-height: none !important;
}
/* THE THIRD LEVELLING MECHANISM, and the one that actually dominated.
   The base grid sets `grid-auto-rows: 1fr`, which makes every row as tall as
   the tallest card in the grid. That is right for a multi-column catalogue —
   cards SIDE BY SIDE have to match — and wrong for a single-column list,
   where it means one service with a long description and a "Read more" sets
   the height of every other row in the category. Measured on the live
   install: bodies 50px, cards 125px, uniformly.

   It is the third of three reservations written for the side-by-side grid
   that this template inherits and does not want, after the two-line title
   min-height and the ghost "Read more". A one-column list levels nothing,
   because there is never anything beside a row to level against.

   Selector shape matches the base rule's (1,3,0) so it does not rely on
   source order. */
#so-booking-widget[data-template="wide"] .so-bw-card-grid:not(:has(.so-bw-card--has-extras-open)) {
    grid-auto-rows: auto;
}
/* THE ROW IS BUILT ON THE DOM THAT EXISTS, not the one this rule assumed.
   A service card is:

       .so-bw-card
         .so-bw-card-media
         .so-bw-card-top
           .so-bw-card-body
             .so-bw-card-title
             .so-bw-card-meta--clamp
             .so-bw-card-more
             .so-bw-card-badges          <- a GRANDCHILD, not a sibling

   This block used to put `44px | 2.2fr | 1fr` on the CARD, which has only two
   children to place. The third track therefore stayed empty and the duration
   and price — being inside the body — stacked underneath the description,
   turning a 56px table row into a ~175px block with the price floating in the
   middle of it. Caught on the live install, not in the fixture: the fixture
   renders the same structure, but nothing was asserting that a row is a row.

   So the card is a simple two-part row (media, then everything else) and the
   TABLE is made where the columns actually live, on .so-bw-card-body.

   The !important opt-out below is the same one C7 documents in full: the base
   `.so-bw-card--service` rule pins flex-direction/align-items/padding/gap
   with !important to protect the stacked service card, so a template that
   replaces the layout has to restate all four. `flex-direction: row` is the
   one that matters most here — without it the media tile sits ABOVE the text
   and there is no row at all. */
#so-booking-widget[data-template="wide"] .so-bw-card--service:not(.so-bw-sp__panel) {
    flex-direction: row !important;
    align-items: center !important;
    padding: 12px 14px !important;
    gap: 14px !important;
}
#so-booking-widget[data-template="wide"] .so-bw-card:not(.so-bw-sp__panel) {
    display: flex;
    flex-direction: row;
    align-items: center;
    gap: 14px;
    padding: 12px 14px;
    border: 0;
    border-bottom: 1px solid var(--so-bw-ds-n100);
    border-radius: 0;
    box-shadow: none;
    min-height: 56px;
}
/* Both, because the two card shapes differ: a SERVICE card wraps its body in
   .so-bw-card-top, a STAFF card does not and puts .so-bw-card-body straight
   under the card. Whichever one is the flex child here has to size to its
   contents — the base gives .so-bw-card-body `flex: 1`, which is a flex-basis
   of ZERO, and on a 360px phone that pushed the staff name up to 84px outside
   its own card. Same trap as the Conversation chip; see C7. */
#so-booking-widget[data-template="wide"] .so-bw-card-top,
#so-booking-widget[data-template="wide"] .so-bw-card:not(.so-bw-sp__panel) > .so-bw-card-body {
    flex: 1 1 auto;
    min-width: 0;
}
/* The actual table: text on the left, duration and price held to the right
   across however many lines the text needs. */
#so-booking-widget[data-template="wide"] .so-bw-card-body {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: center;
    column-gap: 14px;
    row-gap: 2px;
}
#so-booking-widget[data-template="wide"] .so-bw-card-title,
#so-booking-widget[data-template="wide"] .so-bw-card-meta,
#so-booking-widget[data-template="wide"] .so-bw-card-more {
    grid-column: 1;
}
/* The Directions link and the distance chip used to need `grid-column: 1`
   here — placed automatically they landed in the `auto` track built for a
   service's duration and price, which sized them by an intrinsic width that
   does not wrap, and at four-across the address beside them was squeezed to
   51px and broke words down the middle. Both classes were withdrawn with
   "Find nearest", so there is nothing left to place; the rule went with
   them rather than sitting here matching no markup. */
/* The ghost is reserved space, and this template has nothing to reserve it
   for. It exists so that cards SIDE BY SIDE in a multi-column grid stay
   level when only some of them need a "Read more" — Wide Canvas is a
   single-column list, where each row is its own row and unequal heights are
   simply rows of different lengths.
   Left in, it cost every short row a 44px band of empty space (the height it
   inherited from the tap-target floor on the real button), which is what
   pushed the avatar away from the title it belongs to. */
/* BOTH classes on purpose — (1,3,0), so it does not depend on source order.
   Written as `.so-bw-card-more--ghost` alone it is (1,2,0), the same score as
   the tap-target rule further down that sets `.so-bw-card-more` to
   inline-flex with a 44px floor. The later rule then won the tie and the
   ghost came back, still reserving its 44px — which is why every short row
   kept a band of empty space under it after this rule was added. */
#so-booking-widget[data-template="wide"] .so-bw-card-more.so-bw-card-more--ghost {
    display: none;
}
/* Column two holds whatever this row's right-hand fact is: the duration and
   price on a service, "View profile" on a staff member. .so-bw-card-profile
   was previously left unassigned, so grid auto-placement dropped it into the
   next free cell — a THIRD row under the bio — which is what made the staff
   rows twice as tall as the service rows and left the link floating with
   nothing aligned to it. */
/* Pinned to the FIRST row, deliberately, not spanning.
   `grid-row: 1 / -1` reads like "span every row" and does not do it here: -1
   counts back from the end of the EXPLICIT grid, and these rows are all
   implicit (the body declares columns only), so it resolves to row 1 anyway.
   Rather than leave a rule that lies, this says row 1 — which is also the
   better alignment: the price and the "View profile" link line up with the
   NAME, the thing they belong to, instead of floating against the midpoint
   of a description whose length varies from row to row. */
#so-booking-widget[data-template="wide"] .so-bw-card-badges,
#so-booking-widget[data-template="wide"] .so-bw-card-profile {
    grid-column: 2;
    grid-row: 1;
    align-self: center;
    justify-self: end;
    margin-top: 0;
}
#so-booking-widget[data-template="wide"] .so-bw-card:last-child { border-bottom: 0; }
/* The 8% wash that used to be here never applied either (same (1,3,0)
   !important rule); the platform's 15% is what rendered, and it is fine. */
#so-booking-widget[data-template="wide"] .so-bw-card--selected {
    box-shadow: inset 3px 0 0 var(--so-bw-primary);
}
#so-booking-widget[data-template="wide"] .so-bw-card-media {
    width: 44px;
    height: 44px;
    aspect-ratio: 1;
    border-radius: var(--so-bw-ds-radius-chip);
    overflow: hidden;
}
/* The avatar is sized by design-system tokens (68px by default). Re-declare
   the TOKENS rather than forcing width/height, so the glyph and initials
   scale with the smaller tile instead of being clipped inside it. */
/* Also the BARE avatar, not only the one inside a media tile. A service card
   wraps its image in .so-bw-card-media; a staff card does not — it puts
   .so-bw-card-icon--avatar straight on the card. Scoping this to
   `.so-bw-card-media .so-bw-card-icon` therefore left every staff row with
   the full 68px token tile, which on a 360px phone took enough of the row
   that the name beside it was pushed outside the card. */
#so-booking-widget[data-template="wide"] .so-bw-card-media-avatar,
#so-booking-widget[data-template="wide"] .so-bw-card-media .so-bw-card-icon,
#so-booking-widget[data-template="wide"] .so-bw-card:not(.so-bw-sp__panel) > .so-bw-card-icon {
    --so-bw-icon-size: 44px;
    --so-bw-icon-svg-size: 22px;
    --so-bw-icon-text-size: 15px;
    flex: 0 0 auto;
}
#so-booking-widget[data-template="wide"] .so-bw-card-media-img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}
#so-booking-widget[data-template="wide"] .so-bw-card-media-nav,
#so-booking-widget[data-template="wide"] .so-bw-card-media-dots { display: none; }
#so-booking-widget[data-template="wide"] .so-bw-card-top,
#so-booking-widget[data-template="wide"] .so-bw-card-body { padding: 0; }
/* The base reserves TWO lines of title (min-height: 2 × line-height) so that
   cards sitting side by side in a multi-column grid stay level whatever their
   names are. Wide Canvas stacks its rows in one column, so there is nothing
   to level against — and the reservation showed up as a band of empty space
   above every one-line name, which is what pushed the avatar down away from
   the title it belongs to. Same reasoning as the hidden ghost below. */
#so-booking-widget[data-template="wide"] .so-bw-card-title {
    font-size: 14px;
}
/* Note the selector shape: the base reservation is written as
   `#so-booking-widget[data-template] .so-bw-card--service .so-bw-card-title`,
   which scores (1,3,0). A per-template `[data-template="wide"] .so-bw-card-title`
   is (1,2,0) and loses silently — this has to name --service too, and then
   later source order carries it. */
#so-booking-widget[data-template="wide"] .so-bw-card--service .so-bw-card-title {
    -webkit-line-clamp: 1 !important;
    line-clamp: 1;
    min-height: 0;
}
#so-booking-widget[data-template="wide"] .so-bw-card-meta--clamp {
    -webkit-line-clamp: 1 !important;
    line-clamp: 1;
    min-height: 0;
    font-size: 12px;
}
#so-booking-widget[data-template="wide"] .so-bw-card-badges {
    justify-content: flex-end;
    flex-wrap: nowrap;
    gap: 10px;
}
#so-booking-widget[data-template="wide"] .so-bw-badge {
    font-family: var(--so-bw-mono);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
/* was @media (max-width:700px) — now gated on the CONTAINER width
   via data-cw (see booking-widget.js _wireContainerWidth). :where()
   keeps specificity identical to the media query it replaces. */

    /* A table row at 360px is unreadable at any type size, so below this
       width it reverts to a stacked card: media beside the text as before,
       and the duration/price dropping under the description instead of
       competing with it for a ~140px column. A genuine fallback, not a
       squeeze.

       Expressed on .so-bw-card-body, because that is where the two columns
       are — the grid-areas that used to be here named tracks on the CARD,
       which has no such tracks and never did. */
    #so-booking-widget:where([data-cw~="700"])[data-template="wide"] .so-bw-card:not(.so-bw-sp__panel){
        align-items: flex-start;
        padding: 12px;
    }
    /* KNOWN OPEN — Wide Canvas, staff step, <=430px only (7 checkpoints).
       .so-bw-card-body renders ~90px tall inside a card that stays 76px, so
       the bio and "View profile" hang below the row. Reproduce with:
         node tests/visual/pilot-layout-audit.mjs --only=wide --steps=staff \
              --devices=phone-small --probe=".so-bw-card"
       Not the avatar (now 44px here) and not a height property on the card —
       setting height:auto/min-height:0 changes nothing, so the card is not
       taking the body into its flex line at all. Every other width and every
       other template is clean, so this is scoped to the staff row's stacked
       fallback rather than to the primitive. */
    #so-booking-widget:where([data-cw~="700"])[data-template="wide"] .so-bw-card-body{
        grid-template-columns: minmax(0, 1fr);
        row-gap: 6px;
    }
    #so-booking-widget:where([data-cw~="700"])[data-template="wide"] .so-bw-card-badges{
        grid-column: 1;
        grid-row: auto;
        justify-content: flex-start;
        flex-wrap: wrap;
    }


/* ── Wide Canvas · DATE & TIME ───────────────────────────────────────────
   THE SPLIT.  The only pilot that changes the INTERACTION, not just the look.

   Every other template treats this step as two pages: the month, then — once
   a day is chosen — that day's times INSTEAD of the month, with a "Change
   date" link back. In a narrow column that is correct; both will not fit.

   Wide Canvas is the template that is given the width, and its argument is
   that nothing should be hidden behind a step. So the month stays: it holds
   the left track while the times fill the right, and changing your mind about
   the day costs one click on the day you want instead of a click on "Change
   date" and then a second click. `usesSplitDatetime` in booking-widget.js is
   what keeps the calendar mounted (see the two x-show expressions); this
   block is what gives the two tracks a shape.

   Below 900px there is no longer room for two tracks and the template stacks
   — the month above, the times below, both still on screen, so even the
   collapsed form keeps the promise that nothing is hidden.
   ────────────────────────────────────────────────────────────────────────── */
#so-booking-widget[data-template="wide"] .so-bw-datetime {
    /* Explicit tracks rather than the base `auto-fit, minmax(300px, 1fr)`:
       auto-fit gives both tracks equal width, which spends half a wide canvas
       on a month that needs ~330px and starves the times that can use
       everything they are given. */
    grid-template-columns: minmax(0, 340px) minmax(0, 1fr);
    gap: 20px;
    align-items: stretch;
}
/* TWO MATCHED PANELS, not a panel next to a loose grid.
   The month and the times are one row of one decision, so they get the same
   frame, the same radius, the same inner padding and the same top edge. The
   month previously sat unframed beside a bordered slot panel, which read as
   two unrelated things and left the calendar's content indented from the
   step's own left margin with nothing to explain the offset. */
#so-booking-widget[data-template="wide"] .so-bw-calendar {
    padding: 16px;
    border: 1px solid var(--so-bw-border);
    border-radius: var(--so-bw-ds-radius-card);
    background: var(--so-bw-card-bg);
    box-sizing: border-box;
    height: 100%;
}
#so-booking-widget[data-template="wide"] .so-bw-slot-panel {
    padding: 16px;
    border-radius: var(--so-bw-ds-radius-card);
}
/* The two headers sit on the same line: the month name reads as this panel's
   title, exactly as "Time Slot" does in the other. Same height so the grids
   beneath them start level. */
/* 62px is the timezone control's natural height (its label sits ABOVE the
   combobox by design, so that header cannot be shorter). Giving the month
   header the same floor puts both panels' rules on one line — without it the
   two headers are 44px and 62px and the grids beneath them start 18px apart,
   which is the misalignment you notice even when both panels line up. */
#so-booking-widget[data-template="wide"] .so-bw-calendar-header,
#so-booking-widget[data-template="wide"] .so-bw-slot-panel-head {
    min-height: 62px;
    margin: 0 0 14px;
    padding: 0 0 14px;
    border-bottom: 1px solid var(--so-bw-ds-n100);
    align-items: center;
}
#so-booking-widget[data-template="wide"] .so-bw-calendar-month {
    font-size: 16px;
    font-weight: 700;
}
#so-booking-widget[data-template="wide"] .so-bw-calendar-nav {
    border-radius: var(--so-bw-ds-radius-ctl);
}
/* The month is now permanent furniture rather than a page the customer passes
   through, so its cells get the room to be read at a glance. */
#so-booking-widget[data-template="wide"] .so-bw-calendar-day {
    aspect-ratio: auto;
    min-height: 42px;
    font-size: 13.5px;
}
#so-booking-widget[data-template="wide"] .so-bw-calendar-weekdays span {
    padding: 8px 0;
    font-size: 10.5px;
    letter-spacing: .04em;
    text-transform: uppercase;
}
/* The right track has to hold its own beside a 340px framed month — without a
   floor it collapses to the height of whatever the chosen day happens to
   offer, and a three-slot afternoon leaves the row visibly lopsided. */
#so-booking-widget[data-template="wide"] .so-bw-slot-panel {
    min-height: 340px;
}
/* With the calendar permanently on screen this is no longer dead markup: it
   is what the right track shows before a day is chosen, which on a split
   layout is the first thing the customer sees. Centre it in the track so it
   reads as a waiting panel rather than as an error at the top. */
#so-booking-widget[data-template="wide"] .so-bw-slot-panel-empty {
    margin: auto;
}
/* was @media (max-width:900px) — now gated on the CONTAINER width
   via data-cw (see booking-widget.js _wireContainerWidth). :where()
   keeps specificity identical to the media query it replaces. */

    #so-booking-widget:where([data-cw~="900"])[data-template="wide"] .so-bw-datetime{
        grid-template-columns: 1fr;
        gap: 14px;
    }
    #so-booking-widget:where([data-cw~="900"])[data-template="wide"] .so-bw-slot-panel{
        min-height: 0;
    }


/* ── Wide Canvas · TIMES ─────────────────────────────────────────────────
   The month never leaves, so the times share the row with it and have to be
   dense: a tight tabular grid that fits a whole day beside the calendar
   without scrolling. */
/* 76px was too tight: a full range label ("1:30 PM to 2:30 PM") wrapped to
   three lines in every cell, which is dense in the wrong way — the row got
   taller, not more scannable. 104px holds the label on two lines at 12px,
   which is what makes a whole day fit beside the month without scrolling. */
/* auto-FILL, not auto-fit. auto-fit collapses the tracks it did not use and
   lets the survivors absorb the space, so a period with a single opening —
   "Evening: 5:00 PM to 6:00 PM" — rendered as one button stretched across the
   entire panel while the Afternoon buttons above it were 104px. auto-fill
   keeps the empty tracks, so one slot is the same size as any other and the
   grid stays a grid. */
/* 152px, because that is what a full range label needs on ONE line.
   104 and 116 both held "1:30 PM to 2:30 PM" only by wrapping it, and a grid
   of two-line buttons among one-line buttons has no rhythm at all — the row
   heights change with the label. Sized to the content, then the column count
   follows. */
/* THE DEPARTURE BOARD.

   What was here before set a size and a padding and nothing else, so every
   slot was one 12px mono line reading "9:30 AM to 10:00 AM" — and since the
   group titles and the calendar numerals are mono too, the whole step was a
   single typeface at three sizes with no hierarchy at all. Twenty pills of
   identical grey text, where the only thing the customer is actually scanning
   for is the START time; "to 10:00 AM" is the same derived duration repeated
   on every row.

   So the label is split (two spans, see the markup) and set as a timetable
   cell: the start time large, tabular and heavy — the line the eye lands on —
   over a quiet second line for the window it occupies. Mono stays, because
   aligned figures ARE this template's signature and are what separate it from
   the other four; what changes is that mono now means DATA. The group titles
   move to the UI sans so that labels and data no longer look alike, which is
   the distinction the step was missing.

   Stacking also buys width back: a one-line range needed a 152px column, a
   stacked one needs ~116px, so more of the day fits on screen at once — the
   right trade for the template whose whole premise is seeing more at once. */
#so-booking-widget[data-template="wide"] .so-bw-slot-list {
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 116px), 1fr));
    gap: 8px;
    justify-content: start;
}
#so-booking-widget[data-template="wide"] .so-bw-slot-row {
    justify-content: center;
    min-height: 52px;
    padding: 7px 8px;
    line-height: 1.25;
    border-radius: var(--so-bw-ds-radius-chip);
    /* The two lines are centred as a block, not baseline-aligned against the
       flag span that may sit beside them. */
    white-space: nowrap;
}
#so-booking-widget[data-template="wide"] .so-bw-slot-range {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 1px;
}
#so-booking-widget[data-template="wide"] .so-bw-slot-time {
    font-family: var(--so-bw-mono);
    font-size: 14.5px;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    letter-spacing: -0.01em;
}
#so-booking-widget[data-template="wide"] .so-bw-slot-till {
    font-family: var(--so-bw-mono);
    /* 11px, not 9.5px. Every OTHER sub-11px string in the catalogue is a
       typographic device — an uppercase tracked eyebrow ("TOTAL"), a step
       numeral, a day-of-week abbreviation, a service monogram. Those are
       deliberate hierarchy and raising them would flatten the templates.
       This one is different: it is the appointment's END TIME ("to 2:30 PM"),
       content the customer has to READ to choose a slot, set at 9.5px and
       0.62 opacity — the smallest readable string in the widget, on the
       densest template. Audited role-by-role before changing; this was the
       only readability defect the pass found. Mono digits keep the column
       aligned, so the extra 1.5px costs no layout. */
    font-size: 11px;
    font-weight: 400;
    font-variant-numeric: tabular-nums;
    letter-spacing: 0.02em;
    /* 0.62 on 9.5px mono was compounding size with contrast. */
    opacity: 0.72;
}
/* The selected pill fills with the accent, so the tail's 62% wash would sit on
   a dark ground and drop out. Lift it — same ink as the start time, separated
   by weight and size alone. */
#so-booking-widget[data-template="wide"] .so-bw-slot-row--selected .so-bw-slot-till {
    opacity: 0.82;
}
/* Sans, not mono: this is a LABEL. See the note above. */
#so-booking-widget[data-template="wide"] .so-bw-slot-group-title {
    margin-bottom: 5px;
    font-family: var(--so-bw-font);
    font-size: 10px;
    font-weight: 700;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--so-bw-ds-n500);
}


/* ──────────────────────────────────────────────────────────────────────────
   R6 · TWO PANE                                         Wide Canvas
   Details on one side, what you are about to pay on the other.
   ────────────────────────────────────────────────────────────────────────── */
#so-booking-widget[data-template="wide"] .so-bw-summary {
    display: grid;
    grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr);
    gap: 0;
    align-items: stretch;
    border: 1px solid var(--so-bw-border);
    border-radius: var(--so-bw-ds-radius-card);
    overflow: hidden;
    padding: 0;
}
/* TWO GRID ITEMS, ONE PER PANE — and that is the whole fix for this step.
   Placing the blocks individually could not work: fourteen children, ten of
   them unclassed inline-styled divs, and grid auto-placement that only ever
   moves forwards. See the note in booking-widget.js for the measurements.
   Here each pane is a single item that owns its own vertical stack, so both
   start at the top of the panel and neither can interleave with the other. */
#so-booking-widget[data-template="wide"] .so-bw-summary-main,
#so-booking-widget[data-template="wide"] .so-bw-summary-pay {
    display: flex;
    flex-direction: column;
    min-width: 0;
}
#so-booking-widget[data-template="wide"] .so-bw-summary-main {
    grid-column: 1;
    grid-row: 1;
    padding: 4px 0 14px;
}
/* The money pane is the tinted one, and the rule between the panes is its
   left border — so it runs the full height of the taller pane rather than
   stopping under the last price row, which is why `align-items: stretch`
   replaced `start` above. */
#so-booking-widget[data-template="wide"] .so-bw-summary-pay {
    grid-column: 2;
    grid-row: 1;
    gap: 2px;
    padding: 14px 16px;
    background: var(--so-bw-ds-n25);
    border-left: 1px solid var(--so-bw-border);
}
#so-booking-widget[data-template="wide"] .so-bw-summary-section {
    border: 0;
    padding: 14px 16px 0;
}
/* Inside the pay pane these are ordinary stacked rows now; the column, tint
   and rule all belong to the pane. Kept only to sit flush with it and to hold
   the emphasis the total has always had. */
#so-booking-widget[data-template="wide"] .so-bw-summary-row--total,
#so-booking-widget[data-template="wide"] .so-bw-summary-row--deposit {
    padding: 10px 0 0;
    margin: 0;
    border-top: 1px solid var(--so-bw-border);
}
#so-booking-widget[data-template="wide"] .so-bw-summary-row {
    display: flex;
    justify-content: space-between;
    gap: 14px;
    padding: 9px 16px;
}
/* The pay pane supplies its own 16px inset, so rows inside it must not add a
   second one — that is what put the price column 32px off the panel edge. */
#so-booking-widget[data-template="wide"] .so-bw-summary-pay .so-bw-summary-row {
    padding-left: 0;
    padding-right: 0;
}
#so-booking-widget[data-template="wide"] .so-bw-summary-label { flex: 1 1 auto; }
#so-booking-widget[data-template="wide"] .so-bw-summary-value {
    flex: 0 1 auto;
    font-variant-numeric: tabular-nums;
    text-align: right;
}
/* 900, not 760. The money pane carries two text inputs each with an Apply
   button beside it, so it has a hard floor that the details pane does not:
   measured at a 768px viewport the pane came out 240px wide and both
   placeholders were cut mid-word — "ENTER GIFT", "HAVE A REF". The split is
   only worth having when both panes are readable, and below roughly 900px of
   viewport they are not. Stacking earlier costs a scroll; splitting too early
   costs a form the customer cannot read. */
/* was @media (max-width:900px) — now gated on the CONTAINER width
   via data-cw (see booking-widget.js _wireContainerWidth). :where()
   keeps specificity identical to the media query it replaces. */

    /* One column: the panes stack, so the rule between them becomes a rule
       ABOVE the money pane and the tint spans the full width. */
    #so-booking-widget:where([data-cw~="900"])[data-template="wide"] .so-bw-summary{
        grid-template-columns: 1fr;
    }
    #so-booking-widget:where([data-cw~="900"])[data-template="wide"] .so-bw-summary-main,
    #so-booking-widget:where([data-cw~="900"])[data-template="wide"] .so-bw-summary-pay{
        grid-column: 1;
    }
    #so-booking-widget:where([data-cw~="900"])[data-template="wide"] .so-bw-summary-main{ grid-row: 1; }
    #so-booking-widget:where([data-cw~="900"])[data-template="wide"] .so-bw-summary-pay{
        grid-row: 2;
        border-left: 0;
        border-top: 1px solid var(--so-bw-border);
    }



/* ──────────────────────────────────────────────────────────────────────────
   TITLE CLAMP, RELEASED WHERE IT IS WRONG
   A rule further up clamps every `.so-bw-card--service .so-bw-card-title` to
   two lines and reserves that height, so cards in a grid keep equal heights.
   Right for a grid; wrong for the two primitives that are not cards — a chip
   is an answer, and a typographic index line exists to be read in full.
   The clamp is !important, so releasing it needs !important back.
   ────────────────────────────────────────────────────────────────────────── */
#so-booking-widget[data-template="editorial"] .so-bw-card--service .so-bw-card-title {
    display: block !important;
    -webkit-line-clamp: unset !important;
    line-clamp: unset;
    overflow: visible !important;
    text-overflow: clip !important;
    min-height: 0;
}


/* ──────────────────────────────────────────────────────────────────────────
   SHRINK SAFETY
   A grid or flex child defaults to `min-width: auto` and refuses to shrink
   below its content. Every primitive here places content in a track, so every
   one inherits that floor. The rendered audit found it immediately: one staff
   member named "Maya Krishnamurthy-Whitfield" pushed the card 140px past the
   widget's edge on a 360px phone, on three templates at once.
   ────────────────────────────────────────────────────────────────────────── */
#so-booking-widget[data-template="editorial"] .so-bw-card:not(.so-bw-sp__panel),
#so-booking-widget[data-template="hero"] .so-bw-card:not(.so-bw-sp__panel),
#so-booking-widget[data-template="weekboard"] .so-bw-card:not(.so-bw-sp__panel),
#so-booking-widget[data-template="wide"] .so-bw-card:not(.so-bw-sp__panel),
#so-booking-widget[data-template="editorial"] .so-bw-card-top,
#so-booking-widget[data-template="hero"] .so-bw-card-top,
#so-booking-widget[data-template="wide"] .so-bw-card-top,
#so-booking-widget[data-template="editorial"] .so-bw-card-body,
#so-booking-widget[data-template="hero"] .so-bw-card-body,
#so-booking-widget[data-template="wide"] .so-bw-card-body,
#so-booking-widget[data-template="editorial"] .so-bw-card-badges,
#so-booking-widget[data-template="hero"] .so-bw-card-badges,
#so-booking-widget[data-template="wide"] .so-bw-card-badges,
#so-booking-widget[data-template="editorial"] .so-bw-card-grid,
#so-booking-widget[data-template="hero"] .so-bw-card-grid,
#so-booking-widget[data-template="weekboard"] .so-bw-card-grid,
#so-booking-widget[data-template="wide"] .so-bw-card-grid,
#so-booking-widget[data-template="editorial"] .so-bw-summary,
#so-booking-widget[data-template="weekboard"] .so-bw-summary,
#so-booking-widget[data-template="wide"] .so-bw-summary,
#so-booking-widget[data-template="editorial"] .so-bw-summary-row,
#so-booking-widget[data-template="weekboard"] .so-bw-summary-row,
#so-booking-widget[data-template="wide"] .so-bw-summary-row,
#so-booking-widget[data-template="weekboard"] .so-bw-summary-label,
#so-booking-widget[data-template="wide"] .so-bw-summary-label,
#so-booking-widget[data-template="weekboard"] .so-bw-summary-value,
#so-booking-widget[data-template="wide"] .so-bw-summary-value,
#so-booking-widget[data-template="weekboard"] .so-bw-confirmation-detail,
#so-booking-widget[data-template="editorial"] .so-bw-scroll-wrap,
#so-booking-widget[data-template="hero"] .so-bw-scroll-wrap,
#so-booking-widget[data-template="weekboard"] .so-bw-scroll-wrap,
#so-booking-widget[data-template="wide"] .so-bw-scroll-wrap,
#so-booking-widget .so-bw-suggest-opt,
#so-booking-widget .so-bw-suggest-when,
#so-booking-widget .so-bw-week-col {
    min-width: 0;
}
#so-booking-widget[data-template="editorial"] .so-bw-card-title,
#so-booking-widget[data-template="hero"] .so-bw-card-title,
#so-booking-widget[data-template="weekboard"] .so-bw-card-title,
#so-booking-widget[data-template="wide"] .so-bw-card-title,
#so-booking-widget[data-template="editorial"] .so-bw-card-meta,
#so-booking-widget[data-template="hero"] .so-bw-card-meta,
#so-booking-widget[data-template="weekboard"] .so-bw-card-meta,
#so-booking-widget[data-template="wide"] .so-bw-card-meta,
#so-booking-widget[data-template="weekboard"] .so-bw-summary-label,
#so-booking-widget[data-template="wide"] .so-bw-summary-label,
#so-booking-widget[data-template="weekboard"] .so-bw-summary-value,
#so-booking-widget[data-template="wide"] .so-bw-summary-value {
    overflow-wrap: anywhere;
}
#so-booking-widget[data-template="editorial"] .so-bw-card-profile,
#so-booking-widget[data-template="hero"] .so-bw-card-profile,
#so-booking-widget[data-template="weekboard"] .so-bw-card-profile,
#so-booking-widget[data-template="wide"] .so-bw-card-profile {
    max-width: 100%;
    overflow-wrap: anywhere;
}
/* The slot panel is a grid child of .so-bw-datetime and carries its own
   scroller. Conversation, Editorial and Week Board strip its padding to make
   the times read as a list rather than a boxed panel — which removes the
   slack the base layout was relying on, so the panel has to be told it may
   not exceed its track. Same `min-width: auto` floor as the cards above,
   one level up. */
#so-booking-widget[data-template="editorial"] .so-bw-datetime,
#so-booking-widget[data-template="weekboard"] .so-bw-datetime,
#so-booking-widget[data-template="hero"] .so-bw-datetime,
#so-booking-widget[data-template="wide"] .so-bw-datetime,
#so-booking-widget[data-template="editorial"] .so-bw-slot-panel,
#so-booking-widget[data-template="weekboard"] .so-bw-slot-panel,
#so-booking-widget[data-template="hero"] .so-bw-slot-panel,
#so-booking-widget[data-template="wide"] .so-bw-slot-panel,
#so-booking-widget[data-template="editorial"] .so-bw-slot-list,
#so-booking-widget[data-template="weekboard"] .so-bw-slot-list,
#so-booking-widget[data-template="hero"] .so-bw-slot-list,
#so-booking-widget[data-template="wide"] .so-bw-slot-list {
    min-width: 0;
    max-width: 100%;
}
/* The panel's head and scroller carry NEGATIVE horizontal margins (-12px and
   -6px) so their backgrounds bleed to the edges of a panel that has padding.
   Conversation, Editorial and Week Board deliberately strip that padding to
   make the times read as a list rather than a boxed panel — which leaves the
   bleed with nothing to bleed into, and the children hang exactly 12px and
   24px past the panel on a 360px phone.
   Removing the padding means also removing what compensated for it. */
#so-booking-widget[data-template="editorial"] .so-bw-slot-panel-head,
#so-booking-widget[data-template="weekboard"] .so-bw-slot-panel-head,
#so-booking-widget[data-template="editorial"] .so-bw-slot-scroll,
#so-booking-widget[data-template="weekboard"] .so-bw-slot-scroll {
    margin-inline: 0;
    padding-inline: 0;
}


/* ──────────────────────────────────────────────────────────────────────────
   PILOT TAP TARGETS AND STICKY CONFLICTS

   The rendered audit (tests/visual/pilot-layout-audit.mjs) measures every
   control at eight widths. Six controls came back under the design system's
   own 44px promise on EVERY template — including both control templates,
   which is how we know these are shared-layer defects rather than anything
   the pilot primitives introduced:

       .so-bw-card-profile      85x20    "View profile" on a staff card
       .so-bw-card-more         62x20    "Read more" on a service card
       .so-bw-slot-back        570x20    "Change date"
       .so-bw-tz-combo-trigger 230x30    the timezone combobox
       .so-bw-calendar-nav      28x28    the month arrows

   All five are link-styled text buttons whose PAINTED size is deliberate —
   they are meant to be quiet. What was wrong was the hit area, so these add
   height and centre the label inside it rather than making the text bigger.

   Scoped to the five pilots on purpose. The same defect exists on the other
   fourteen templates and the same fix will apply to them, but those templates
   have not been through this review and inflating every staff card in the
   plugin is not a change to make as a side effect of a pilot.
   ────────────────────────────────────────────────────────────────────────── */
#so-booking-widget[data-template="editorial"],
#so-booking-widget[data-template="hero"],
#so-booking-widget[data-template="weekboard"],
#so-booking-widget[data-template="wide"] {
    --so-bw-calendar-nav-size: 40px;
}
#so-booking-widget[data-template="editorial"] .so-bw-card-profile,
#so-booking-widget[data-template="hero"] .so-bw-card-profile,
#so-booking-widget[data-template="weekboard"] .so-bw-card-profile,
#so-booking-widget[data-template="wide"] .so-bw-card-profile,
#so-booking-widget[data-template="editorial"] .so-bw-card-more,
#so-booking-widget[data-template="hero"] .so-bw-card-more,
#so-booking-widget[data-template="weekboard"] .so-bw-card-more,
#so-booking-widget[data-template="wide"] .so-bw-card-more,
#so-booking-widget[data-template="editorial"] .so-bw-slot-back,
#so-booking-widget[data-template="hero"] .so-bw-slot-back,
#so-booking-widget[data-template="weekboard"] .so-bw-slot-back,
#so-booking-widget[data-template="wide"] .so-bw-slot-back {
    display: inline-flex;
    align-items: center;
    min-height: var(--so-bw-ds-tap);
}
/* --ghost is the reserved-space copy on cards whose description is short. It
   has to track the real button's height exactly or the rows go ragged — which
   is the one thing the card grid may never do. It inherits the rule above by
   class, and this only restates that it must not be re-laid out. */
/* Wide Canvas is deliberately absent: it is a single-column list, so it hides
   the ghost outright (see its card block). Listing it here would re-show it,
   because this rule is later in the file at the same specificity — which is
   exactly what happened, and it put a 44px band of empty space under every
   short row. */
#so-booking-widget[data-template="editorial"] .so-bw-card-more--ghost,
#so-booking-widget[data-template="hero"] .so-bw-card-more--ghost,
#so-booking-widget[data-template="weekboard"] .so-bw-card-more--ghost {
    display: inline-flex;
}
#so-booking-widget[data-template="editorial"] .so-bw-tz-combo-trigger,
#so-booking-widget[data-template="hero"] .so-bw-tz-combo-trigger,
#so-booking-widget[data-template="weekboard"] .so-bw-tz-combo-trigger,
#so-booking-widget[data-template="wide"] .so-bw-tz-combo-trigger {
    min-height: var(--so-bw-ds-tap);
}

/* The slot panel's head is `position: sticky; top: …` so it could stay put
   while a long list scrolled underneath it. That inner scroller no longer
   exists — the base panel says so in as many words ("No inner max-height /
   scroll: the panel flows at its natural height and the card body provides
   the single scroll region") — so the head now sticks against the CARD's
   scroller, which is also what .so-bw-nav sticks to from the bottom.

   On a short list the two sticky boxes meet and overlap: the audit caught
   "so-bw-slot-panel-head / so-bw-nav 570x44" on several widths, and 570x44 is
   the whole head sitting under the whole nav.

   Sticky with nothing to scroll past is not doing anything except colliding
   with the one element that genuinely needs to stay pinned, so it goes.
   (This also retires the .so-bw-slot-panel-head--menu z-index lift, which
   existed only to get the timezone dropdown out from under the nav.) */
#so-booking-widget[data-template="editorial"] .so-bw-slot-panel-head,
#so-booking-widget[data-template="hero"] .so-bw-slot-panel-head,
#so-booking-widget[data-template="weekboard"] .so-bw-slot-panel-head,
#so-booking-widget[data-template="wide"] .so-bw-slot-panel-head {
    position: static;
}


/* ──────────────────────────────────────────────────────────────────────────
   PILOT FOCUS RINGS — a halo, not a box

   Reported from the live install: clicking almost anything left a hard
   rectangle sitting around it. That rectangle is this widget's own focus
   ring — `outline: 2px solid var(--so-bw-primary); outline-offset: 2px` —
   and on a slate or gold accent it reads as a stray black box drawn around a
   small text link.

   It shows up after a MOUSE click (not just keyboard) because the profile
   dialog restores focus to the control that opened it when it closes, and
   Chrome matches :focus-visible on programmatic focus. The same stylesheet
   already documents that behaviour over in the popup section.

   The ring is NOT simply removed. A focusable control with no visible focus
   state is only nominally keyboard-reachable — you can tab to it and cannot
   tell where you are — and this file makes that argument itself, twice, next
   to the rules being softened here. So instead it becomes the same soft halo
   the timezone combobox has used all along: a 3px translucent accent glow
   that follows the control's own shape, with no offset gap and therefore no
   rectangle.

   Cards keep a ring too, for the same reason. Nothing about the service or
   staff card LAYOUT changes — this is the focus state only.

   ── AND THE BOX THAT SURVIVED ALL OF THAT ─────────────────────────────────

   The halo below fixed :focus-visible, and the box kept appearing on "Read
   more" and "View profile". It was never this stylesheet's ring. Enumerating
   the matched rules on the live install, on a button that had just been
   clicked, turns up exactly one source:

       :where(.wp-site-blocks :focus) { outline-width: 2px; outline-style: solid }

   That is WORDPRESS, not this plugin — the global stylesheet generated from
   theme.json and inlined into every page. Two properties of it matter:

     · it keys off `:focus`, NOT `:focus-visible`, so it fires on a MOUSE
       click, which is when the ring below deliberately stays away; and
     · it sets no outline-COLOR, so the box paints in `currentColor` — black
       on "Read more", slate on "View profile". Both match the screenshots.

   `:where()` scores (0,0,0), so it was never outranking anything: nothing
   here styled the plain :focus state at all, and an unopposed zero beats an
   absent rule. That is also why raising specificity on the halo did nothing.

   The fix suppresses the outline ONLY where the browser has already judged
   that focus should not be drawn — `:focus:not(:focus-visible)`, i.e. pointer
   focus and the programmatic restore when a dialog closes. Keyboard focus
   never matches the negation, so every ring below is untouched and nothing
   becomes less reachable. It is scoped to the widget's own controls and is
   inert wherever no stray outline was drawn, which is why it is written once
   here rather than five times per pilot. */
#so-booking-widget :is(a, button, input, select, textarea, summary, [role="button"], [tabindex]):focus:not(:focus-visible) {
    outline: none;
}
#so-booking-widget[data-template="editorial"] .so-bw-card:not(.so-bw-sp__panel)[role="button"]:focus-visible,
#so-booking-widget[data-template="hero"] .so-bw-card:not(.so-bw-sp__panel)[role="button"]:focus-visible,
#so-booking-widget[data-template="weekboard"] .so-bw-card:not(.so-bw-sp__panel)[role="button"]:focus-visible,
#so-booking-widget[data-template="wide"] .so-bw-card:not(.so-bw-sp__panel)[role="button"]:focus-visible,
#so-booking-widget[data-template="editorial"] .so-bw-card-profile:focus-visible,
#so-booking-widget[data-template="hero"] .so-bw-card-profile:focus-visible,
#so-booking-widget[data-template="weekboard"] .so-bw-card-profile:focus-visible,
#so-booking-widget[data-template="wide"] .so-bw-card-profile:focus-visible,
#so-booking-widget[data-template="editorial"] .so-bw-card-more:focus-visible,
#so-booking-widget[data-template="hero"] .so-bw-card-more:focus-visible,
#so-booking-widget[data-template="weekboard"] .so-bw-card-more:focus-visible,
#so-booking-widget[data-template="wide"] .so-bw-card-more:focus-visible,
#so-booking-widget[data-template="editorial"] .so-bw-calendar-nav:focus-visible,
#so-booking-widget[data-template="hero"] .so-bw-calendar-nav:focus-visible,
#so-booking-widget[data-template="weekboard"] .so-bw-calendar-nav:focus-visible,
#so-booking-widget[data-template="wide"] .so-bw-calendar-nav:focus-visible,
#so-booking-widget[data-template="editorial"] .so-bw-slot-back:focus-visible,
#so-booking-widget[data-template="hero"] .so-bw-slot-back:focus-visible,
#so-booking-widget[data-template="weekboard"] .so-bw-slot-back:focus-visible,
#so-booking-widget[data-template="wide"] .so-bw-slot-back:focus-visible,
#so-booking-widget[data-template="editorial"] .so-bw-slot-row:focus-visible,
#so-booking-widget[data-template="hero"] .so-bw-slot-row:focus-visible,
#so-booking-widget[data-template="weekboard"] .so-bw-slot-row:focus-visible,
#so-booking-widget[data-template="wide"] .so-bw-slot-row:focus-visible,
#so-booking-widget[data-template="editorial"] .so-bw-calendar-day:focus-visible,
#so-booking-widget[data-template="hero"] .so-bw-calendar-day:focus-visible,
#so-booking-widget[data-template="weekboard"] .so-bw-calendar-day:focus-visible,
#so-booking-widget[data-template="wide"] .so-bw-calendar-day:focus-visible,
#so-booking-widget[data-template="weekboard"] .so-bw-week-slot:focus-visible{
    outline: none;
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--so-bw-primary) 28%, transparent);
}
/* Forced-colors mode drops box-shadow entirely, so the halo would leave those
   users with no focus state at all. Give the outline back there — a visible
   rectangle is exactly what that mode wants. */
@media (forced-colors: active) {
    #so-booking-widget .so-bw-card[role="button"]:focus-visible,
    #so-booking-widget .so-bw-card-profile:focus-visible,
    #so-booking-widget .so-bw-card-more:focus-visible,
    #so-booking-widget .so-bw-calendar-nav:focus-visible,
    #so-booking-widget .so-bw-slot-back:focus-visible,
    #so-booking-widget .so-bw-slot-row:focus-visible,
    #so-booking-widget .so-bw-calendar-day:focus-visible,
    #so-booking-widget .so-bw-week-slot:focus-visible,
    #so-booking-widget .so-bw-suggest-opt:focus-visible {
        outline: 2px solid Highlight;
        outline-offset: 2px;
    }
}


/* ──────────────────────────────────────────────────────────────────────────
   PILOT RESPONSIVE GUARDS
   Kept together so the breakpoints are reviewable as a set.
   ────────────────────────────────────────────────────────────────────────── */
/* was @media (max-width: 380px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    /* Suggested times: the chip is a two-part row (when / time). At the narrow
       end the time drops under the day rather than the two colliding. */
    #so-booking-widget:where([data-cw~="380"]) .so-bw-suggest-opt{
        flex-direction: column;
        align-items: flex-start;
        gap: 4px;
    }

/* was @media (max-width: 480px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    #so-booking-widget:where([data-cw~="480"])[data-template="editorial"] .so-bw-card-title{ font-size: 17px; }
    #so-booking-widget:where([data-cw~="480"])[data-template="editorial"] .so-bw-card:not(.so-bw-sp__panel){ padding: 14px 2px; }
    #so-booking-widget:where([data-cw~="480"])[data-template="editorial"] .so-bw-slot-row{ font-size: 16px; }

/* was @media (max-width: 640px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    /* A 148px label column on a phone leaves ~180px for the field. Stack. */
    #so-booking-widget:where([data-cw~="640"])[data-template="editorial"] .so-bw-form-group,
    #so-booking-widget:where([data-cw~="640"])[data-template="editorial"] .so-bw-form-row{
        grid-template-columns: 1fr;
    }
    #so-booking-widget:where([data-cw~="640"])[data-template="editorial"] .so-bw-label{ text-align: left; }

/* was @media (max-width: 420px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    /* The .so-bw-label / .so-bw-summary-row caps that used to live here kept
       the retired bubbles clear of the opposite edge. Neither element is
       inset any more — both run the full width — so a cap would only crop
       them. The summary's own stacking rule at this width is in R5 above. */

/* No pilot primitive may let the page scroll sideways. */
#so-booking-widget[data-template="editorial"] .so-bw-card-grid,
#so-booking-widget[data-template="hero"] .so-bw-card-grid,
#so-booking-widget[data-template="weekboard"] .so-bw-card-grid,
#so-booking-widget[data-template="wide"] .so-bw-card-grid,
#so-booking-widget[data-template="editorial"] .so-bw-summary,
#so-booking-widget[data-template="weekboard"] .so-bw-summary,
#so-booking-widget[data-template="wide"] .so-bw-summary {
    max-width: 100%;
}


/* ==========================================================================
   17. PHASE 2  —  THE REMAINING FOURTEEN
   --------------------------------------------------------------------------
   Section 16 carried five templates through the whole journey. The other
   fourteen still shared one card grid, one form, one review and one
   confirmation between them — six of the nine steps identical across the
   catalogue, so a business that picked Agenda because its clients want the
   soonest slot got the same grey two-column review as one that picked Dusk
   for a late-night members' club.

   This section closes that. Same rule as section 16, restated because it is
   the thing that keeps this file legible:

       A PRIMITIVE owns its layout.
       A TEMPLATE selects primitives, and owns palette and character.
       A TEMPLATE never restyles a primitive's internals.

   Corollary the pilot paid for: when a template REPLACES a primitive's layout
   rather than selecting it, the replacement owns every declaration it depends
   on. Inheriting half a layout is what shipped Conversation's chip with its
   label outside the pill.

   -- Order of this section ------------------------------------------------

     17.0  Review-markup primitives  (prerequisite — read the banner below)
     17.1  Centred            single measure
     17.4  Tabbed Rail        free navigation
     17.6  Agenda             list, never a grid
     17.7  Card Deck          a physical stack
     17.9  Daypart            three panels
     17.10 Timeline           a day drawn as an axis
     17.12 Frost              floating sheets
     17.13 Dusk               subtraction, in the dark
     17.14 Coral Pop          the soft register

   The gaps are withdrawn templates, and the numbers are deliberately NOT
   closed up: 17.3 was Accordion, 17.2 Split Canvas, 17.5 Dialog, 17.8 Inline
   and 17.11 Dual Month. Renumbering would invalidate every cross-reference in
   this file — and there are many, since each block cites the ones it borrows
   from — to save nothing.

   -- The five hazards every block below is checked against ------------------

   1. `.so-bw-card` IS NOT ONLY A CARD. The staff-profile panel is
      `<div class="so-bw-card so-bw-sp__panel">`. Every bare `.so-bw-card`
      rule carries `:not(.so-bw-sp__panel)`. Conversation's 999px radius
      reached that panel once and opened the profile as a disc with its
      buttons clipped off the curve.
   2. `.so-bw-card--service` OPTS OUT OF ROW LAYOUT WITH !important, at
      (1,2,0). A primitive that makes the service card a row must use
      !important at higher specificity, and say why.
   3. SELECTED STATE CANNOT BE REPAINTED. The 15% wash is (1,3,0) AND
      !important. A per-template `background: var(--so-bw-primary)` loses the
      fill and keeps its `color: #fff` — invisible text. Use the wash and the
      accent ring, as the other eighteen do.
   4. `grid-auto-rows: 1fr` ON THE CARD GRID levels every row to the tallest
      card. Right for a multi-column catalogue, wrong for every one-column
      list primitive here. Opt out at matching (1,3,0) specificity.
   5. MEDIA QUERIES ADD NO SPECIFICITY. A token set on
      `#so-booking-widget .so-bw-container` (1,1,0) beats one set on
      `#so-booking-widget` (1,0,0) at every width.

   -- Breakpoint ladder -----------------------------------------------------

   The file collapsed at fourteen different widths before this section, most
   chosen per-rule. Everything below collapses at FIVE, and only these:

       900px   two-pane and rail architectures go single column
       768px   3-up grids to 2-up; side-by-side date/time stacks
       640px   2-up grids to 1-up; dense slot grids drop a column
       480px   the existing edge-to-edge tier (gutter 16, shell radius 0)
       380px   last resort; nothing new should need it

   Grandfathered, because they ship and are audited: F2's 700px, Week Board's
   640px, Timeline's 560px. Split's old 860px moves to 900px here — Split is
   one of the fourteen, so it adopts the ladder.
   ========================================================================== */


/* ==========================================================================
   17.0 · REVIEW-MARKUP PRIMITIVES
   --------------------------------------------------------------------------
   WHY THIS BLOCK EXISTS, AND WHY IT HAD TO COME FIRST.

   The review step's price rows carried their rules and colours in `style=""`
   attributes in booking-widget.js:

       <div class="so-bw-summary-row"
            style="color: …; border-top: 1px solid …; padding-top: 14px;">

   An inline style beats ANY stylesheet rule that is not !important, whatever
   its specificity. So every review primitive that re-rules or re-pads
   `.so-bw-summary-row` was already losing on those rows — the shipped pilots
   included. Week Board's R3 gives each row a 1px bottom border and 12px 14px
   padding; the tax and referral rows kept their own top border and 14px top
   padding on top of it, which is a doubled rule and a 2px step in the
   left-hand column on Week Board, Wide Canvas and Editorial today.

   Fourteen more review primitives on that basis would have multiplied the
   fault by fourteen. So the attributes are gone from the markup and the
   declarations live here, at (1,1,0), where a per-template block at (1,2,0)
   can simply win.

   NOTHING HERE IS NEW DESIGN. Every value reproduces what the inline style
   did, with two exceptions that were defects rather than decisions:

     • `--so-bw-success` is #10b981, which is 2.48:1 on white. It was being
       used as TEXT colour on six rows. Text now reads from a new
       `--so-bw-success-ink` (#0f7a52, 4.71:1); the bright token keeps its
       existing non-text jobs untouched.
     • The "Remove gift card" / "Remove" links were 11px text with no padding,
       about 15px tall. They are targets, so they get the tap floor.
   ========================================================================== */

#so-booking-widget .so-bw-container {
    /* Text-safe ends of the status ramp. The bright tokens stay as they are —
       they are correct as fills and borders, and changing them would repaint
       banners across the whole widget. */
    --so-bw-success-ink: #0f7a52;
    --so-bw-warning-ink: #8a5300;
    --so-bw-error-ink: #b42318;
}
/* Dark-first templates: the same three, lifted off ink. */
#so-booking-widget[data-theme="dark"] .so-bw-container,
#so-booking-widget[data-template-id="dusk-champagne"] .so-bw-container {
    --so-bw-success-ink: #5fd3a0;
    --so-bw-warning-ink: #e8b563;
    --so-bw-error-ink: #f88b81;
}

/* ── The price blocks ────────────────────────────────────────────────────
   A "block" is a rule-topped group inside the pay column. Two flavours: the
   default stacks its own rows, `--open` holds arbitrary content (a code
   field, a radio row) and leaves the stacking to it. */
#so-booking-widget .so-bw-summary-block {
    display: flex;
    flex-direction: column;
    gap: 6px;
    border-top: 1px solid var(--so-bw-border);
    padding-top: 14px;
}
#so-booking-widget .so-bw-summary-block--open {
    display: block;
}
#so-booking-widget .so-bw-summary-block--pay {
    margin-bottom: 14px;
}
#so-booking-widget .so-bw-summary-block-title {
    margin-bottom: 8px;
    font-size: 12px;
    font-weight: 600;
    color: var(--so-bw-text);
}
#so-booking-widget .so-bw-summary-block-title--split {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
}
#so-booking-widget .so-bw-summary-block-aside {
    font-size: 11px;
    font-weight: 400;
    color: var(--so-bw-text-secondary);
    font-family: var(--so-bw-mono);
    font-variant-numeric: tabular-nums;
}
#so-booking-widget .so-bw-summary-stack {
    display: flex;
    flex-direction: column;
    gap: 6px;
}

/* ── Row modifiers ──────────────────────────────────────────────────────
   `--rule` is the one that mattered: it is the top hairline the tax and
   referral rows used to draw inline. As a class it is overridable, so a
   template that rules its rows differently (Week Board's line items, Dusk's
   hairlines) can turn it off in one declaration. */
#so-booking-widget .so-bw-summary-row--rule {
    border-top: 1px solid var(--so-bw-border);
    padding-top: 14px;
}
#so-booking-widget .so-bw-summary-row--inset {
    margin-top: 6px;
}
#so-booking-widget .so-bw-summary-row--accent {
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}
#so-booking-widget .so-bw-summary-row--credit {
    color: var(--so-bw-success-ink);
}
#so-booking-widget .so-bw-summary-row--deposit {
    margin-top: 6px;
    padding-top: 10px;
    border-top: 1px dashed var(--so-bw-border);
}
#so-booking-widget .so-bw-summary-row--deposit .so-bw-summary-label {
    font-weight: 600;
    color: var(--so-bw-warning-ink);
}
#so-booking-widget .so-bw-summary-row--deposit .so-bw-summary-value {
    font-weight: 700;
    color: var(--so-bw-warning-ink);
}
#so-booking-widget .so-bw-summary-value--strong {
    font-weight: 700;
}
/* The home-visit address is a multi-line value typed into a textarea. */
#so-booking-widget .so-bw-summary-value--multiline {
    white-space: pre-line;
}
#so-booking-widget .so-bw-summary-code {
    font-family: var(--so-bw-mono);
    font-size: 11px;
    letter-spacing: 0.04em;
    color: var(--so-bw-text-secondary);
}
#so-booking-widget .so-bw-summary-chip {
    display: inline-block;
    margin-left: 6px;
    padding: 1px 6px;
    border-radius: var(--so-bw-r-4);
    background: var(--so-bw-bg-tertiary);
    color: var(--so-bw-text-secondary);
    font-size: 11px;
    font-weight: 600;
}

/* ── Notes ──────────────────────────────────────────────────────────────
   The small print under a block: an error, a reassurance, a deposit warning.
   All three used to be a bare div with an inline colour. */
#so-booking-widget .so-bw-summary-note {
    margin: 6px 0 0;
    font-size: 12px;
    line-height: 1.5;
    color: var(--so-bw-text-secondary);
}
#so-booking-widget .so-bw-summary-note--credit {
    font-size: 11px;
    color: var(--so-bw-success-ink);
}
#so-booking-widget .so-bw-summary-note--error {
    color: var(--so-bw-error-ink);
}
#so-booking-widget .so-bw-summary-note--warning {
    color: var(--so-bw-warning-ink);
}

/* ── The undo links ─────────────────────────────────────────────────────
   "Remove gift card" and "Remove". Both were 11px underlined text with no
   padding — roughly 15px tall, i.e. a target under every threshold the
   design system and WCAG 2.2 set. The label stays small; the target does
   not. */
#so-booking-widget .so-bw-summary-undo {
    align-self: flex-start;
    display: inline-flex;
    align-items: center;
    min-height: var(--so-bw-ds-tap);
    padding: 0 2px;
    border: 0;
    background: none;
    color: var(--so-bw-text-secondary);
    font-family: inherit;
    font-size: 11px;
    text-decoration: underline;
    text-underline-offset: 3px;
    cursor: pointer;
}
#so-booking-widget .so-bw-summary-undo:hover {
    color: var(--so-bw-text);
}
#so-booking-widget .so-bw-summary-undo:focus-visible {
    outline: 2px solid var(--so-bw-primary);
    outline-offset: 2px;
    border-radius: var(--so-bw-r-4);
}
#so-booking-widget .so-bw-req {
    color: var(--so-bw-error-ink);
}

/* ── The code / redeem row ──────────────────────────────────────────────
   Gift card, referral and loyalty all render an input beside a button.
   `.so-bw-code-input` is already `flex: 1 1 auto`, so it has always wanted a
   flex row round it; it used to get one from `style="display:flex;gap:8px"`. */
#so-booking-widget .so-bw-code-row {
    display: flex;
    align-items: center;
    gap: 8px;
}
/* Loyalty's redeem field is a NUMBER, so it must not inherit the uppercase
   letter-spaced treatment a voucher code gets. */
#so-booking-widget .so-bw-num-input {
    flex: 1 1 auto;
    min-width: 0;
    font-family: var(--so-bw-mono);
    font-variant-numeric: tabular-nums;
}
/* "Max" is a helper beside the field, not a commit — it takes the ghost
   treatment the rest of the widget gives a retreat. */
#so-booking-widget .so-bw-code-apply--ghost {
    background: transparent;
    border: 1px solid var(--so-bw-border);
    color: var(--so-bw-ds-n700);
    white-space: nowrap;
}
#so-booking-widget .so-bw-code-apply--ghost:hover:not(:disabled) {
    background: var(--so-bw-ds-n25);
    border-color: var(--so-bw-ds-n300);
    color: var(--so-bw-ds-n900);
}

/* ── Payment method ─────────────────────────────────────────────────────
   Was a bound `:style` string that rebuilt the whole declaration list on
   every keystroke and could not be themed. Now a bound CLASS, so a template
   restyles it like anything else. */
#so-booking-widget .so-bw-pay-methods {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
}
#so-booking-widget .so-bw-pay-method {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    min-height: var(--so-bw-ds-tap);
    padding: 8px 12px;
    border: 1px solid var(--so-bw-border);
    border-radius: var(--so-bw-ds-radius-ctl);
    background: var(--so-bw-card-bg);
    color: var(--so-bw-text);
    font-size: 13px;
    font-weight: 600;
    cursor: pointer;
    transition: all var(--so-bw-ds-motion);
}
/* The border does NOT thicken to 2px on selection. It used to, and a 1px→2px
   swap moves the label by a pixel and reflows the whole wrapping row — the
   chips visibly jump as you pick between them. An inset ring paints in the
   same box. */
#so-booking-widget .so-bw-pay-method--on {
    border-color: var(--so-bw-primary);
    box-shadow: inset 0 0 0 1px var(--so-bw-primary);
    background: color-mix(in srgb, var(--so-bw-primary) 10%, transparent);
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}
#so-booking-widget .so-bw-pay-method:focus-within {
    outline: 2px solid var(--so-bw-primary);
    outline-offset: 2px;
}
#so-booking-widget .so-bw-pay-radio {
    margin: 0;
    accent-color: var(--so-bw-primary);
}

/* ── Membership banner ──────────────────────────────────────────────────
   `.so-bw-summary-section--member` had a class all along and carried its
   entire appearance in `style=""` anyway. */
#so-booking-widget .so-bw-summary-section--member {
    margin-top: 12px;
    padding: 12px 14px;
    border: 1px solid var(--so-bw-primary);
    border-radius: var(--so-bw-ds-radius-card);
    background: color-mix(in srgb, var(--so-bw-primary) 8%, transparent);
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}
#so-booking-widget .so-bw-member-row {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 10px;
}
#so-booking-widget .so-bw-member-main {
    flex: 1 1 auto;
    min-width: 0;
}
#so-booking-widget .so-bw-member-eyebrow {
    font-family: var(--so-bw-mono);
    font-size: 10.5px;
    font-weight: 500;
    letter-spacing: 0.14em;
    text-transform: uppercase;
}
#so-booking-widget .so-bw-member-plan {
    margin-top: 2px;
    font-size: 13px;
    font-weight: 600;
    color: var(--so-bw-text);
}
#so-booking-widget .so-bw-member-badge {
    display: inline-block;
    margin-left: 6px;
    padding: 1px 8px;
    border: 1px solid var(--so-bw-primary);
    border-radius: 999px;
    background: var(--so-bw-card-bg);
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
    font-size: 10px;
    font-weight: 700;
    letter-spacing: 0.05em;
    text-transform: uppercase;
}
#so-booking-widget .so-bw-member-saving {
    flex: 0 0 auto;
    text-align: right;
}
#so-booking-widget .so-bw-member-saving-k {
    font-size: 11px;
    font-weight: 600;
}
#so-booking-widget .so-bw-member-saving-v {
    font-family: var(--so-bw-mono);
    font-variant-numeric: tabular-nums;
    font-size: 14px;
    font-weight: 700;
}

/* ── The duplicate-booking alert ────────────────────────────────────────
   Rendered identically on three steps (date/time, details, review) and
   inlined three times. One class, one place. */
#so-booking-widget .so-bw-alert {
    display: flex;
    align-items: flex-start;
    gap: 10px;
    margin: 0 0 16px;
    padding: 12px 14px;
    border: 1px solid var(--so-bw-border);
    border-radius: var(--so-bw-ds-radius-ctl);
    background: var(--so-bw-bg-tertiary);
}
#so-booking-widget .so-bw-alert--error {
    border-color: var(--so-bw-error);
    background: var(--so-bw-error-light);
    color: var(--so-bw-error-ink);
}
#so-booking-widget .so-bw-alert-icon {
    flex-shrink: 0;
    margin-top: 1px;
}
#so-booking-widget .so-bw-alert-body {
    min-width: 0;
    font-size: 12px;
    line-height: 1.5;
}
#so-booking-widget .so-bw-alert-title {
    display: block;
    margin-bottom: 2px;
    font-size: 13px;
    font-weight: 700;
}
#so-booking-widget .so-bw-alert-line {
    display: block;
    margin-top: 4px;
}
#so-booking-widget .so-bw-alert-line--soft {
    color: var(--so-bw-warning-ink);
}


/* ==========================================================================
   17.13 · DUSK  (dusk-champagne, layout key "dusk")
   --------------------------------------------------------------------------
   THE DARK REGISTER, AND THEREFORE SUBTRACTION.

   Section 14 gave Dusk its shape — full-width canvas, summary docked as a
   slim bar along the bottom — and then the journey reverted to the shared
   components, which are drawn for paper. On #14161d a column of nineteen
   bordered, shadowed, filled cards is not "the same design in the dark", it
   is noise: every card edge is a second line competing with the one above it,
   and the eye has nowhere to rest.

   So this template's whole idea at every step is to REMOVE surface. Hairlines
   instead of cards. Underlines instead of field boxes. One champagne accent,
   used once per screen, on the thing that matters. The docked bar carries the
   running total in peripheral vision so nothing has to shout.

   The ramp is already inverted for this template in section 15 — n900 is
   #f2efe8 here, n0 is #14161d. Everything below reads ramp tokens, never
   literals, so it stays correct on both sides of that inversion.
   ========================================================================== */

/* ── C18 · HAIRLINE ROWS ─────────────────────────────────────────────────
   Modelled on Editorial's C2, and deliberately so: both argue that a list of
   choices is an index rather than a shelf. They diverge on everything after
   that — Editorial numbers its entries and sets them in a serif, Dusk leaves
   them unnumbered and lets the champagne price do the work. */
#so-booking-widget[data-template="dusk"] .so-bw-card-grid {
    grid-template-columns: 1fr;
    gap: 0;
    border-top: 1px solid var(--so-bw-ds-n100);
}
/* Hazard 4. `grid-auto-rows: 1fr` levels every row to the tallest card, which
   is right when cards sit SIDE BY SIDE and wrong here — one service with a
   long description would set the height of every other row in the list.
   Matches the base rule's (1,3,0) so it does not depend on source order; the
   :has() guard keeps the levelling while an add-ons panel is open, where the
   rows genuinely do need to agree. */
#so-booking-widget[data-template="dusk"] .so-bw-card-grid:not(:has(.so-bw-card--has-extras-open)) {
    grid-auto-rows: auto;
}
#so-booking-widget[data-template="dusk"] .so-bw-card:not(.so-bw-sp__panel) {
    border: 0;
    border-bottom: 1px solid var(--so-bw-ds-n100);
    border-radius: 0;
    background: transparent;
    box-shadow: none;
}
/* Hover is a wash, not a lift: a raised card in the dark reads as a modal. */
#so-booking-widget[data-template="dusk"] .so-bw-card:not(.so-bw-sp__panel):hover {
    background: var(--so-bw-ds-n25);
}
/* Hazard 3. The 15% wash and the ring are set globally with !important, so
   this does NOT repaint the row — it adds the champagne edge that makes a
   selected line legible without a fill. */
#so-booking-widget[data-template="dusk"] .so-bw-card--selected:not(.so-bw-sp__panel) {
    box-shadow: inset 3px 0 0 var(--so-bw-primary);
}
/* A photograph is a big lit rectangle on a dark page and it takes over the
   list. The images stay reachable — the service dialog shows them — but the
   catalogue is type. */
#so-booking-widget[data-template="dusk"] .so-bw-card-media {
    display: none;
}
#so-booking-widget[data-template="dusk"] .so-bw-card-title {
    font-size: 15px;
    font-weight: 600;
    color: var(--so-bw-ds-n900);
}
#so-booking-widget[data-template="dusk"] .so-bw-card-meta--clamp {
    color: var(--so-bw-ds-n500);
}
/* The one champagne mark per row. The duration stays quiet beside it — a
   dark UI can carry exactly one accent per line before it stops being one. */
#so-booking-widget[data-template="dusk"] .so-bw-badge--price,
#so-booking-widget[data-template="dusk"] .so-bw-badge--free {
    background: none;
    border: 0;
    padding: 0;
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
    font-family: var(--so-bw-mono);
    font-variant-numeric: tabular-nums;
    font-size: 13px;
    font-weight: 600;
}
#so-booking-widget[data-template="dusk"] .so-bw-badge--duration {
    background: none;
    border: 0;
    padding: 0;
    color: var(--so-bw-ds-n500);
    font-size: 12px;
}

/* ── Dusk · DATE & TIME ──────────────────────────────────────────────────
   The month needs no work beyond what section 15 already inverted. The TIMES
   do: the shared pill grid draws nineteen filled boxes, which is the exact
   thing this template removes everywhere else. A ruled tabular grid instead —
   the slot is the type, the rules are the structure. */
#so-booking-widget[data-template="dusk"] .so-bw-slot-panel {
    border: 0;
    border-radius: 0;
    background: none;
    padding: 0;
}
#so-booking-widget[data-template="dusk"] .so-bw-slot-list {
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: 1px;
    background: var(--so-bw-ds-n100);
    border: 1px solid var(--so-bw-ds-n100);
    border-radius: var(--so-bw-ds-radius-chip);
    overflow: hidden;
}
/* The 1px gap on a tinted grid IS the rule between cells — cheaper than
   twelve borders and it can never double at a seam. Each cell repaints the
   canvas so the tint only shows in the gaps. */
#so-booking-widget[data-template="dusk"] .so-bw-slot-row {
    justify-content: center;
    min-height: 46px;
    padding: 10px 6px;
    border: 0;
    border-radius: 0;
    background: var(--so-bw-ds-n0);
    box-shadow: none;
    font-size: 13px;
    font-weight: 500;
}
#so-booking-widget[data-template="dusk"] .so-bw-slot-row:hover:not(.so-bw-slot-row--booked) {
    background: var(--so-bw-ds-n50);
}
#so-booking-widget[data-template="dusk"] .so-bw-slot-row--booked {
    background: var(--so-bw-ds-n25);
    color: var(--so-bw-ds-n300);
}
/* Champagne is light, so a filled slot takes the dark ink the section-15
   signature already sets for buttons and selected days. */
#so-booking-widget[data-template="dusk"] .so-bw-slot-row--selected {
    background: var(--so-bw-primary);
    color: #241d12;
    font-weight: 700;
}
#so-booking-widget[data-template="dusk"] .so-bw-slot-group-title {
    color: var(--so-bw-ds-n500);
}

/* ── F9 · UNDERLINE FIELDS ───────────────────────────────────────────────
   A filled input box on a dark canvas is another lit rectangle. The rule
   under the field is enough to say "type here", and it is the same hairline
   the choice list uses, so the two steps read as one document.

   THE RESTING UNDERLINE IS NOT n100. #2a2f3d on #14161d is about 1.3:1 — an
   affordance you cannot see is not an affordance, and WCAG 2.2 wants 3:1 for
   the boundary of a control. n300 (#4a4f5e on this template) measures 3.1:1.
   That one token is the difference between this primitive being a design and
   being an accessibility defect, so it does not get "toned down" later. */
#so-booking-widget[data-template="dusk"] .so-bw-form {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 18px 20px;
    align-items: start;
}
#so-booking-widget[data-template="dusk"] .so-bw-form-row {
    display: contents;
}
#so-booking-widget[data-template="dusk"] .so-bw-form-group:has(.so-bw-textarea) {
    grid-column: 1 / -1;
}
#so-booking-widget[data-template="dusk"] .so-bw-input,
#so-booking-widget[data-template="dusk"] .so-bw-textarea,
#so-booking-widget[data-template="dusk"] .so-bw-select {
    border: 0;
    border-bottom: 1px solid var(--so-bw-ds-n300);
    border-radius: 0;
    background: transparent;
    padding-left: 0;
    padding-right: 0;
}
#so-booking-widget[data-template="dusk"] .so-bw-select {
    /* The chevron is painted as a background image on the right edge, so the
       right inset has to come back for the select alone or the arrow lands on
       top of the value. */
    padding-right: 28px;
}
#so-booking-widget[data-template="dusk"] .so-bw-input:focus,
#so-booking-widget[data-template="dusk"] .so-bw-textarea:focus,
#so-booking-widget[data-template="dusk"] .so-bw-select:focus {
    border-bottom-color: var(--so-bw-primary);
    border-bottom-width: 2px;
    box-shadow: none;
    outline: none;
    /* The 1px→2px swap would move the field's content box up by a pixel on
       focus and nudge the whole row. Absorbed here so the text stays put. */
    padding-bottom: calc(var(--so-bw-input-pad-y) - 1px);
}
#so-booking-widget[data-template="dusk"] .so-bw-label {
    color: var(--so-bw-ds-n500);
    font-size: 12px;
}
/* was @media (max-width:768px) — now gated on the CONTAINER width
   via data-cw (see booking-widget.js _wireContainerWidth). :where()
   keeps specificity identical to the media query it replaces. */

    #so-booking-widget:where([data-cw~="768"])[data-template="dusk"] .so-bw-form{
        grid-template-columns: 1fr;
    }


/* ── R12 · DOCKED TOTAL ──────────────────────────────────────────────────
   The bar at the bottom already carries the money for the whole journey, so
   the review does not need a second, louder total — it needs the DETAIL, and
   the same hairlines the rest of the template is built from.

   The block-level rules from 17.0 are turned off here, because a section that
   is already ruled row by row does not want a heavier line every fourth row:
   that reads as a table with random groupings. */
#so-booking-widget[data-template="dusk"] .so-bw-summary {
    border: 0;
    border-radius: 0;
    background: none;
    padding: 0;
}
#so-booking-widget[data-template="dusk"] .so-bw-summary-section {
    border: 0;
    padding: 0;
}
#so-booking-widget[data-template="dusk"] .so-bw-summary-section-title {
    margin: 22px 0 2px;
    padding-bottom: 8px;
    border-bottom: 1px solid var(--so-bw-primary);
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}
#so-booking-widget[data-template="dusk"] .so-bw-summary-section:first-child .so-bw-summary-section-title {
    margin-top: 0;
}
#so-booking-widget[data-template="dusk"] .so-bw-summary-row {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, auto);
    gap: 16px;
    align-items: baseline;
    padding: var(--so-bw-row-pad-y) 0;
    border-bottom: 1px solid var(--so-bw-ds-n100);
}
#so-booking-widget[data-template="dusk"] .so-bw-summary-row--wide {
    grid-template-columns: 1fr;
    gap: 4px;
}
#so-booking-widget[data-template="dusk"] .so-bw-summary-value {
    text-align: right;
    color: var(--so-bw-ds-n900);
}
#so-booking-widget[data-template="dusk"] .so-bw-summary-row--wide .so-bw-summary-value {
    text-align: left;
}
/* 17.0's `--rule` and `--deposit` add a SECOND top border to rows that
   already carry a bottom one, which on a hairline review is a doubled line
   with 14px of dead space inside it. The rhythm is uniform here. */
#so-booking-widget[data-template="dusk"] .so-bw-summary-row--rule,
#so-booking-widget[data-template="dusk"] .so-bw-summary-row--deposit {
    border-top: 0;
    margin-top: 0;
    padding-top: 11px;
}
#so-booking-widget[data-template="dusk"] .so-bw-summary-block {
    border-top: 0;
    padding-top: 0;
    gap: 0;
}
#so-booking-widget[data-template="dusk"] .so-bw-summary-block--open {
    padding-top: 18px;
}
/* The one loud figure on the screen, and it is champagne on ink. */
#so-booking-widget[data-template="dusk"] .so-bw-summary-row--total {
    margin-top: 4px;
    padding-top: 16px;
    border-top: 1px solid var(--so-bw-primary);
    border-bottom: 0;
}
#so-booking-widget[data-template="dusk"] .so-bw-summary-row--total .so-bw-summary-value {
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
    font-size: 20px;
    font-weight: 700;
}

/* ── N17 · LOW LIGHT ─────────────────────────────────────────────────────
   No green tick. A members' club does not congratulate you for arriving.
   A champagne rule, the time set large in mono, and the record beneath it. */
#so-booking-widget[data-template="dusk"] .so-bw-confirmation-icon {
    width: 46px;
    height: 46px;
    background: none;
    border: 1px solid var(--so-bw-primary);
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}
#so-booking-widget[data-template="dusk"] .so-bw-confirmation-title {
    margin-top: 18px;
    padding-top: 18px;
    border-top: 1px solid var(--so-bw-primary);
    font-size: clamp(22px, calc(var(--so-bw-cw, 100vw) * 0.034), 30px);
    font-weight: 500;
    letter-spacing: -0.01em;
}
#so-booking-widget[data-template="dusk"] .so-bw-confirmation-text {
    color: var(--so-bw-ds-n500);
}
#so-booking-widget[data-template="dusk"] .so-bw-confirmation-details {
    max-width: 420px;
    margin-inline: auto;
    padding: 0;
    border: 0;
    background: none;
}
#so-booking-widget[data-template="dusk"] .so-bw-confirmation-detail {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, auto);
    gap: 14px;
    padding: var(--so-bw-row-pad-y) 0;
    border-bottom: 1px solid var(--so-bw-ds-n100);
}
#so-booking-widget[data-template="dusk"] .so-bw-confirmation-detail:last-child {
    border-bottom: 0;
}
#so-booking-widget[data-template="dusk"] .so-bw-confirmation-detail-value {
    text-align: right;
    color: var(--so-bw-ds-n900);
}

/* ── Dusk · responsive ───────────────────────────────────────────────────
   Two bottom-anchored regions share the foot of this template: the docked
   summary bar (grid-row 3) and the commit nav (grid-row 4). They are grid
   SIBLINGS, not stickies, so they cannot occlude each other — but on a phone
   the bar's horizontal row of five key/value pairs wraps to five lines and
   pushes the commit button below the fold. It lies down into a column at 900
   already (section 14); below 640 the labels go and the values carry
   themselves, which is what a strip has room for. */
/* was @media (max-width: 640px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    #so-booking-widget:where([data-cw~="640"])[data-template="dusk"] .so-bw-slot-list{
        grid-template-columns: repeat(3, minmax(0, 1fr));
    }
    #so-booking-widget:where([data-cw~="640"])[data-template="dusk"] .so-bw-rail{
        padding: 10px var(--so-bw-gutter);
    }

/* was @media (max-width: 480px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    #so-booking-widget:where([data-cw~="480"])[data-template="dusk"] .so-bw-slot-list{
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }
    #so-booking-widget:where([data-cw~="480"])[data-template="dusk"] .so-bw-summary-section-title{
        margin-top: 18px;
    }



/* ==========================================================================
   17.6 · AGENDA  (agenda-indigo, layout key "agenda")
   --------------------------------------------------------------------------
   LIST, NEVER A GRID — AT EVERY STEP.

   Agenda throws the calendar away because most people want the soonest
   convenient opening and a month makes them work for it. Section 14 built
   that for the date step and then let the other eight steps fall through to
   grids: a 2-up card catalogue, a chip grid of times, a two-column summary.
   The thesis held for one screen out of nine.

   Nothing below is a grid. Choices are a running list. Times are a running
   list. The review reads FORWARD through the visit rather than down a table
   of fields, which is the same argument applied to the last screen — and it
   is why R9 exists.
   ========================================================================== */

/* ── C11 · RUNNING LIST ──────────────────────────────────────────────────
   A list, still — one choice per row, full width, never a gallery. What
   changed is the ROW, and it changed because the borderless version was
   reported three times in a row: "the boxes have no border for the height",
   "the text is at the corner", "the page is not looking filled".

   All three describe the same row. It was `border: 0; background:
   transparent`, separated by hairlines, with the duration set in an 84px
   column at the far left and the name beside it — so a 948px row carried its
   entire content in the first 260px and ended in ~690px of nothing, with no
   edge anywhere to say where one choice stopped and the next began. On a
   1280px embed that is a step made almost entirely of empty white.

   THE TEMPLATE ALREADY OWNED THE ANSWER. Its date step (section G) draws each
   day as a white bordered card: 1px n100 rule, card radius, 14/16 padding,
   `justify-content: space-between` so the date holds the left edge and the
   opening count holds the right, 8px between rows. That is a filled row with
   a visible box, and it is Agenda's own language — the service, staff and
   location rows were the odd ones out, not the day rows.

   So the row below IS the day row, with the service's name where the date
   goes and its duration and price where the count goes. Same border, same
   radius, same fill, same padding, same gap. Two lists on two steps that look
   like one design, and no step that reads as unfinished.

   The hairline separators go with it: a bordered card in a gapped list draws
   its own edges, and a `border-bottom` under it would read as a second rule
   floating between two boxes. */
/* `.so-bw-step` is in the first selector to buy specificity, not to scope —
   the same correction Counter Sand's roster had to make (section 24), against
   the same rule. The wide tier in section 21 sets the LOCATION and STAFF grids
   to `repeat(auto-fill, minmax(min(100%, 220px), 1fr))` on
   `:where([data-cwmin~="960"]) .so-bw-step:nth-child(n) .so-bw-card-grid` —
   (1,3,0), because :where() zeroes the attribute but `.so-bw-step:nth-child()`
   still counts twice. A plain `[data-template="agenda"] .so-bw-card-grid` is
   (1,2,0) and loses however late it appears.

   So the template whose thesis is the banner above — LIST, NEVER A GRID, AT
   EVERY STEP — rendered steps 1 and 4 as a four-up gallery in any container
   over 960px. And it did not merely look like the wrong template: the card
   body below is a `84px minmax(0, 1fr)` two-column row built for a full-width
   service line, and 84px of duration column inside a 237px tile left the name
   track computing to ZERO — every specialist's name set one letter per line,
   fifteen rows tall. Measured at 1280px: "priyankaboopathi" down the page.

   Adding the ancestor makes it (1,3,0) and source order decides. */
#so-booking-widget[data-template="agenda"] .so-bw-step .so-bw-card-grid,
#so-booking-widget[data-template="agenda"] .so-bw-card-grid {
    grid-template-columns: 1fr;
    /* 8px, the same air the day list keeps between its rows, and !important
       for the same reason and against the same rule as the exception filed
       beside classic in section 15 rule 7 — the shared `gap: 14px !important`
       floor outranks a plain per-template value however late it appears.
       Restated here so the template's own block reads as the whole
       description of the list: a reader who finds 8 and measures 14 has been
       told where to look. */
    gap: 8px !important;
    /* No rule above the first row. The grid drew one when the rows were
       hairline-separated and it was the list's opening edge; now every row
       carries its own border and a line above the first would sit 1px off the
       top card's own top border. */
    border-top: 0;
}
/* ── The list ran 4px short of everything above it ───────────────────────
   The step's scroll port (section 15 rule 6) is padded `4px 12px 6px 8px` and
   pulled back out with `margin: 0 -8px`. Left cancels exactly, so the cards
   line up with the search field and the heading. Right does not: the gutter is
   12 against a -8 margin, and the extra 4 is there — the rule says so — because
   "the scrollbar is painted in it".

   There is no scrollbar. Section 13 rule 3 took the inner scrollers off every
   list (`max-height: none !important; overflow-y: visible`) and the page
   scrolls instead. What the reservation leaves behind is a list inset 4px on
   the right and flush on the left.

   4px of asymmetric air is invisible on a template whose cards are inset from
   their column anyway. It is not invisible here: this list is full-bleed by
   design — every row runs the whole width of the step, directly under a search
   field that does the same — so the two disagreed about where the right edge
   was, on every row, all the way down.

   THAT DIAGNOSIS WAS RIGHT AND IS NOW FIXED AT THE SOURCE. Agenda was the only
   template where the 4px showed plainly enough to be chased down, so it was
   patched here by shrinking the right PADDING to meet the margin. It was never
   an Agenda problem: the same -4px was measured on Split, Dialog, Inline,
   Timeline, Dusk, Editorial, Roster and Workspace, and section 15 rule 6 now
   mirrors the margin to the padding for all of them.

   With the general rule correct, this local patch became an overshoot in the
   other direction — 8px of padding against a -12px margin put Agenda's rows
   4px PAST the search field instead of 4px inside it — so it is removed rather
   than re-tuned. Agenda keeps the full 12px gutter it always had room for, and
   its edges land on the step's margins because the shared rule now says so. */
#so-booking-widget[data-template="agenda"] .so-bw-card-grid:not(:has(.so-bw-card--has-extras-open)) {
    grid-auto-rows: auto;
}
/* ...and the same policy one level down, which was missed.
   `.so-bw-card-meta--clamp` carries `min-height: calc(2 * 1.45em)` so a
   one-line description cannot leave its card shorter than the card BESIDE it.
   That is right in a multi-column grid and meaningless here: this template is
   a single column (`grid-template-columns: 1fr` above), so no card is ever
   beside another one to match.

   What it bought instead was two blank reserved lines under every row —
   including the services that carry no description at all, which is most of
   them, because the box renders for the whole list as soon as ANY service has
   one. Measured: a row 110px tall carrying ~20px of text, with 38px of empty
   description under the name and the hairline stranded well below it. That
   void is what reads as "step 1 is not aligned".

   The rule directly above already opted this template out of row levelling for
   exactly the same reason. This is the other half of the same decision. */
#so-booking-widget[data-template="agenda"] .so-bw-card-meta--clamp {
    min-height: 0;
}
/* The row, stated exactly as .so-bw-agenda-day states it (section G). Same
   border, same fill, same padding rhythm — the radius is the one property it
   cannot set here, because the "sharp seven" block near the end of the file
   is later in source at the same specificity; Agenda has been moved out of
   that list and into the rounded set instead, and the reason is recorded
   there.

   `min-height` is the "height" half of the report. A service with no
   description and a short name measured 60px while the one under it, carrying
   two lines, measured 96 — a column of boxes no two of which were the same
   size. 72px is the day row's own height (14 + 15px line + 14, plus its
   border), so the two lists sit on one rhythm and a row is a row whatever is
   written in it. Cards with more to say still grow past it. */
#so-booking-widget[data-template="agenda"] .so-bw-card:not(.so-bw-sp__panel) {
    min-height: 72px;
    border: 1px solid var(--so-bw-ds-n100);
    background: var(--so-bw-ds-n0);
    box-shadow: none;
    transition: border-color var(--so-bw-ds-motion), background var(--so-bw-ds-motion);
}
/* Hover reads on the EDGE, not on the fill — a bordered box already has an
   outline to light up, and the day row's hover says the same thing. */
#so-booking-widget[data-template="agenda"] .so-bw-card:not(.so-bw-sp__panel):hover {
    border-color: var(--so-bw-primary);
    background: var(--so-bw-ds-n25);
}
/* Selected: the edge carries it, exactly as hover does and exactly as the day
   row does. The wash and the check come from the platform. */
#so-booking-widget[data-template="agenda"] .so-bw-card--selected:not(.so-bw-sp__panel) {
    border-color: var(--so-bw-primary);
    box-shadow: inset 3px 0 0 var(--so-bw-primary);
}
/* ── The selection RING still does not belong on this row ────────────────
   The platform draws selection as a 2px ring in `.so-bw-card--selected::after`
   with `inset: calc(-1 * border-width)` and `border-radius: inherit`, i.e.
   just OUTSIDE the card's own border.

   That was written for a card whose border is the only edge it has. This row
   now has a border of its own AND a 3px accent bar inside it, so the ring
   would be a third edge on the same 1px of geometry: at 8px of radius the
   outer 2px ring and the inner 1px border produce a visibly doubled corner,
   and the ring standing proud of the card is what makes a gapped list of them
   look ragged — the rows no longer share a common left edge with the search
   field above.

   (Before the row was a box at all, the fault was worse and differently
   shaped: with `border: 0` and square corners the ring drew a hard rectangle
   1px proud of a full-bleed list, cutting through the hairlines above and
   below it. Same rule, same decision, different reason.)

   Matched on both attributes the tag always carries, (1,4,0) against the
   platform rule's (1,3,0), so it wins on specificity rather than on source
   order. */
#so-booking-widget[data-template="agenda"][data-template-id] .so-bw-card--selected:not(.so-bw-sp__panel)::after {
    display: none;
}
/* !important, and it is not decoration — it is the ONLY thing that makes the
   paragraph above true. The block that moved selection off box-shadow and onto
   the ::after overlay (section 15, ~14882) also nulls the property it vacated,
   at `box-shadow: none !important`, for every card on every template. So this
   template ended up with the ring switched off by the rule above and the bar it
   was traded for switched off by that one: a selected row painted the 15% wash
   and the check and NOTHING on its edge. Specificity cannot answer an
   !important; only another one can. */
#so-booking-widget[data-template="agenda"][data-template-id] .so-bw-card--selected:not(.so-bw-sp__panel) {
    box-shadow: inset 3px 0 0 var(--so-bw-primary) !important;
}
#so-booking-widget[data-template="agenda"] .so-bw-card-media {
    display: none;
}
/* The badges row normally sits UNDER the title and description, at the foot
   of a stacked card. In a running list it belongs on the row's far side, in
   its own column, where it can be scanned without reading anything else — and
   where it holds the right edge the way the day row's opening count does.
   Moved by grid placement, not in the DOM, so the reading order for a screen
   reader stays name-then-duration — correct, because the name is what
   identifies the row. */
#so-booking-widget[data-template="agenda"] .so-bw-card--service .so-bw-card-top {
    align-items: flex-start;
}
/* `--service`, and the guard is load-bearing. This is the SERVICE row: an 84px
   duration column, then the name. The staff and location lists share
   `.so-bw-card-body`, and neither has a duration to put in that column — what
   they do have is a description (`.so-bw-card-meta`, without the `--clamp`
   modifier that maps to the `desc` area) and a "View profile" link, both of
   which auto-placed into the first free cell, i.e. the empty 84px gutter. So
   "We'll assign the first available staff member." set five lines deep in an
   84px column beside a name pushed 180px off the avatar, and the row measured
   133px against the 81px of the ones under it.
   Unguarded, the row shape for one step was being applied to three. */
/* NAME LEFT, DURATION AND PRICE RIGHT — the day row's own arrangement.

   It used to be the other way round: an 84px duration column first, then the
   name. That put every word in the row inside its first quarter and left the
   rest of a 948px card empty, which is the "text is at the corner / the page
   is not filled" half of the report. The `auto` track is shrink-to-fit and
   `justify-self: end` pins it, so the meta reaches the row's right padding at
   any width and the name takes everything between. */
#so-booking-widget[data-template="agenda"] .so-bw-card--service .so-bw-card-body {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas:
        "title meta"
        "desc  meta"
        "more  meta";
    gap: 0 18px;
    align-items: baseline;
}
/* The ADD-ON row is the same shape of fact as the service row — a name, and
   what it costs in time and money — so it takes the same arrangement. It is
   stated separately rather than folded into the selector above because its
   description is a plain `.so-bw-card-meta` (no `--clamp` modifier, which is
   what maps to the `desc` area), and an unplaced child auto-fills the first
   free cell: without the second rule the blurb would land in the meta column
   and push the duration out, which is the fault the staff list was carrying.

   Unverified against a live add-on — no service on this install has one, so
   the step never renders. The shape is inherited from the service row rather
   than invented, which is the safest form an unverified rule can take. */
#so-booking-widget[data-template="agenda"] .so-bw-card--addon .so-bw-card-body {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas:
        "title meta"
        "desc  meta";
    gap: 0 18px;
    align-items: baseline;
}
#so-booking-widget[data-template="agenda"] .so-bw-card--addon .so-bw-card-meta {
    grid-area: desc;
}
#so-booking-widget[data-template="agenda"] .so-bw-card-title {
    grid-area: title;
}
#so-booking-widget[data-template="agenda"] .so-bw-card-meta--clamp {
    grid-area: desc;
}
#so-booking-widget[data-template="agenda"] .so-bw-card-more {
    grid-area: more;
    justify-self: start;
}
#so-booking-widget[data-template="agenda"] .so-bw-card-badges {
    grid-area: meta;
    display: flex;
    flex-direction: column;
    /* Right-aligned, and pinned to the end of its track: the duration and the
       price stack on the row's right edge under each other, so a column of
       rows lines its numbers up on the right the way the day list lines up its
       opening counts. */
    align-items: flex-end;
    justify-self: end;
    text-align: right;
    gap: 4px;
    /* Beats `.so-bw-card--service .so-bw-card-badges { margin-top: auto }`,
       which pushes the badges to the bottom of a stacked card — wrong once
       they are a column of their own. */
    margin-top: 0;
    padding-top: 0;
}
#so-booking-widget[data-template="agenda"] .so-bw-badge--duration {
    background: none;
    border: 0;
    padding: 0;
    color: var(--so-bw-text);
    font-family: var(--so-bw-mono);
    font-variant-numeric: tabular-nums;
    font-size: 14px;
    font-weight: 600;
}
#so-booking-widget[data-template="agenda"] .so-bw-badge--price,
#so-booking-widget[data-template="agenda"] .so-bw-badge--free {
    background: none;
    border: 0;
    padding: 0;
    color: var(--so-bw-text-secondary);
    font-family: var(--so-bw-mono);
    font-size: 12px;
}

/* ── Agenda · TIMES ──────────────────────────────────────────────────────
   The day list hands over to the times, and the times must not resolve into
   the chip grid the whole template exists to avoid. Full-width rows, the
   start time set as the line the eye lands on, the range beside it.

   The "Back to days" control is sticky because the forward list is the thing
   the customer is navigating: on a long day they should be able to leave the
   times without scrolling back up to find the way out. */
/* THE SAME ROW AS THE TWO LISTS BEFORE IT. A customer reaches this list by
   passing through the service list and the day list, and until now the third
   one contradicted both: borderless, hairline-separated, the time set hard
   against the left edge of a panel it did not share edges with, and the whole
   right half of every row empty. Same three faults the service step was
   reported for, one step later.

   Card, not hairline — 1px n100, --so-bw-ds-radius-card, white, 8px between
   rows — and the row is FILLED: the start time holds the left edge and the
   end time is pushed to the right, which is what `space-between` on the
   range wrapper below does. A slot is a span of time, so the two ends of it
   are the two ends of the row. */
#so-booking-widget[data-template="agenda"] .so-bw-slot-list {
    display: flex;
    flex-direction: column;
    gap: 8px;
    border-top: 0;
}
#so-booking-widget[data-template="agenda"] .so-bw-slot-row {
    justify-content: flex-start;
    gap: 14px;
    min-height: 56px;
    padding: 14px 16px;
    border: 1px solid var(--so-bw-ds-n100);
    border-radius: var(--so-bw-ds-radius-card);
    background: var(--so-bw-ds-n0);
    box-shadow: none;
    text-align: left;
    font-size: 15px;
    font-weight: 600;
    transition: border-color var(--so-bw-ds-motion), background var(--so-bw-ds-motion);
}
/* The range wrapper is an inline span carrying both times (it exists so the
   space between them survives — see the markup's own note). Made a flex row
   that spans the card, it becomes the row's layout: start left, end right.
   `width: 100%` rather than `flex: 1`, because the flag below is an optional
   third item and the range must not shrink when it appears. */
#so-booking-widget[data-template="agenda"] .so-bw-slot-range {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 12px;
    width: 100%;
}
#so-booking-widget[data-template="agenda"] .so-bw-slot-till {
    font-weight: 500;
    color: var(--so-bw-text-secondary);
}
/* Hover and selection read on the EDGE, matching the service and day rows.
   The 10% wash was tuned against a transparent row; on white it sat too close
   to the n25 hover to tell the two apart, so selection now carries the border
   and the bar as well — the same three signals the service card uses. */
#so-booking-widget[data-template="agenda"] .so-bw-slot-row:hover:not(.so-bw-slot-row--booked) {
    border-color: var(--so-bw-primary);
    background: var(--so-bw-ds-n25);
}
#so-booking-widget[data-template="agenda"] .so-bw-slot-row--selected {
    border-color: var(--so-bw-primary);
    background: color-mix(in srgb, var(--so-bw-primary) 12%, transparent);
    color: var(--so-bw-text);
    box-shadow: inset 3px 0 0 var(--so-bw-primary);
}
/* A full slot must not look bookable: no white card, no hover edge. */
#so-booking-widget[data-template="agenda"] .so-bw-slot-row--booked {
    background: var(--so-bw-ds-n25);
    border-style: dashed;
}
#so-booking-widget[data-template="agenda"] .so-bw-slot-row-flag {
    margin-left: auto;
    font-size: 12px;
}
#so-booking-widget[data-template="agenda"] .so-bw-slot-back {
    position: sticky;
    top: 0;
    z-index: 2;
    /* Opaque, or the rows scroll visibly through it. */
    background: var(--so-bw-bg);
    padding-block: 8px;
}

/* ── F11 · AXIS FIELDS ───────────────────────────────────────────────────
   One column, each group hung off a 2px rule so the form reads down the page
   the way the list above it does. Shared with Timeline, which is the other
   template built on a forward line.

   An axis item is a DIRECT CHILD of `.so-bw-form`, and that distinction is
   load-bearing rather than tidiness. `.so-bw-form-row` is ONE item holding the
   First/Last name pair, not two. Hanging the rule off every
   `.so-bw-form-group` instead — which is what these selectors used to do —
   gave the nested Last Name group its own axis, and three things followed:

     • `:last-child` is scoped to its own parent, so Last Name ALSO matched the
       "stop the rule here" branch below and drew it as a short inset stub
       floating between the two columns, attached to nothing above or below.
     • Its `::before` node was rendered against that stub, mid-form.
     • Its 18px indent + 2px border pushed the second column 20px further right
       than the first, so the gutter between the name fields measured 36px
       against the row's declared 16px and the pair sat off the form's rhythm.

   The `>` combinator is what fixes all three: a group inside a row is not a
   direct child of the form, so it is simply not an axis item any more. */
#so-booking-widget[data-template="agenda"] .so-bw-form,
#so-booking-widget[data-template="timeline"] .so-bw-form {
    display: flex;
    flex-direction: column;
    gap: 0;
}
#so-booking-widget[data-template="agenda"] .so-bw-form-row,
#so-booking-widget[data-template="timeline"] .so-bw-form-row {
    display: flex;
    flex-wrap: wrap;
    gap: 0 16px;
}
#so-booking-widget[data-template="agenda"] .so-bw-form-row > .so-bw-form-group,
#so-booking-widget[data-template="timeline"] .so-bw-form-row > .so-bw-form-group {
    flex: 1 1 220px;
    min-width: 0;
}
#so-booking-widget[data-template="agenda"] .so-bw-form > .so-bw-form-group,
#so-booking-widget[data-template="agenda"] .so-bw-form > .so-bw-form-row,
#so-booking-widget[data-template="timeline"] .so-bw-form > .so-bw-form-group,
#so-booking-widget[data-template="timeline"] .so-bw-form > .so-bw-form-row {
    /* Gutter = the item's left padding + half its border, i.e. how far the
       label's edge sits from the rule's CENTRE. The node offset reads this. */
    --so-bw-axis-gutter-f11: calc(18px + var(--so-bw-axis-line-w) / 2);
    padding: 0 0 16px 18px;
    border-left: var(--so-bw-axis-line-w) solid var(--so-bw-axis-line);
}
/* Two fields sharing one axis item share ONE node — the first one's, which is
   the only label sitting on the rule. Without this the second column drew a
   dot at its own -23px, i.e. in the middle of the form. */
#so-booking-widget[data-template="agenda"] .so-bw-form-row > .so-bw-form-group:not(:first-child) .so-bw-label::before,
#so-booking-widget[data-template="timeline"] .so-bw-form-row > .so-bw-form-group:not(:first-child) .so-bw-label::before {
    content: none;
}
/* The rule stops at the last field rather than running on into the footer,
   where it read as "there is more below" when there was not. Same correction
   the Timeline axis already carries at .so-bw-slot-group:last-child. */
#so-booking-widget[data-template="agenda"] .so-bw-form > .so-bw-form-group:last-child,
#so-booking-widget[data-template="agenda"] .so-bw-form > .so-bw-form-row:last-child,
#so-booking-widget[data-template="timeline"] .so-bw-form > .so-bw-form-group:last-child,
#so-booking-widget[data-template="timeline"] .so-bw-form > .so-bw-form-row:last-child {
    border-left-color: transparent;
    box-shadow: inset 2px 0 0 var(--so-bw-ds-n100);
    /* The inset shadow paints the same 2px line but only as far as the
       element's own box, and the element ends at its last field. */
}
#so-booking-widget[data-template="agenda"] .so-bw-label,
#so-booking-widget[data-template="timeline"] .so-bw-label {
    position: relative;
}
/* The node on the line, matching the day axis's own 10px node. */
/* The node on the line. The label's own left edge sits one item-padding plus
   one border in from the rule, so that distance IS the gutter — derived here
   rather than typed as the -23px it used to be. */
#so-booking-widget[data-template="agenda"] .so-bw-label::before,
#so-booking-widget[data-template="timeline"] .so-bw-label::before {
    content: '';
    position: absolute;
    left: calc((var(--so-bw-axis-gutter-f11) + var(--so-bw-axis-node) / 2) * -1);
    top: 50%;
    width: var(--so-bw-axis-node);
    height: var(--so-bw-axis-node);
    margin-top: calc(var(--so-bw-axis-node) / -2);
    border-radius: 50%;
    background: var(--so-bw-card-bg);
    border: var(--so-bw-axis-line-w) solid var(--so-bw-axis-ring);
}

/* ── R9 · ITINERARY ──────────────────────────────────────────────────────
   Shared with Timeline. The visit read forward: arrive, each service in the
   order it runs, finish. Built on the .so-bw-itinerary markup, which is
   emitted for every template and revealed for exactly these two.

   THE SHARE WITH TIMELINE IS DELIBERATE, AND THIS IS THE RECORD OF THAT.
   R9 and F11 are the only two screens where Timeline and Agenda draw the same
   pane, and an earlier audit flagged them as the redesign's likeliest way of
   collapsing back into "Timeline is Agenda in another hue" — the exact fault
   that cost Timeline a promotion once already. The decision is to KEEP the
   share, for two reasons:

     1. Both templates argue the same thing here — that a booking is a shape in
        time, not a table of fields — so drawing it two ways would be
        decoration, not identity.
     2. The screens are still told apart, because the CHROME differs on every
        step: Agenda's sidebar is four summary facts over a total, Timeline's is
        six numbered steps on a connected rail. A greyscale reader identifies
        the template from the sidebar without reading the pane at all.

   So the share is scoped to the pane and is not allowed to grow. If a future
   change makes the two sidebars converge, this share stops being safe and the
   panes have to diverge instead.

   The Date and Time summary rows are suppressed BELOW, and only for these
   two, because this block states both — a fact should appear once per screen.
   Nothing else is hidden: duration, timezone, custom fields, contact details
   and the whole price breakdown are untouched. */
#so-booking-widget .so-bw-itinerary { display: none; }
#so-booking-widget[data-template="agenda"] .so-bw-itinerary,
#so-booking-widget[data-template="timeline"] .so-bw-itinerary {
    display: block;
    margin: 0 0 20px;
    padding: 16px 18px;
    border: 1px solid var(--so-bw-border);
    border-radius: var(--so-bw-ds-radius-card);
    background: var(--so-bw-bg-secondary);
}
/* Agenda only: white, like every other card the customer has met on the way
   here. Timeline keeps the tinted panel — its steps are drawn on a tinted
   field, so a white card there would be the odd one out for the same reason a
   tinted one is here. */
#so-booking-widget[data-template="agenda"] .so-bw-itinerary {
    background: var(--so-bw-ds-n0);
}
/* ── THE RECAP IS A LIST HERE, NOT A TABLE ───────────────────────────────
   The shared review (section BOUNDED SHELL rule 2) puts the two recap
   sections two-up from 600px, which is right for the templates whose review
   is a dense table. It is wrong here twice over.

   Structurally: the number of visible rows is decided at runtime by x-show
   (location, timezone, custom fields, add-ons all come and go), so an odd
   count leaves the last cell EMPTY — and an empty grid cell draws no bottom
   border, so the panel's horizontal rule stops halfway across and the column
   divider runs into blank space. Measured on this install: "Duration ·
   1h 10min" alone on its row with a hole beside it, three full-width rows
   under it, and a hairline ending mid-panel. That is the ragged edge in the
   report, and it cannot be tuned away — it depends on what the customer
   happened to fill in.

   Thematically: this is the template whose banner reads LIST, NEVER A GRID,
   AT EVERY STEP, and whose own itinerary sits directly above this panel as a
   single column of rows. The recap was the last screen still arguing with it.

   One column, and the divider that only made sense between two goes with it.
   (1,2,0) against the shared rule's (1,2,0) — :where() zeroes the cwmin
   attribute — so this wins on source order, which section 17.6 has. */
#so-booking-widget[data-template="agenda"] .so-bw-summary-section {
    display: block;
}
#so-booking-widget[data-template="agenda"] .so-bw-summary-section > .so-bw-summary-row {
    border-inline-start: 0;
    margin-inline-start: 0;
}
#so-booking-widget .so-bw-itinerary-head {
    margin-bottom: 12px;
    padding-bottom: 10px;
    border-bottom: 1px solid var(--so-bw-border);
    font-size: 14px;
    font-weight: 700;
    color: var(--so-bw-text);
}
#so-booking-widget .so-bw-itinerary-row {
    /* The line runs INSIDE the row here rather than outside it, so the axis
       centre is a positive offset — the one site where it is. */
    --so-bw-axis-x: 4px;
    position: relative;
    display: grid;
    /* minmax(0, auto) on the last track, never a bare auto: a long add-on
       name in the middle column must be allowed to wrap rather than force the
       row wider than the card. */
    grid-template-columns: 74px minmax(0, 1fr) minmax(0, auto);
    gap: 4px 14px;
    align-items: baseline;
    padding: var(--so-bw-row-pad-y) 0 var(--so-bw-row-pad-y) 20px;
}
/* The line the rows hang off, drawn once per row so it is continuous through
   the gaps without a wrapper element to hang a single line on. */
#so-booking-widget .so-bw-itinerary-row::before {
    content: '';
    position: absolute;
    left: calc(var(--so-bw-axis-x) - var(--so-bw-axis-line-w) / 2);
    top: 0;
    bottom: 0;
    width: var(--so-bw-axis-line-w);
    background: var(--so-bw-axis-ring);
}
#so-booking-widget .so-bw-itinerary-row:first-of-type::before { top: 50%; }
#so-booking-widget .so-bw-itinerary-row:last-of-type::before { bottom: 50%; }
#so-booking-widget .so-bw-itinerary-row::after {
    content: '';
    position: absolute;
    left: calc(var(--so-bw-axis-x) - var(--so-bw-axis-node) / 2);
    top: 50%;
    width: var(--so-bw-axis-node);
    height: var(--so-bw-axis-node);
    margin-top: calc(var(--so-bw-axis-node) / -2);
    border-radius: 50%;
    background: var(--so-bw-card-bg);
    border: var(--so-bw-axis-line-w) solid var(--so-bw-ds-n300);
}
#so-booking-widget .so-bw-itinerary-row--arrive::after,
#so-booking-widget .so-bw-itinerary-row--finish::after {
    background: var(--so-bw-primary);
    border-color: var(--so-bw-primary);
}
#so-booking-widget .so-bw-itinerary-time {
    font-family: var(--so-bw-mono);
    font-variant-numeric: tabular-nums;
    font-size: 13px;
    font-weight: 600;
    color: var(--so-bw-text);
    white-space: nowrap;
}
#so-booking-widget .so-bw-itinerary-label {
    min-width: 0;
    font-size: 14px;
    overflow-wrap: anywhere;
}
#so-booking-widget .so-bw-itinerary-row--arrive .so-bw-itinerary-label,
#so-booking-widget .so-bw-itinerary-row--finish .so-bw-itinerary-label {
    font-weight: 600;
    color: var(--so-bw-text-secondary);
}
#so-booking-widget .so-bw-itinerary-dur {
    font-family: var(--so-bw-mono);
    font-variant-numeric: tabular-nums;
    font-size: 12px;
    color: var(--so-bw-text-secondary);
    white-space: nowrap;
}
/* Stated once, above, so not stated twice. */
#so-booking-widget[data-template="agenda"] .so-bw-summary-row--date,
#so-booking-widget[data-template="agenda"] .so-bw-summary-row--time,
#so-booking-widget[data-template="timeline"] .so-bw-summary-row--date,
#so-booking-widget[data-template="timeline"] .so-bw-summary-row--time {
    display: none;
}
/* Below the phone tier the three-track row is 74 + content + duration in
   about 300px, which wraps the add-on name to four words a line. The duration
   drops under the label, where it still reads and costs no width. */
/* was @media (max-width: 480px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    #so-booking-widget:where([data-cw~="480"]) .so-bw-itinerary-row{
        grid-template-columns: 66px minmax(0, 1fr);
    }
    #so-booking-widget:where([data-cw~="480"]) .so-bw-itinerary-dur{
        grid-column: 2;
    }
    #so-booking-widget:where([data-cw~="480"])[data-template="agenda"] .so-bw-itinerary,
    #so-booking-widget:where([data-cw~="480"])[data-template="timeline"] .so-bw-itinerary{
        padding: 14px;
    }


/* ── N10 · NEXT IN LIST ──────────────────────────────────────────────────
   The confirmation is the top entry of a forward list, and what happens next
   is listed under it. Consistent with the one idea this template has: things
   that are coming, in the order they come. */
#so-booking-widget[data-template="agenda"] .so-bw-confirmation-details {
    max-width: 460px;
    margin-inline: auto;
    padding: 0;
    border: 0;
    border-top: 2px solid var(--so-bw-primary);
    /* The base component is a CARD: background, a border on all four sides and
       `border-radius: var(--so-bw-radius-lg)`. This template keeps only the top
       edge — so the radius has nothing left to round, and instead deforms the
       one rule that IS still drawn. At the stock 15px it bends the 2px accent
       line into two hooks curving down at each end and pulls its visible start
       ~15px inward, while the row hairlines underneath still run the full 460px.
       Three different left edges inside one card, which is what reads as "the
       border radius and the alignment are wrong".

       Overriding the border, the background and the padding without also
       overriding the radius is the same omission section 17 records for the
       two-column forms: a rule that REPLACES a primitive's look has to own
       every declaration that look depends on, and `border-radius` is one of
       them. */
    border-radius: 0;
    background: none;
}
#so-booking-widget[data-template="agenda"] .so-bw-confirmation-detail {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, auto);
    gap: 14px;
    /* Flush, not 2px in. The accent rule above and the hairline below each row
       both span the card's full width, so a 2px inset put the label and the
       value on an edge nothing else in the block shares — text sitting just
       proud of the rule it is meant to hang from. Zero makes the accent rule,
       the separators, the labels and the values one single edge, which is the
       whole point of a list this template presents as a forward-reading table. */
    padding: 12px 0;
    border-bottom: 1px solid var(--so-bw-ds-n100);
}
#so-booking-widget[data-template="agenda"] .so-bw-confirmation-detail:last-child {
    border-bottom: 0;
}
#so-booking-widget[data-template="agenda"] .so-bw-confirmation-detail-value {
    text-align: right;
    font-family: var(--so-bw-mono);
    font-variant-numeric: tabular-nums;
}

/* ── Agenda · responsive ─────────────────────────────────────────────────
   The meta column is the only thing here that can fail on a narrow screen.
   The row is `minmax(0, 1fr) auto` — name left, duration and price pinned
   right — and `auto` is shrink-to-fit, so on a 360px card the meta keeps the
   ~110px "1h 10min / £499.98" needs and the name is left with about 130.
   "Aromatherapy Massage" then sets four lines beside two.

   Below 480 the row stacks instead, and the name goes FIRST: it is what
   identifies the row, and once there is no column to scan the duration down,
   leading with it buys nothing. (It used to lead, back when the desktop row
   put the meta in a left-hand column and the stack was preserving that scan
   order. The desktop row no longer does, so neither does this.)

   BOTH RULES CARRY `.so-bw-card--service`, and that is not decoration: the
   desktop rule they exist to override gained the same guard when the staff
   and location lists had to stop inheriting the service row's shape, which
   took it to (1,3,0) — above these at (1,2,0). Matching the guard restores
   the tie, and a media-query-derived block that comes later in the file wins
   it. Without this the phone tier silently stopped applying to the one card
   type it was written for. */
/* was @media (max-width: 480px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    #so-booking-widget:where([data-cw~="480"])[data-template="agenda"] .so-bw-card--service .so-bw-card-body{
        grid-template-columns: minmax(0, 1fr);
        grid-template-areas:
            "title"
            "desc"
            "meta"
            "more";
        gap: 2px;
    }
    #so-booking-widget:where([data-cw~="480"])[data-template="agenda"] .so-bw-card--service .so-bw-card-badges{
        flex-direction: row;
        align-items: baseline;
        /* Back to the left edge, under the name it belongs to. The right-hand
           pin is a two-column device and there is no second column here. */
        justify-self: start;
        text-align: left;
        gap: 10px;
    }
    #so-booking-widget:where([data-cw~="480"])[data-template="agenda"] .so-bw-slot-row{
        font-size: 14px;
    }



/* ==========================================================================
   17.1 · CENTRED  (cloud-violet, layout key "centred")
   --------------------------------------------------------------------------
   ONE MEASURE, ALL THE WAY DOWN.

   Centred is the safe default and the reference implementation, so its
   identity cannot be a device — a device would make it the opinionated one.
   Its identity is a CONSTANT: every step's content sits in the SAME column,
   centred, so the eye never has to re-acquire the left edge between screens.
   Nothing else in the catalogue holds a single measure end to end, and it is
   the quietest way a template can be recognisable.

   That column was 520px and is now the full width of the step (see
   --so-bw-measure below). The constant is the SHARED EDGE, not the number —
   this note used to name the number, which left the next reader deriving a
   geometry the stylesheet had stopped producing.

   The measure goes on the GROUP, never on the list alone. Centred already
   learned that with the slot groups: centring only .so-bw-slot-list left its
   own "Morning" heading at the panel's full width, visibly detached from the
   chips it labelled, up to 345px away. Same trap, same fix, for the card grid
   and the review.
   ========================================================================== */

/* THE MEASURE IS NOW THE FULL COLUMN. It was 520px, and that produced exactly
   the reading the section note above describes — one constant column, centred,
   end to end. What it also produced, once the search field and the group were
   brought into it, was a 520px card sitting under a header that runs the
   widget's whole width: the step's content box is 660px at a 1000px embed, so
   every row gave away 70px on each side while the heading above it did not.

   100% keeps the mechanism and drops the clamp. Every rule that reads
   --so-bw-measure — the card grid, the service group, the search field, the
   step description, the form, the review, the confirmation and the slot
   groups — still resolves to ONE width and still centres on ONE axis, so the
   left edge is as constant from step to step as it ever was. It is simply the
   column's own width now rather than a narrower one inside it, which is what
   the phone tier below has always done (it releases the measure to 100% rather
   than shrinking it).

   Keeping the single measure is what makes this a one-value change: the
   alignment work does not have to be redone to widen the list, because
   nothing is sized independently of it any more. */
#so-booking-widget[data-template="centred"] .so-bw-container {
    --so-bw-measure: 100%;
}

/* ── C4 · SINGLE MEASURE ─────────────────────────────────────────────────
   One column of full-width rows: a real check indicator at the left, the name
   and description in the middle, price and duration right on a shared
   baseline. It reads as a form question with options rather than as a
   catalogue, which is the register the rest of this template is in. */
#so-booking-widget[data-template="centred"] .so-bw-card-grid {
    grid-template-columns: 1fr;
    gap: 8px;
}
/* THE GRID IS NOT THE SAME ELEMENT ON EVERY STEP, and C4b's note used to claim
   it was: "the grid keeps its own cap for the steps that have no group wrapper
   (staff and location render the grid directly), where it is already correct."
   That sentence was false, and it is why the grid was the one block left out of
   the width:100% fix.

   On the SERVICE step the grid is a block child of `.so-bw-service-group`, and
   the pattern is harmless. On the ADD-ONS step (js:2100) it is a direct FLEX
   child of `.so-bw-step`, so the auto inline margin cancelled its stretch and
   it shrank to its own content: 212px of add-on names inside a 660px column,
   +224px of dead space on each side, at every container width — and the width
   moved with the longest add-on name, so two installs of the same template
   laid out differently.

   On the LOCATION and STAFF steps (js:1721, js:2143) the grid is ALSO the bleed
   clipper, `.so-bw-card-grid--scroll`, and there `margin-inline: auto` did a
   second kind of damage: it overwrote the mirrored -8px/-12px gutter margins
   that section 15 rule 6 sets on that same element at the same specificity, so
   the cards lost the compensation that lines them up with the search field and
   sat inset by the padding instead. Those two steps also collapsed outright
   below the 960 container bucket, where the `:nth-child(1)`/`:nth-child(4)`
   escape hatch at section 21 stops applying — 950px was the cliff, +251px on
   location and +122px on staff.

   So the column goes on the grid only where the grid is NOT the clipper. The
   scroll variant keeps its bleed margins and stretches as a flex item should,
   which is what makes its edges land on the step's own margins at every tier
   including the phone one — restating the numbers here would fork them again. */
#so-booking-widget[data-template="centred"] .so-bw-card-grid:not(.so-bw-card-grid--scroll) {
    width: 100%;
    max-width: var(--so-bw-measure);
    margin-inline: auto;
}
/* ── C4b · THE REST OF THE GROUP, IN THE SAME MEASURE ────────────────────
   The measure went on the card grid and stopped there, and the section note
   twelve rules up already says why that is not enough: "The measure goes on
   the GROUP, never on the list alone. Centred already learned that with the
   slot groups: centring only .so-bw-slot-list left its own 'Morning' heading
   at the panel's full width, visibly detached from the chips it labelled."

   The service step is the same trap, unlearned. Measured at a 1000px embed:
   the step description, the search field and the category heading all ran the
   step's full 660px, while the list they belong to sat in its 520px column —
   a 70px shelf of empty space on each side of the cards, with a search field
   above them that was visibly wider than everything it filtered. The search
   bar is `position: sticky`, so on a long catalogue it stays on screen while
   the rows scroll underneath it, and the mismatched right edge is drawn
   against every row in turn.

   It also broke the promise the template is named for. The form, the review
   and the confirmation each carry the measure already, so they centre on the
   step and land on 235/755; the service list centres inside the scroll port
   and — until the gutter was mirrored in section 15 rule 6 — landed on
   233/753. "The eye never has to re-acquire the left edge between screens"
   was off by two pixels between choosing a service and filling in the form.

   So: the measure goes on every block in the step, not just the list. The
   grid keeps its own cap for the steps that have no group wrapper (staff and
   location render the grid directly), where it is already correct. */
#so-booking-widget[data-template="centred"] .so-bw-step-desc,
#so-booking-widget[data-template="centred"] .so-bw-search,
#so-booking-widget[data-template="centred"] .so-bw-service-group,
#so-booking-widget[data-template="centred"] .so-bw-empty,
#so-booking-widget[data-template="centred"] .so-bw-multi-summary {
    /* `width: 100%` is load-bearing, not belt-and-braces. The step is a COLUMN
       FLEX container, and an auto cross-axis margin on a flex item switches off
       the stretch that was sizing it — so `max-width` + `margin-inline: auto`
       alone left each of these shrink-to-fit. The paragraph survived by
       accident (its text is long enough to reach the cap), but the search field
       collapsed to the intrinsic 265px of its own input and sat centred in the
       middle of the column, narrower than the rows it filters. A definite width
       gives the auto margins something to centre and the cap something to
       clamp. */
    width: 100%;
    max-width: var(--so-bw-measure);
    margin-inline: auto;
}
#so-booking-widget[data-template="centred"] .so-bw-card-grid:not(:has(.so-bw-card--has-extras-open)) {
    grid-auto-rows: auto;
}
/* The check moves to the LEFT, which is where a radio lives in a form. `order`
   does it without touching the DOM, so the accessible name is still read as
   title-then-state rather than state-then-title. */
#so-booking-widget[data-template="centred"] .so-bw-card-top {
    align-items: center;
}
#so-booking-widget[data-template="centred"] .so-bw-card-check {
    order: -1;
    flex: 0 0 auto;
}
/* The check is normally revealed only once selected. As the row's leading
   affordance it has to be visible unselected too, or the list has a ragged
   left edge that snaps straight the moment you pick something. */
#so-booking-widget[data-template="centred"] .so-bw-card-check {
    opacity: 1;
    visibility: visible;
    border: 1.5px solid var(--so-bw-ds-n300);
    background: transparent;
    color: transparent;
}
#so-booking-widget[data-template="centred"] .so-bw-card-check--visible {
    border-color: var(--so-bw-primary);
    background: var(--so-bw-primary);
    color: #fff;
}
#so-booking-widget[data-template="centred"] .so-bw-card-media {
    display: none;
}
#so-booking-widget[data-template="centred"] .so-bw-card-badges {
    margin-top: 6px;
}

/* ── Centred · DATE & TIME ───────────────────────────────────────────────
   The month and the slot groups were already capped and centred at 460 in
   section 14. What was missing is the SEAM: the period heading and its chips
   read as two unrelated things because nothing joined them. A hairline
   running out from the label is the smallest mark that says "these belong
   together", and it costs no height. */
#so-booking-widget[data-template="centred"] .so-bw-slot-group-title {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-bottom: 8px;
}
#so-booking-widget[data-template="centred"] .so-bw-slot-group-title::after {
    content: '';
    flex: 1 1 auto;
    height: 1px;
    background: var(--so-bw-ds-n200);
}

/* ── F1 · STACKED, in the measure ────────────────────────────────────────
   The default stacked form is right here; what it lacked was the column.

   …and the column never actually arrived, because `max-width` plus an auto
   inline margin cannot size a flex item — the step is a column flex container,
   and an auto cross-axis margin cancels the stretch that was doing the sizing.
   Every block written this way was therefore shrink-to-fit at its own
   intrinsic width, centred on the right axis at the wrong size: the form 472,
   the review 380, the month 392, the slot groups 460, the cards 660. Five
   blocks, five widths, one centre line, and no two edges in the whole flow
   lining up. `width: 100%` is what makes the measure mean anything. */
#so-booking-widget[data-template="centred"] .so-bw-form {
    width: 100%;
    max-width: var(--so-bw-measure);
    margin-inline: auto;
}

/* ── R1 · CARD, in the measure ───────────────────────────────────────────
   The default review card, held to the same column, with exactly one bold
   thing on the screen. */
#so-booking-widget[data-template="centred"] .so-bw-summary {
    width: 100%;
    max-width: var(--so-bw-measure);
    margin-inline: auto;
}
#so-booking-widget[data-template="centred"] .so-bw-summary-row--total .so-bw-summary-value {
    font-size: 18px;
    font-weight: 700;
}

/* ── N1 · SEAL ───────────────────────────────────────────────────────────
   Centred tick, title, then the record as a plain two-column list on the same
   measure. Nothing decorative — the template's whole argument is that the
   least chrome is the most focus, and that has to hold at the end too. */
#so-booking-widget[data-template="centred"] .so-bw-confirmation-details {
    width: 100%;
    max-width: var(--so-bw-measure);
    margin-inline: auto;
    padding: 0;
    border: 0;
    background: none;
}
#so-booking-widget[data-template="centred"] .so-bw-confirmation-detail {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, auto);
    gap: 14px;
    padding: var(--so-bw-row-pad-y) 0;
    border-bottom: 1px solid var(--so-bw-ds-n100);
}
#so-booking-widget[data-template="centred"] .so-bw-confirmation-detail:last-child {
    border-bottom: 0;
}
#so-booking-widget[data-template="centred"] .so-bw-confirmation-detail-value {
    text-align: right;
}

/* was @media (max-width: 640px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    /* The phone release of the measure used to live here — "the measure stops
       being a measure once the viewport is narrower than it. Released rather
       than shrunk." That is now the base value at every width, so this rule set
       100% to 100% and did nothing. Removed rather than left in place: a second
       declaration of a value that already holds is how the two ends of a rule
       drift apart, which is what put a 4px slip in the scroll gutter and two
       different band depths on Card Deck. */

/* was @media (max-width: 480px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */




/* ==========================================================================
   17.4 · TABBED RAIL  (tabbed-forest, layout key "tabbed")
   --------------------------------------------------------------------------
   FREE NAVIGATION, MADE CHEAP TO USE.

   The tabs let a customer jump back. That was already true and it was already
   half a feature: jumping back is only useful if COMING BACK is cheap, and
   coming back meant re-finding the time you had picked and re-picking it.

   Two mechanisms fix that, and both are why this template exists:

     • THE RAIL IS NAVIGATION. Each row jumps to the step that set it —
       bound in the markup for this layout only, with role, tabindex and a
       keyboard path, so it is a control rather than a click target.
     • THE CHOSEN TIME DOCKS. Once a day is picked, its times sit in a strip
       pinned directly under the tab bar. Leave to change the service, come
       back, and the time you chose is where you left it.

   The docked strip is a SECOND sticky in the same scroll container as the
   search bar, which is how stickies come to overlap. It reserves its own
   height rather than overlaying, so it cannot.
   ========================================================================== */

/* Tab items were dimmed with `opacity: .6`. On white that puts a #42485a
   label at roughly 3.4:1 — under the 4.5:1 body-text floor — and it dims the
   accent segment of the active tab along with everything else. Colour does
   the dimming instead, from the ramp, where the value is known.

   The labels are named EXPLICITLY, not left to inherit: the base step bar
   colours its own label (accent on the active item, secondary on a done one)
   at (0,2,0), which outranks nothing here but is applied to the label rather
   than the item, so setting `color` on the item alone leaves both untouched.
   Stated here, both read against the pane and neither uses the button fill
   as text. */
#so-booking-widget[data-template="tabbed"] .so-bw-stepbar-item {
    opacity: 1;
    color: var(--so-bw-ds-n500);
}
#so-booking-widget[data-template="tabbed"] .so-bw-stepbar-item--done,
#so-booking-widget[data-template="tabbed"] .so-bw-stepbar-item--active {
    color: var(--so-bw-ds-n900);
}
#so-booking-widget[data-template="tabbed"] .so-bw-stepbar-label,
#so-booking-widget[data-template="tabbed"] .so-bw-stepbar-item--done .so-bw-stepbar-label {
    color: var(--so-bw-ds-n700);
}
#so-booking-widget[data-template="tabbed"] .so-bw-stepbar-item--active .so-bw-stepbar-label {
    color: var(--so-bw-ds-n900);
}
/* Hover on a tab you can actually go back to. Inset, so the 3px segment
   underneath stays the thing that says which tab is current. */
#so-booking-widget[data-template="tabbed"] .so-bw-stepbar-item--done:hover {
    background: color-mix(in srgb, var(--so-bw-primary) 7%, transparent);
}
/* The ring is INSET. A tab sits flush against the top of the pane and flush
   against its neighbours, so an outward ring is clipped on three sides. */
#so-booking-widget[data-template="tabbed"] .so-bw-stepbar-item[tabindex]:focus-visible {
    outline: 2px solid var(--so-bw-primary);
    outline-offset: -2px;
    border-radius: var(--so-bw-ds-radius-chip);
}
/* The rail rows, where they are controls. Only ever on this layout: the
   markup binds role and tabindex to the same getter, so nothing else grows a
   tab stop that does nothing. */
#so-booking-widget .so-bw-rail-row--link {
    cursor: pointer;
    border-radius: var(--so-bw-ds-radius-chip);
    transition: background var(--so-bw-ds-motion);
}
#so-booking-widget .so-bw-rail-row--link:hover {
    background: color-mix(in srgb, var(--so-bw-primary) 8%, transparent);
}
#so-booking-widget .so-bw-rail-row--link:focus-visible {
    outline: 2px solid var(--so-bw-primary);
    outline-offset: 2px;
}
/* An affordance, so the row reads as operable before it is hovered. Painted
   in the accent rather than in the key's own grey: this is the one mark in the
   rail that says "go here", and it is the accent's job in this column. */
#so-booking-widget[data-template="tabbed"] .so-bw-rail-row--link .so-bw-rail-k::after {
    content: '';
    display: inline-block;
    width: 5px;
    height: 5px;
    margin-left: 6px;
    border-top: 1.5px solid var(--so-bw-primary-ink, var(--so-bw-primary));
    border-right: 1.5px solid var(--so-bw-primary-ink, var(--so-bw-primary));
    transform: rotate(45deg) translateY(-1px);
}

/* ── C8 · PANE GRID ──────────────────────────────────────────────────────
   Compact cards in the ~610px pane. This template's cards are the smallest in
   the catalogue on purpose: the pane is two thirds of a 1040px shell because
   the rail has the other third, the tab strip takes 48px off the top, and a
   customer who is here to REVISE a choice is scanning a list, not reading it.

   MEASURED before this pass: a service tile came out 298 x 261px, so two rows
   of them did not fit the 560px scroll port — on the one architecture whose
   promise is that changing your mind is cheap, changing it meant scrolling.

   Two selectors, `.so-bw-step` and bare, for specificity. The 960-tier rule in
   section 4 sets this grid through `.so-bw-step:nth-child(n)`, which scores
   (1,3,0) and outranks a plain `[data-template] .so-bw-card-grid` — the same
   trap Roster and Counter document at their own grids. Matching its shape is
   not enough; the selector has to reach it.

   The gap is NOT set here. Section 15.7 declares a 14px floor with
   `!important` and keeps a 10px dense family beside it; this template is on
   that list, which is the only place a gap for it can win. */
/* ── One card height for the whole catalogue ─────────────────────────────
   `grid-auto-rows: 1fr` — the shared value — sizes every row in the grid to
   the tallest CARD IN THE GRID rather than to the tallest in its own row, so
   every card in the step is exactly as tall as every other. That is what is
   wanted here: this template shows the catalogue five across, and a row of
   tiles that each end at a different height reads as a broken grid rather
   than as a list.

   This briefly ran as `auto`, to stop 79px of dead space per card. That was
   treating the symptom. `1fr` is only expensive when ONE card is genuinely
   taller than the rest, and on this template two rules had already made the
   card's content a fixed size — the title reserves two lines and
   `.so-bw-card-meta--clamp` reserves exactly two lines of description on
   every card whether it uses them or not (see the `anyServiceHasDesc` gate in
   booking-widget.js). The 79px came from somewhere else entirely: the ONE
   service in the catalogue with an uploaded photo, whose 24:9 media band made
   its card 76px taller than the eleven without one. That band is now hidden
   on this template (section 22, beside the placeholder rule), so the tallest
   card IS the ordinary card and levelling to it costs nothing.

   MEASURED on the live install at 1440: rows at 175 / 175 / 251 before, all
   cards at 175 after — uniform AND the short height, which `auto` could give
   only one of.

   The `:has()` guard stays. An open add-ons panel is a real height difference
   with a real cause, and levelling the whole catalogue to an expanded card
   would put 200px of nothing under eleven collapsed ones.

   Selector shape matches the base rule's so it does not rely on source order. */
#so-booking-widget[data-template="tabbed"] .so-bw-card-grid:has(.so-bw-card--has-extras-open) {
    grid-auto-rows: auto;
}
/* ── The track: at most five, at least 180px, otherwise as many as fit ────
   One declaration, no breakpoint, and it replaced two rules that between them
   made the Customize → Widget Size control behave inconsistently.

   WHAT WAS HERE. A shared fluid track (`minmax(min(100%, 180px), 1fr)`) plus a
   hard `repeat(5, minmax(0, 1fr))` at `[data-cwmin~="1280"]`. The five was
   right about the intent and wrong about the mechanism: 1280 is a ROOT width,
   and only Full Width ever reaches it — every other size is capped at 1100px by
   `.alignwide` — so Small / Medium / Large sat on the fluid track and Full alone
   snapped to five. Adjacent settings changed the layout's KIND rather than its
   degree, which is two designs, not a responsive one. (It also needed a 1280
   entry in SO_BW_MIN_BREAKPOINTS, kept because it is a legitimate tier for
   anything else that wants it.)

   Before that, the plain shared track put EIGHT cards in a row on a 1598px Full
   Width pane — a twelve-service catalogue reading as a row of eight and a row of
   four. Five is the row this template is laid out around: it is what the pager's
   page size is a multiple of (`svcPerPage` in booking-widget.js), so a full page
   is two clean rows.

   HOW THE ONE LINE DOES BOTH:

     track min = max(--so-bw-card-min, (100% - 4 gaps) / 5)

   Read it at the ends. On a wide pane a fifth of the row exceeds the 180px
   floor, so the floor stops binding and `auto-fill` fits exactly five tracks and
   no more — a sixth would need the minimum to be a sixth of the row, and it is
   not. On a narrow pane a fifth is under 180px, the floor takes over, and
   auto-fill steps down through four, three and two on its own, exactly as it did
   before any of this. Five is a CEILING, not a pin.

   MEASURED across the control: Small / Medium / Large (1100px root, ~710px grid)
   three across at 230px; Full at 1920 five across at ~311px. Same rule, every
   size, differing by degree.

   `min(100%, …)` still wraps the whole thing. It is the guard the shared track
   carried: when the pane is narrower than one card the track must fall back to
   the pane, or a single 180px minimum overflows a 160px column.

   The 40px is four gaps of this template's 10px grid gap — five tracks have four
   gaps between them. A literal rather than a var because §15.7 owns that gap
   with `!important` for the dense family, so a var here would read the wrong
   value. Both selectors are for specificity, matching the base rule. */
#so-booking-widget[data-template="tabbed"] .so-bw-step .so-bw-card-grid,
#so-booking-widget[data-template="tabbed"] .so-bw-card-grid {
    grid-template-columns: repeat(auto-fill, minmax(min(100%, max(var(--so-bw-card-min), calc((100% - 40px) / 5))), 1fr));
}
/* The card's own metrics come from the tokens rather than from per-element
   overrides, so the padding, the icon gap and the two type sizes stay in
   proportion to each other instead of drifting one rule at a time. */
#so-booking-widget[data-template="tabbed"] .so-bw-container {
    /* Horizontal padding is load-bearing for alignment — 12px is what puts the
       title and the pills on the card's own left edge (see the lead tile's
       align-self below). Change it and the card's internals stop registering. */
    --so-bw-card-pad: 12px;
    --so-bw-card-gap: 8px;
    --so-bw-card-title-size: 15px;
    --so-bw-card-meta-size: 12.5px;
}
/* ── Height: every dimension, not one big cut ────────────────────────────
   MEASURED at a 1040px shell: 302 x 329, of which 135 was cover and 192 the
   text block. Two rows of that overflow a 560px scroll port, which on THIS
   template is the thing that matters — its whole promise is that changing your
   mind is cheap, and it is not cheap if going back means scrolling to find the
   card you want.

   Taken from six places rather than one, because a single big cut would have
   had to come out of the cover (the card's only picture) or the title (its only
   name). Vertical padding, the gap under the mark, the mark itself, the pill
   height and the two leadings each give up a little and none of them is the
   thing the customer came for. Horizontal padding is untouched throughout —
   it is what the alignment work pinned. */
#so-booking-widget[data-template="tabbed"] .so-bw-card-top {
    /* Longhand: the shared service-card rule derives these from the pad token
       (`calc(pad - 4px)` / `- 2px`) and the horizontal values must survive. */
    padding-top: 6px;
    padding-bottom: 8px;
}

/* ── C8b · THE CARD IS A STACK ───────────────────────────────────────────
   Cover, then mark, then name, then the facts as pills along the bottom.

   The base card is a ROW — tile beside a text column — and the service card
   is a column only so its add-ons strip has somewhere to go. Here the column
   is the design: each card is a tile you read top to bottom, so the eye
   travels down a card and across a row rather than zig-zagging through four
   tile-then-text rows.

   Three things make it hold together as a grid rather than as four
   independent boxes:

     • THE PILLS SIT ON THE CARD'S FLOOR. Grid stretches items, so the two
       cards in a row are already the same height; `margin-top: auto` on the
       badge row spends that slack above the pills instead of below them, and
       the duration/price row lines up across the row whatever the title did.
       That needs the flex chain unbroken all the way down — card, top, body —
       which is why all three are declared and not just the last one.
     • A CARD WITH NO PHOTOGRAPH HAS NO BAND. The placeholder band exists to
       give a photoless service the same height as a photographed one; with
       the mark moved out of the band and under it, an empty 135px tint is
       just 135px of nothing. The content starts identically either way — same
       tile, same left edge, same order — so the two read as one design at two
       heights, which the floor above then resolves.
     • THE MARK IS NOT IN THE BAND. See the lead tile in booking-widget.js. */
#so-booking-widget[data-template="tabbed"] .so-bw-card {
    flex-direction: column;
    align-items: stretch;
    text-align: left;
}
#so-booking-widget[data-template="tabbed"] .so-bw-card:not(.so-bw-card--service) {
    gap: var(--so-bw-card-gap);
}
#so-booking-widget[data-template="tabbed"] .so-bw-card-top {
    flex: 1 1 auto;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--so-bw-card-gap);
}
#so-booking-widget[data-template="tabbed"] .so-bw-card-body {
    flex: 1 1 auto;
    display: flex;
    flex-direction: column;
    width: 100%;
}
/* The band's crop and the empty-band case are BOTH settled in section 22,
   which owns the cover on this template and sits later in the file than this
   section — a declaration here would lose. */
/* The lead tile: a rounded square, not the base 50%-ish avatar. It carries the
   admin's icon or the service initials in the service's own colour, so it is
   the one place on the card where a per-service hue is allowed. */
#so-booking-widget[data-template="tabbed"] .so-bw-card-icon,
#so-booking-widget[data-template="tabbed"] .so-bw-card-lead {
    --so-bw-icon-size: 32px;
    --so-bw-icon-svg-size: 16px;
    --so-bw-icon-text-size: 13px;
    border-radius: var(--so-bw-r-9);
}
/* …and it aligns with the title underneath it, which needs saying because the
   service card says otherwise. That card sets `align-self: center` on
   `.so-bw-card-icon` (search: so-bw-card--service .so-bw-card-icon) and is
   right to: in the ROW it was written for, the cross axis is vertical, so
   centring keeps a fixed square optically on the title of a stretched cell.
   This template turns that box into a COLUMN, where the cross axis is
   horizontal and the very same declaration centres the tile across the card.
   MEASURED: tile at x=156 in a card at x=25 w=300 — dead centre — with its own
   title at x=38.

   `align-self` has no meaning independent of the direction of the box it sits
   in, so a layout that turns the box has to restate it. The second selector
   covers the location and staff cards, whose tile is a direct child; they
   already measure correctly, and this is what keeps that true on purpose
   rather than by luck. */
#so-booking-widget[data-template="tabbed"] .so-bw-card-lead,
#so-booking-widget[data-template="tabbed"] .so-bw-card > .so-bw-card-icon {
    align-self: flex-start;
}
/* The selection tick goes to the corner. It is a SIBLING of the body inside
   .so-bw-card-top, so turning that row into a column stacked it underneath —
   MEASURED as 49px of tick and gap between the pills and the card's bottom
   edge, which is exactly the space `margin-top: auto` was supposed to be
   collecting. Out of flow, it stops competing for a row and lands where a
   selection mark belongs on a picture card: over the top-right of the cover.

   Its own opaque fill is not decoration — unselected it is a hairline ring,
   and a hairline ring on a photograph is invisible. */
#so-booking-widget[data-template="tabbed"] .so-bw-card-check {
    position: absolute;
    top: 10px;
    right: 10px;
    z-index: 2;
    background: var(--so-bw-card-bg);
    box-shadow: 0 1px 3px rgba(27, 31, 42, 0.18);
}
#so-booking-widget[data-template="tabbed"] .so-bw-card-title {
    font-weight: 700;
    line-height: 1.2;
    letter-spacing: -0.2px;
    margin-bottom: 2px;
}
#so-booking-widget[data-template="tabbed"] .so-bw-card-meta {
    line-height: 1.35;
    color: var(--so-bw-ds-n500);
}

/* The two fact pills are no longer stated here. They were built for this
   template and they are now the SHARED card language — see "THE FACT PILLS"
   in section 15.7, beside the badge primitives they restyle. The only thing
   this template still needs is the row's own spacing, below. */
#so-booking-widget[data-template="tabbed"] .so-bw-card-badges {
    padding-top: 8px;
}
/* The cover band is the other half of the height — roughly 135px of the 261 —
   and it is not settled here. Section 22 owns the band on this template (it is
   one of the four that had hidden it and had the hiding undone), so the crop is
   tightened there, next to the decision it qualifies. The dead `display: none`
   that used to sit at this spot has gone: section 22 is later in the file and
   had been winning for as long as both rules existed, so it was stating a
   policy the stylesheet no longer had. */

/* ── Tabbed Rail · DATE & TIME · the docked strip ────────────────────────
   The slot panel becomes a strip pinned under the tab bar, its times running
   in a horizontal snap row. `top: 0` is against .so-bw-body, which is the
   scroll container — the tab bar is in the header, outside it, so the strip
   lands directly beneath the tabs without needing to know their height. */
#so-booking-widget[data-template="tabbed"] .so-bw-calendar {
    max-width: 420px;
    margin-inline: 0;
}
#so-booking-widget[data-template="tabbed"] .so-bw-slot-panel {
    position: sticky;
    top: 0;
    z-index: 3;
    margin: 0 0 14px;
    padding: 10px 0 12px;
    border: 0;
    border-bottom: 1px solid var(--so-bw-border);
    border-radius: 0;
    /* Opaque. A translucent sticky strip with slot pills scrolling through it
       is unreadable at exactly the moment it matters. */
    background: var(--so-bw-bg);
    /* The base panel carries `min-height: 200px`, sized for a panel that is
       the whole right-hand column of the date-time step. Here it is one row
       of chips under a one-line head, and 200px of reserved height put the
       calendar most of a screen down the pane on the step where the calendar
       IS the screen. 88px is the measured content: a 20px head, 8px of gap,
       a 44px chip row, and the 12px bottom pad.

       It is a min-height rather than a fixed height, and it is RESERVED
       rather than overlaid, because this is a second sticky in the same
       scroll container as the search head — the strip has to hold its own
       space or the two overlap the moment the pane scrolls. */
    min-height: 88px;
    justify-content: center;
}
#so-booking-widget[data-template="tabbed"] .so-bw-slot-panel-head {
    padding-bottom: 8px;
}
/* "Kept · 09:30 AM". The payoff of the whole template, in one badge: the
   customer left this step to change something, came back, and is told the
   time is still theirs rather than being handed forty chips to search.
   Bound on slotsRevisited (see goToStep / selectTime), so it appears only on
   a RETURN and only while the kept time is still the selected one — which is
   also what makes the returned-to state visibly different from the first
   visit rather than identical to it. */
#so-booking-widget[data-template="tabbed"] .so-bw-slot-kept {
    display: inline-flex;
    align-items: center;
    align-self: flex-start;
    height: 20px;
    margin-top: 4px;
    padding: 0 8px;
    border-radius: 999px;
    background: color-mix(in srgb, var(--so-bw-primary) 22%, var(--so-bw-card-bg, #fff));
    color: var(--so-bw-ds-n900);
    font-family: var(--so-bw-mono);
    font-size: 10px;
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    white-space: nowrap;
}
/* And the chip it refers to wears a ring, so "kept" names something the eye
   can find in the strip without reading the badge twice. */
#so-booking-widget[data-template="tabbed"] .so-bw-slot-panel--kept .so-bw-slot-row--selected {
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--so-bw-primary) 22%, transparent);
}
#so-booking-widget[data-template="tabbed"] .so-bw-slot-group {
    display: flex;
    align-items: center;
    gap: 12px;
    min-width: 0;
}
#so-booking-widget[data-template="tabbed"] .so-bw-slot-group-title {
    flex: 0 0 auto;
    margin: 0;
}
#so-booking-widget[data-template="tabbed"] .so-bw-slot-list {
    display: flex;
    flex: 1 1 auto;
    gap: 8px;
    min-width: 0;
    padding-bottom: 4px;
    overflow-x: auto;
    scroll-snap-type: x proximity;
    overscroll-behavior-x: contain;
}
#so-booking-widget[data-template="tabbed"] .so-bw-slot-row {
    flex: 0 0 auto;
    justify-content: center;
    min-width: 96px;
    scroll-snap-align: start;
}

/* ── F2 · TWO COLUMN, and R2 · DETAILS ONLY ──────────────────────────────
   Both shared; the F2 assignment is in 17.11 with the other two-column
   templates, and R2's rules are stated once in 17.2 above (Split Canvas),
   including this template's selectors. Nothing is repeated here. */

/* ── N8 · TAB COLLAPSE ───────────────────────────────────────────────────
   The strip of tabs has nothing left to offer once the booking exists, so it
   goes, and the rail — which has been the running order all along — becomes
   the record of what was booked. */
#so-booking-widget[data-template="tabbed"] .so-bw-container--done .so-bw-rail-title {
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}
#so-booking-widget[data-template="tabbed"] .so-bw-confirmation-details {
    max-width: 440px;
    margin-inline: auto;
    padding: 0;
    border: 0;
    border-top: 2px solid var(--so-bw-primary);
    /* Same correction as Agenda's N10 block, and for the same reason: this
       keeps ONLY the top edge of the base card, so the inherited
       `border-radius: var(--so-bw-radius-lg)` has nothing left to round and
       bends the accent rule into two hooks instead. The other three templates
       that strip this card (Centred, Split, Dusk) drop the border outright, so
       their radius is genuinely inert and is deliberately left alone — it is
       the surviving top edge that makes the radius visible. */
    border-radius: 0;
    background: none;
}
#so-booking-widget[data-template="tabbed"] .so-bw-confirmation-detail {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, auto);
    gap: 12px;
    /* Flush with the accent rule and the row hairlines, which both span the
       full width — see the note on Agenda's row padding. */
    padding: 11px 0;
    border-bottom: 1px solid var(--so-bw-ds-n100);
}
#so-booking-widget[data-template="tabbed"] .so-bw-confirmation-detail:last-child {
    border-bottom: 0;
}
#so-booking-widget[data-template="tabbed"] .so-bw-confirmation-detail-value {
    text-align: right;
}

/* was @media (max-width:900px) — now gated on the CONTAINER width
   via data-cw (see booking-widget.js _wireContainerWidth). :where()
   keeps specificity identical to the media query it replaces. */

    /* The rail is below the pane here (section 14), so it is no longer a
       standing context panel and its rows stop being navigation the customer
       can see. They keep working — goToStep is still bound — but the strip
       un-docks, because a sticky strip in a single-column phone flow eats the
       top of every screen. */
    #so-booking-widget:where([data-cw~="900"])[data-template="tabbed"] .so-bw-slot-panel{
        position: static;
    }
    /* MEASURED, and the reason the un-dock has to be more than `position:
       static`: the times were still laid out as the DOCK's horizontal snap
       row — `flex`, `min-width: 96px` per chip, `overflow-x: auto` — in a
       panel that is no longer docked. At a 400px container that put a 366px
       sideways scroll inside a full-width panel, so a phone user swiped
       horizontally through a list that had the whole column to grow down
       into. The strip's row is a consequence of being a strip; once it stops
       being one, the times go back to the shared auto-fill grid. */
    #so-booking-widget:where([data-cw~="900"])[data-template="tabbed"] .so-bw-slot-group{
        display: block;
    }
    #so-booking-widget:where([data-cw~="900"])[data-template="tabbed"] .so-bw-slot-list{
        display: grid;
        grid-template-columns: repeat(auto-fill, minmax(clamp(74px, calc(var(--so-bw-cw, 100vw) * 0.09), 96px), 1fr));
        overflow-x: visible;
        scroll-snap-type: none;
    }
    #so-booking-widget:where([data-cw~="900"])[data-template="tabbed"] .so-bw-slot-row{
        flex: 0 1 auto;
        min-width: 0;
    }
    #so-booking-widget:where([data-cw~="900"])[data-template="tabbed"] .so-bw-slot-group-title{
        margin-bottom: 6px;
    }
    #so-booking-widget:where([data-cw~="900"])[data-template="tabbed"] .so-bw-calendar{
        max-width: 460px;
        margin-inline: auto;
    }

    /* ── ONE BACK-AFFORDANCE, NOT TWO ────────────────────────────────────
       Above the collapse this template offers two ways back and they explain
       each other: the tabs are the map, the rail beside them is the record,
       and a chevron on a rail row reads as "and this one is also a way back".
       Below it the rail is UNDER the pane. It is off-screen while the
       customer works, it is no longer adjacent to the tabs, and a chevron on
       it now promises navigation from something that reads as a footer.

       So the tabs take the job alone, and say so: the strip gets a solid rule
       beneath it — not a 3px accent SEGMENT under one tab, but a continuous
       2px line under all of them — which is the phone idiom for "this row is
       the control". The rail keeps its rows operable (nothing is taken away
       from a customer who finds them) but drops the chevron, because an
       affordance that points at a screen the customer cannot see is worse
       than no affordance.

       The alternative — re-rendering the four rail keys as a scrollable chip
       row above the pane — reads better but costs a node in markup shared by
       nineteen architectures, and it would be the rail rendered twice. If it
       is ever wanted, it has to be the same list, not a copy of it. */
    #so-booking-widget:where([data-cw~="900"])[data-template="tabbed"] .so-bw-stepbar{
        border-bottom: 2px solid var(--so-bw-ds-n200);
    }
    #so-booking-widget:where([data-cw~="900"])[data-template="tabbed"] .so-bw-rail-row--link .so-bw-rail-k::after{
        content: none;
    }

/* was @media (max-width: 640px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    #so-booking-widget:where([data-cw~="640"])[data-template="tabbed"] .so-bw-card-grid{
        grid-template-columns: 1fr;
    }
    /* The slot group / title rules that used to sit here moved up to the
       cw 900 block, which is where the strip stops being a strip. `data-cw`
       lists every breakpoint at or below the container width, so a rule
       gated on 900 already applies at 640 — keeping a second copy here only
       created two places to change the same decision. */

/* was @media (max-width: 560px) — now gated on the CONTAINER width
   via data-cw (see booking-widget.js _wireContainerWidth). The ID stays
   OUTSIDE :where() so the selector still scores (1,0,0). */

    /* ── THE TABS WRAP RATHER THAN SCROLL ────────────────────────────────
       Section 13.7 already wraps this strip into rows of four below 560px,
       for the reason stated there: the row is wider than the card, the phone
       block hides the scrollbar outright (correctly — a touch surface should
       not paint one), so the steps past the fold read as cut off rather than
       scrollable. That much is shared with nine other layouts and is not
       repeated here.

       What is specific to a TAB is the cell. Three things change:

         • The future tabs get their labels BACK. They lose them at 1040px so
           the active tab has room to state itself; in a wrapped grid every
           cell is 25% whatever it holds, so a bare number buys nothing and
           costs the customer a step name.
         • 40px, not 48px — two rows of 48 is a third of a phone screen spent
           on navigation.
         • Only the active tab paints a segment. Eight 3px segments in a
           two-row grid draw a line across the middle of the strip; the strip
           gets one solid rule underneath instead (see the cw 900 block).

       The future-tab selector is listed alongside the plain one because the
       desktop rule that collapses it scores a class higher through its
       :not() chain — setting the base alone would leave four cells at 34px. */
    #so-booking-widget:where([data-cw~="560"])[data-template="tabbed"] .so-bw-stepbar-item,
    #so-booking-widget:where([data-cw~="560"])[data-template="tabbed"] .so-bw-stepbar-item--active,
    #so-booking-widget:where([data-cw~="560"])[data-template="tabbed"] .so-bw-stepbar-item:not(.so-bw-stepbar-item--done):not(.so-bw-stepbar-item--active){
        /* MEASURED: a flat `flex-basis: 25%` fits THREE per row, not four.
           Section 13.7 gives the strip `gap: 10px 2px`, and four 25% cells
           plus three 2px gaps is 6px wider than the line — so eight tabs
           wrapped to three rows and spent 140px of a 400px card on
           navigation. Taking the gap out of the basis fits four exactly:
           4 x (25% - 1.5px) + 3 x 2px = 100%. Two rows, 98px. */
        flex: 0 1 calc(25% - 1.5px);
        max-width: calc(25% - 1.5px);
        /* 44px, not the 40px a two-row strip would like to be: this is the
           primary back-navigation on a touch screen, so it takes the tap
           floor over the 8px of height. */
        height: 44px;
        gap: 5px;
        padding: 0 var(--so-bw-tab-inset);
        justify-content: flex-start;
    }
    /* The tabs tighten to 6px here, so the strip's pull-out tightens with
       them — one token, so the first tab of EVERY wrapped row still starts on
       the gutter rather than 6px inside it. */
    #so-booking-widget:where([data-cw~="560"])[data-template="tabbed"] .so-bw-stepbar{
        --so-bw-tab-inset: 6px;
    }
    #so-booking-widget:where([data-cw~="560"])[data-template="tabbed"] .so-bw-stepbar-item:not(.so-bw-stepbar-item--done):not(.so-bw-stepbar-item--active) .so-bw-stepbar-label{
        display: block;
    }
    #so-booking-widget:where([data-cw~="560"])[data-template="tabbed"] .so-bw-stepbar-label,
    #so-booking-widget:where([data-cw~="560"])[data-template="tabbed"] .so-bw-stepbar-item--active .so-bw-stepbar-label{
        font-size: 11px;
    }
    /* A square badge, not a pill: at 16px a circle around two digits is a
       ring with nothing inside it. */
    #so-booking-widget:where([data-cw~="560"])[data-template="tabbed"] .so-bw-stepbar-dot{
        width: 16px;
        height: 16px;
        border-radius: var(--so-bw-r-4);
        font-size: 10px;
    }
    /* The 3px segments are the strip's rule at desktop width. In a two-row
       grid they draw a line across the middle of the strip, so the segment
       thins to 2px and only the active tab paints one. */
    #so-booking-widget:where([data-cw~="560"])[data-template="tabbed"] .so-bw-stepbar-item{
        border-bottom-width: 2px;
        border-bottom-color: transparent;
    }
    #so-booking-widget:where([data-cw~="560"])[data-template="tabbed"] .so-bw-stepbar-item--active{
        border-bottom-color: var(--so-bw-primary);
    }



/* ==========================================================================
   17.7 · CARD DECK  (pocket-violet, layout key "deck")
   --------------------------------------------------------------------------
   THE CARD IS THE UNIT.

   Progress is felt rather than read: a stack, the next card peeking beneath,
   no numbered stepper. Section 14 built the stack. What it did not do is make
   the CONTENT behave like a card — and a card has a rule the rest of the
   catalogue does not have:

       nothing may grow the card without bound.

   A forty-slot day, a thirty-service catalogue, an eight-field form — each of
   those turns "a card with the next one peeking" into "a very long page with
   two decorative rectangles at the top". So every step here is either
   naturally short or scrolls INSIDE its own region.

   TWO THINGS NOT TO TOUCH:
     • The container is deliberately `max-height: none` and `overflow:
       visible` so the peek is not clipped. Do not add a max-height at any
       breakpoint — section 15 has the same warning against Coral Pop.
     • Coral Pop is a different layout key (`soft`) and drops the stack
       entirely. Nothing here may leak to it, or the two read as one design
       again, which is the fault the register was rebuilt to fix.
   ========================================================================== */

/* ── C12 · DECK TILES ────────────────────────────────────────────────────
   Two-up tiles, big targets, no descriptions. A card in a stack states its
   options; it does not explain them. The full text is one tap away on every
   template via Read more, so nothing is lost — and hiding the clamp here is
   the second of only two exemptions from the description contract (the other
   is Conversation's chip). It needs !important for the same reason that one
   did: the clamp is declared !important to beat the never-truncate block. */
/* TWO-UP IS A FLOOR, NOT A FIXED COUNT. `repeat(2, ...)` pinned the column
   count and let the TILES stretch instead, so every extra pixel of embed went
   into making each card wider — 261px at the medium size, 336 at large, 411 at
   full. A photo band is a ratio of that width, so the header grew with it and
   the tile with the header; pinning the band's depth to stop that then turned
   the band into a 3.67:1 letterbox on the widest tile, and `object-fit: cover`
   centre-cropped a real service photo down to 18-25% of its height — subjects
   sliced off at the top edge. Short cards and a sane crop looked like opposites
   only because the tile width was unbounded.

   auto-fill spends the extra width on MORE COLUMNS instead: the tile stays in
   the 200-280px band at every embed size, so the header keeps its 21:9
   proportion honestly rather than by being clamped, the card stays short, and
   a wider embed shows more of the catalogue — which is what a deck of pocket
   tiles should do with the room. min(100%, …) keeps it to a single column when
   the container is narrower than one track.

   190px, and the number is decided by the SMALL embed rather than the large
   one. auto-fill only lays whole tracks, so the track width sets the point at
   which a column is dropped: `small` gives the grid about 412px, and a 240px
   track needs 490 for two of them — it fell to ONE 416px tile, which is worse
   than the 201px pair `repeat(2, …)` was giving. 190 needs 390 and holds the
   pair. It then adds a third column around the large embed and a fourth at
   full, which is where the width was going to waste before. */
#so-booking-widget[data-template="deck"] .so-bw-card-grid {
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 190px), 1fr));
    gap: 10px;
}
#so-booking-widget[data-template="deck"] .so-bw-card-meta--clamp,
#so-booking-widget[data-template="deck"] .so-bw-card-more {
    display: none !important;
}
#so-booking-widget[data-template="deck"] .so-bw-card-media {
    display: none;
}
#so-booking-widget[data-template="deck"] .so-bw-card:not(.so-bw-sp__panel) {
    border-radius: var(--so-bw-r-14);
}
#so-booking-widget[data-template="deck"] .so-bw-card--service .so-bw-card-top {
    min-height: 84px;
    padding: 14px;
}
#so-booking-widget[data-template="deck"] .so-bw-card-title {
    font-size: 14.5px;
    line-height: 1.35;
}

/* ── Card Deck · TIMES · the snap strip ──────────────────────────────────
   One horizontally scrolling row per period. This is the mechanism that keeps
   the card's height constant however many openings a day has — a forty-slot
   Saturday and a four-slot Tuesday occupy exactly the same space, so the
   stack never lurches.

   THE WRAPPER NEEDS `width: max-content`. .so-bw-slot-list is the scroller;
   the strip inside it grows past the container, and if the intermediate
   element keeps its default width the OVERFLOW happens there — on an element
   that is not a scroll container — instead of at the scroller. Conversation's
   day rail hit exactly this; the fix is the same and it is not optional. */
#so-booking-widget[data-template="deck"] .so-bw-slot-panel {
    border: 0;
    border-radius: 0;
    background: none;
    padding: 0;
}
#so-booking-widget[data-template="deck"] .so-bw-slot-group {
    min-width: 0;
}
#so-booking-widget[data-template="deck"] .so-bw-slot-group-title {
    margin-bottom: 8px;
}
#so-booking-widget[data-template="deck"] .so-bw-slot-list {
    display: flex;
    grid-template-columns: none;
    gap: 8px;
    min-width: 0;
    padding-bottom: 6px;
    overflow-x: auto;
    scroll-snap-type: x proximity;
    overscroll-behavior-x: contain;
    scrollbar-width: thin;
}
#so-booking-widget[data-template="deck"] .so-bw-slot-row {
    flex: 0 0 auto;
    justify-content: center;
    min-width: 104px;
    min-height: 50px;
    border-radius: var(--so-bw-r-12);
    scroll-snap-align: start;
    font-size: 13.5px;
    font-weight: 600;
}

/* ── F12 · DECK FIELDS ───────────────────────────────────────────────────
   One column, large controls, and the whole form in one card. The card
   scrolls internally rather than growing, which is what keeps the commit bar
   docked and the peek visible on a long form. */
#so-booking-widget[data-template="deck"] .so-bw-form {
    display: flex;
    flex-direction: column;
    gap: 14px;
}
#so-booking-widget[data-template="deck"] .so-bw-form-row {
    display: flex;
    flex-wrap: wrap;
    gap: 14px;
}
#so-booking-widget[data-template="deck"] .so-bw-form-row > .so-bw-form-group {
    flex: 1 1 200px;
    min-width: 0;
}
#so-booking-widget[data-template="deck"] .so-bw-input,
#so-booking-widget[data-template="deck"] .so-bw-select,
#so-booking-widget[data-template="deck"] .so-bw-textarea {
    min-height: 48px;
    border-radius: var(--so-bw-r-12);
}

/* ── R14 · TOP OF STACK ──────────────────────────────────────────────────
   The review is the last card on the pile, so it gets the pile's own
   geometry: a rounded panel with the total in the position the commit bar
   will occupy a moment later. */
#so-booking-widget[data-template="deck"] .so-bw-summary {
    border: 1px solid var(--so-bw-ds-n200);
    border-radius: var(--so-bw-r-14);
    padding: 4px 16px;
}
#so-booking-widget[data-template="deck"] .so-bw-summary-row {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, auto);
    gap: 14px;
    align-items: baseline;
    padding: var(--so-bw-row-pad-y) 0;
}
#so-booking-widget[data-template="deck"] .so-bw-summary-row--wide {
    grid-template-columns: 1fr;
    gap: 4px;
}
#so-booking-widget[data-template="deck"] .so-bw-summary-value {
    text-align: right;
}
#so-booking-widget[data-template="deck"] .so-bw-summary-row--wide .so-bw-summary-value {
    text-align: left;
}
#so-booking-widget[data-template="deck"] .so-bw-summary-row--total {
    margin: 6px -16px -4px;
    padding: 14px 16px;
    border-radius: 0 0 var(--so-bw-r-13) var(--so-bw-r-13);
    background: color-mix(in srgb, var(--so-bw-primary) 10%, transparent);
}
#so-booking-widget[data-template="deck"] .so-bw-summary-row--total .so-bw-summary-value {
    font-size: 18px;
    font-weight: 700;
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}

/* ── N11 · TOP CARD ──────────────────────────────────────────────────────
   The stack resolves: the two decorative cards square up behind the last one
   and the confirmation is simply what the top card now says.

   THE MOTION IS THE MEANING HERE, so it needs a fallback that carries the
   same meaning without moving. Under prefers-reduced-motion the offsets stay
   where they land — the stack still reads as settled, it just does not travel
   there. The global reduced-motion block already clamps the duration; this
   makes sure the END STATE is one that reads. */
#so-booking-widget[data-template="deck"] .so-bw-container--done::before,
#so-booking-widget[data-template="deck"] .so-bw-container--done::after {
    transition: all 240ms ease;
}
#so-booking-widget[data-template="deck"] .so-bw-container--done::before {
    left: 14px;
    right: 14px;
    top: -10px;
    opacity: 0.35;
}
#so-booking-widget[data-template="deck"] .so-bw-container--done::after {
    left: 7px;
    right: 7px;
    top: -5px;
    opacity: 0.6;
}
#so-booking-widget[data-template="deck"] .so-bw-confirmation-details {
    max-width: 400px;
    margin-inline: auto;
    border: 1px solid var(--so-bw-ds-n200);
    border-radius: var(--so-bw-r-14);
    padding: 4px 16px;
    background: var(--so-bw-ds-n0);
}
#so-booking-widget[data-template="deck"] .so-bw-confirmation-detail {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, auto);
    gap: 12px;
    padding: var(--so-bw-row-pad-y) 0;
    border-bottom: 1px solid var(--so-bw-ds-n100);
}
#so-booking-widget[data-template="deck"] .so-bw-confirmation-detail:last-child {
    border-bottom: 0;
}
#so-booking-widget[data-template="deck"] .so-bw-confirmation-detail-value {
    text-align: right;
}

/* was @media (max-width: 640px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    /* The forced single column that used to live here is gone. It was the right
       companion to `repeat(2, …)`: with the count pinned, two tracks in a 640px
       container were cramped, so dropping to one was the lesser evil. Against
       an auto-fill track it is the WORSE one — it took a 552px container that
       comfortably holds two 243px tiles and gave it a single 496px tile, which
       is precisely the wide-tile-deep-band case (band clamped to 132px, a 3.8:1
       slot, the photograph centre-cropped through its subject).

       auto-fill already declines to lay a second track when there is no room
       for one: at a 350px container the grid has ~282px, two 190px tracks need
       390px, and it returns a single full-width tile on its own. The phone
       behaviour this rule existed to guarantee is now a consequence of the
       track size rather than an override fighting it. */

/* was @media (max-width: 480px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    /* The scrim padding is what makes the peek visible; at 480 the widget
       goes edge-to-edge and the peek would be clipped by the viewport rather
       than by the widget. Kept just enough inset for the two cards behind. */
    #so-booking-widget:where([data-cw~="480"])[data-template="deck"]{
        padding: 22px 12px 28px;
    }
    #so-booking-widget:where([data-cw~="480"])[data-template="deck"] .so-bw-container::before{ left: 18px; right: 18px; }
    #so-booking-widget:where([data-cw~="480"])[data-template="deck"] .so-bw-container::after{ left: 9px;  right: 9px;  }



/* ==========================================================================
   17.14 · CORAL POP  (coral-pop, layout key "soft")
   --------------------------------------------------------------------------
   THE SOFT REGISTER, CARRIED PAST THE PAINT.

   Section 15 already learned the lesson this block is built on: Coral Pop was
   once nothing but radius tokens, and an 18px corner beside a 12px one is a
   difference you can measure and not one you can see. The blush surfaces and
   the round geometry fixed the first three steps. They did not reach the two
   screens where a friendly template feels least friendly — the REVIEW and the
   CONFIRMATION, which are exactly where a key/value table reads as paperwork.

   So R13 is the point of this block. The rest is consistency.

   TWO TOKENS TO RESPECT:
     • --so-bw-ds-tap is 52px here, not 44. Every control follows it.
     • --so-bw-gutter does NOT position this template. Soft routes its
       horizontal inset through .so-bw-body (see the gutter exception in
       section 14), so a primitive that assumes the gutter will be off by the
       difference.
   ========================================================================== */

/* ── C19 · PILLOW ROWS ───────────────────────────────────────────────────
   One card per row already; what it lacked was a left-hand anchor. A coral
   disc holds the service's initial so the list has a rhythm down its left
   edge instead of a column of ragged text starts. */
#so-booking-widget[data-template="soft"] .so-bw-card-grid {
    grid-template-columns: 1fr;
    gap: 10px;
}
#so-booking-widget[data-template="soft"] .so-bw-card-grid:not(:has(.so-bw-card--has-extras-open)) {
    grid-auto-rows: auto;
}
#so-booking-widget[data-template="soft"] .so-bw-card--service .so-bw-card-top {
    min-height: var(--so-bw-ds-tap);
    padding: 16px 18px;
    align-items: center;
}
#so-booking-widget[data-template="soft"] .so-bw-card-title {
    font-size: 15px;
    font-weight: 600;
}
#so-booking-widget[data-template="soft"] .so-bw-card-media {
    border-radius: var(--so-bw-r-22) var(--so-bw-r-22) 0 0;
}
/* The price as a filled coral pill — the one saturated mark per row. */
#so-booking-widget[data-template="soft"] .so-bw-badge--price,
#so-booking-widget[data-template="soft"] .so-bw-badge--free {
    padding: 4px 12px;
    border: 0;
    border-radius: 999px;
    background: color-mix(in srgb, var(--so-bw-primary) 14%, transparent);
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
    font-weight: 700;
}
#so-booking-widget[data-template="soft"] .so-bw-badge--duration {
    padding: 4px 12px;
    border: 0;
    border-radius: 999px;
    background: var(--so-bw-bg-tertiary);
}
#so-booking-widget[data-template="soft"] .so-bw-card-check {
    width: 30px;
    height: 30px;
}

/* ── Coral Pop · DATE & TIME ─────────────────────────────────────────────
   Round day chips and round pills.

   THE DAY CHIP IS THE ONE DOCUMENTED EXCEPTION TO THIS TEMPLATE'S 52px TAP
   TOKEN, and the arithmetic is why: seven columns at 52px plus six 4px gaps
   is 388px of month. A 360px phone cannot hold it at any gutter, so the month
   would either overflow the widget or force a horizontal scroller onto the
   one step that must not have one. 44px gives 332px, which fits — and 44px is
   the house floor, so nothing is being given up that the design system
   promised. Stated here rather than left to be rediscovered. */
#so-booking-widget[data-template="soft"] .so-bw-calendar-day {
    min-height: 44px;
    border-radius: 999px;
}
#so-booking-widget[data-template="soft"] .so-bw-slot-list {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 10px;
}
#so-booking-widget[data-template="soft"] .so-bw-slot-row {
    justify-content: center;
    min-height: var(--so-bw-ds-tap);
    border-radius: 999px;
    font-size: 13.5px;
    font-weight: 600;
}
#so-booking-widget[data-template="soft"] .so-bw-slot-group-title {
    margin-bottom: 10px;
    letter-spacing: 0.08em;
}

/* ── F10 · PILLOW FIELDS ─────────────────────────────────────────────────
   The pill inputs already exist as tokens. What they needed was room and a
   label that is not shouting in 11px uppercase at a family booking a haircut. */
#so-booking-widget[data-template="soft"] .so-bw-form {
    display: flex;
    flex-direction: column;
    gap: 16px;
}
#so-booking-widget[data-template="soft"] .so-bw-form-row {
    display: flex;
    flex-wrap: wrap;
    gap: 16px;
}
#so-booking-widget[data-template="soft"] .so-bw-form-row > .so-bw-form-group {
    flex: 1 1 200px;
    min-width: 0;
}
#so-booking-widget[data-template="soft"] .so-bw-label {
    font-size: 14px;
    font-weight: 600;
    text-transform: none;
    letter-spacing: 0;
}
#so-booking-widget[data-template="soft"] .so-bw-input,
#so-booking-widget[data-template="soft"] .so-bw-select {
    min-height: var(--so-bw-ds-tap);
    padding-inline: 20px;
}
#so-booking-widget[data-template="soft"] .so-bw-textarea {
    border-radius: var(--so-bw-r-22);
    padding: 16px 20px;
}

/* ── R13 · PLAIN LANGUAGE ────────────────────────────────────────────────
   Not a key/value table. The booking is stated the way a person would say it,
   and the price is the only figure on the screen.

   HOW THE SENTENCE IS BUILT, because it looks like magic and is not: the
   summary rows are already semantic pairs — label and value — so the label is
   set small and quiet above its value and the value is set large. Read down,
   that IS the sentence: "Date / Friday 12 September", "Time / 2:30 PM". No
   string concatenation, no new markup, no per-locale word order to get wrong,
   and a screen reader still hears a labelled value rather than a fragment.

   The alternative — composing real sentences in JS — was rejected: it would
   need per-locale grammar, it would break the moment a service name ends in a
   preposition, and it would make the one template with the friendliest voice
   the one most likely to say something ungrammatical. */
#so-booking-widget[data-template="soft"] .so-bw-summary {
    border: 0;
    border-radius: var(--so-bw-r-22);
    background: var(--so-bw-ds-n0);
    padding: 4px 20px;
}
#so-booking-widget[data-template="soft"] .so-bw-summary-section {
    border: 0;
    padding: 0;
}
#so-booking-widget[data-template="soft"] .so-bw-summary-section-title {
    margin: 20px 0 6px;
    font-size: 13px;
    font-weight: 700;
    letter-spacing: 0;
    text-transform: none;
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}
#so-booking-widget[data-template="soft"] .so-bw-summary-section:first-child .so-bw-summary-section-title {
    margin-top: 14px;
}
#so-booking-widget[data-template="soft"] .so-bw-summary-row {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 1px;
    padding: var(--so-bw-row-pad-y) 0;
}
#so-booking-widget[data-template="soft"] .so-bw-summary-label {
    order: -1;
    font-size: 11.5px;
    font-weight: 600;
    color: var(--so-bw-text-secondary);
}
#so-booking-widget[data-template="soft"] .so-bw-summary-value {
    font-size: 16px;
    font-weight: 600;
    line-height: 1.35;
    text-align: left;
}
/* The money keeps its two-column shape — a price under its own label reads as
   a heading with a number, and this is the one row that should read as a
   total. */
#so-booking-widget[data-template="soft"] .so-bw-summary-row--total {
    flex-direction: row;
    align-items: baseline;
    justify-content: space-between;
    gap: 14px;
    margin: 10px -20px 0;
    padding: 16px 20px;
    border-top: 0;
    border-radius: 0 0 var(--so-bw-r-22) var(--so-bw-r-22);
    background: color-mix(in srgb, var(--so-bw-primary) 12%, transparent);
}
#so-booking-widget[data-template="soft"] .so-bw-summary-row--total .so-bw-summary-label {
    order: 0;
    font-size: 14px;
    color: var(--so-bw-text);
}
#so-booking-widget[data-template="soft"] .so-bw-summary-row--total .so-bw-summary-value {
    font-size: 22px;
    font-weight: 800;
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}
/* 17.0's hairline block rules would draw a rule across a rounded blush panel
   every fourth row. The money block keeps one soft divider instead. */
#so-booking-widget[data-template="soft"] .so-bw-summary-row--rule,
#so-booking-widget[data-template="soft"] .so-bw-summary-row--deposit {
    border-top: 0;
    margin-top: 0;
    padding-top: 9px;
}
#so-booking-widget[data-template="soft"] .so-bw-summary-block {
    gap: 2px;
    padding-top: 12px;
    border-top-color: var(--so-bw-ds-n100);
}
#so-booking-widget[data-template="soft"] .so-bw-summary-block .so-bw-summary-row,
#so-booking-widget[data-template="soft"] .so-bw-summary-pay .so-bw-summary-row {
    flex-direction: row;
    align-items: baseline;
    justify-content: space-between;
    gap: 14px;
}
#so-booking-widget[data-template="soft"] .so-bw-summary-block .so-bw-summary-label,
#so-booking-widget[data-template="soft"] .so-bw-summary-pay .so-bw-summary-label {
    order: 0;
    font-size: 13px;
}
#so-booking-widget[data-template="soft"] .so-bw-summary-block .so-bw-summary-value,
#so-booking-widget[data-template="soft"] .so-bw-summary-pay .so-bw-summary-value {
    font-size: 14px;
    text-align: right;
}

/* ── N18 · WARM CLOSE ────────────────────────────────────────────────────
   The counterpart to Dusk's restraint, and the other end of the catalogue's
   emotional range: an oversized round tick, warm type, and the record in a
   soft-cornered card. */
#so-booking-widget[data-template="soft"] .so-bw-confirmation-icon {
    width: 84px;
    height: 84px;
    border-radius: 999px;
}
#so-booking-widget[data-template="soft"] .so-bw-confirmation-icon svg {
    width: 40px;
    height: 40px;
}
#so-booking-widget[data-template="soft"] .so-bw-confirmation-title {
    font-size: clamp(24px, calc(var(--so-bw-cw, 100vw) * 0.04), 32px);
    line-height: 1.15;
}
#so-booking-widget[data-template="soft"] .so-bw-confirmation-details {
    max-width: 420px;
    margin-inline: auto;
    padding: 6px 20px;
    border: 0;
    border-radius: var(--so-bw-r-22);
    background: var(--so-bw-ds-n0);
}
#so-booking-widget[data-template="soft"] .so-bw-confirmation-detail {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 1px;
    padding: var(--so-bw-row-pad-y) 0;
    text-align: left;
}
#so-booking-widget[data-template="soft"] .so-bw-confirmation-detail-label {
    order: -1;
    font-size: 11.5px;
    font-weight: 600;
}
#so-booking-widget[data-template="soft"] .so-bw-confirmation-detail-value {
    font-size: 16px;
    font-weight: 600;
    text-align: left;
}

/* was @media (max-width: 640px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    #so-booking-widget:where([data-cw~="640"])[data-template="soft"] .so-bw-slot-list{
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }

/* was @media (max-width: 480px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    /* Edge-to-edge, but the sheet stays attached to the top of the viewport
       rather than floating: square at the top, round at the foot. */
    #so-booking-widget:where([data-cw~="480"])[data-template="soft"] .so-bw-container,
    #so-booking-widget:where([data-cw~="480"])[data-template="soft"] .so-bw-header{
        border-radius: 0;
    }
    #so-booking-widget:where([data-cw~="480"])[data-template="soft"] .so-bw-nav{
        border-radius: 0 0 var(--so-bw-r-28) var(--so-bw-r-28);
    }
    #so-booking-widget:where([data-cw~="480"])[data-template="soft"] .so-bw-summary{
        padding-inline: 16px;
    }
    #so-booking-widget:where([data-cw~="480"])[data-template="soft"] .so-bw-summary-row--total{
        margin-inline: -16px;
        padding-inline: 16px;
    }



/* ==========================================================================
   17.9 · DAYPART  (daypart-teal, layout key "daypart")
   --------------------------------------------------------------------------
   THREE, EVERYWHERE.

   Section 14 made the dayparts the structure of the time step. This carries
   the triptych into the two steps either side of it, and fixes the one thing
   the panels could not say for themselves.

   THE COUNT IS THE POINT. A panel headed only "Evening" cannot tell a quiet
   evening from a full one, which is precisely the question this template
   exists to answer — "is there anything after work?" is a question about
   QUANTITY. slotsByPeriod now carries openCount and the head states it. That
   is the difference between three labelled columns and three panels that mean
   something.

   THE PANELS MUST NOT LEVEL TO THE TALLEST. A morning with eighteen openings
   beside an evening with two would give the evening sixteen slots of empty
   space, which reads as broken rather than as quiet. Each panel caps and
   scrolls inside itself.
   ========================================================================== */

/* ── C14 · TRIPTYCH GRID ─────────────────────────────────────────────────
   Three across, echoing the panel count. Same auto-fit construction the slot
   panel uses, so the two steps break at the same widths for the same reason
   rather than by coincidence. */
#so-booking-widget[data-template="daypart"] .so-bw-card-grid {
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 190px), 1fr));
    gap: 12px;
}
#so-booking-widget[data-template="daypart"] .so-bw-card-media {
    display: none;
}
#so-booking-widget[data-template="daypart"] .so-bw-card-title {
    font-size: 14px;
}
/* One line of description in a 190px column, not three: at that width a
   three-line clamp is most of the tile. The clamp is declared !important
   upstream to beat the never-truncate block, so overriding the line count
   needs the same weight. */
#so-booking-widget[data-template="daypart"] .so-bw-card-meta--clamp {
    -webkit-line-clamp: 2 !important;
    line-clamp: 2 !important;
}

/* ── Daypart · TIMES · the counted heads ─────────────────────────────────
   The head becomes a real head: a tinted bar with the period on the left and
   its count on the right, so the three panels can be compared at a glance
   without reading a single time. */
#so-booking-widget[data-template="daypart"] .so-bw-slot-group {
    border: 1px solid var(--so-bw-border);
    border-radius: var(--so-bw-ds-radius-card);
    overflow: hidden;
    /* The cap. Six rows of 44px plus gaps and the head; past that the panel
       scrolls rather than growing, so the three stay comparable. */
    max-height: 340px;
    display: flex;
    flex-direction: column;
}
#so-booking-widget[data-template="daypart"] .so-bw-slot-group-title {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 8px;
    flex: 0 0 auto;
    margin: 0;
    padding: 10px 12px;
    background: var(--so-bw-ds-n25);
    border-bottom: 1px solid var(--so-bw-border);
}
/* Emitted for every template, revealed here alone. */
#so-booking-widget .so-bw-slot-group-count { display: none; }
#so-booking-widget[data-template="daypart"] .so-bw-slot-group-count {
    display: inline;
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
    letter-spacing: 0.04em;
}
#so-booking-widget[data-template="daypart"] .so-bw-slot-list {
    grid-template-columns: 1fr;
    gap: 4px;
    flex: 1 1 auto;
    /* `min-height: 0` + `overflow-y: auto` was the pair that let a period panel
       shrink below its content so it could scroll. Released with the rest. */
    min-height: auto;
    padding: 8px;
    overflow-y: visible;
}
#so-booking-widget[data-template="daypart"] .so-bw-slot-row {
    justify-content: center;
    min-height: var(--so-bw-ds-tap);
    font-size: 13px;
}
/* A period with nothing in it is never emitted (slotsByPeriod filters empty
   buckets), so there is no empty-panel state to design — the day simply shows
   two panels instead of three, which is honest and needs no placeholder. */

/* ── F2 · TWO COLUMN ─────────────────────────────────────────────────────
   Shared. Assigned to Tabbed Rail, Daypart and Dual Month here; the primitive
   itself lives in section 16 with Week Board and Wide Canvas, and its 700px
   collapse is the grandfathered value it has always used. */
#so-booking-widget[data-template="tabbed"] .so-bw-form,
#so-booking-widget[data-template="daypart"] .so-bw-form {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 14px 18px;
    align-items: start;
}
#so-booking-widget[data-template="tabbed"] .so-bw-form-group,
#so-booking-widget[data-template="daypart"] .so-bw-form-group {
    grid-column: auto;
}
#so-booking-widget[data-template="tabbed"] .so-bw-form-row,
#so-booking-widget[data-template="daypart"] .so-bw-form-row {
    display: contents;
}
#so-booking-widget[data-template="tabbed"] .so-bw-form-group:has(.so-bw-textarea),
#so-booking-widget[data-template="daypart"] .so-bw-form-group:has(.so-bw-textarea) {
    grid-column: 1 / -1;
}
/* was @media (max-width:700px) — now gated on the CONTAINER width
   via data-cw (see booking-widget.js _wireContainerWidth). :where()
   keeps specificity identical to the media query it replaces. */

    #so-booking-widget:where([data-cw~="700"])[data-template="tabbed"] .so-bw-form,
    #so-booking-widget:where([data-cw~="700"])[data-template="daypart"] .so-bw-form{
        grid-template-columns: 1fr;
    }


/* ── R10 · THREE BLOCK ───────────────────────────────────────────────────
   The review takes the triptych too: What, When and Who as three columns,
   with the money full width beneath. Built on the row classes rather than on
   nth-child, because which rows exist depends on the booking — a home visit
   has no location row, a single-service booking has no services list.

   `order` groups them into columns; the DOM order is unchanged, so a screen
   reader still hears the summary in the order the booking was made. */
#so-booking-widget[data-template="daypart"] .so-bw-summary-main {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 0 20px;
    align-items: start;
}
#so-booking-widget[data-template="daypart"] .so-bw-summary-section {
    display: contents;
}
/* The two section headings would land in arbitrary cells of a three-column
   grid. The columns are labelled by their own eyebrows instead, drawn on the
   first row of each. */
#so-booking-widget[data-template="daypart"] .so-bw-summary-section-title {
    display: none;
}
#so-booking-widget[data-template="daypart"] .so-bw-summary-row {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 2px;
    padding: var(--so-bw-row-pad-y) 0;
    border-bottom: 1px solid var(--so-bw-ds-n100);
}
#so-booking-widget[data-template="daypart"] .so-bw-summary-label {
    font-family: var(--so-bw-mono);
    font-size: 10.5px;
    font-weight: 500;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--so-bw-text-secondary);
}
#so-booking-widget[data-template="daypart"] .so-bw-summary-value {
    font-size: 14px;
    text-align: left;
}
/* WHAT — column 1. */
#so-booking-widget[data-template="daypart"] .so-bw-summary-row--service,
#so-booking-widget[data-template="daypart"] .so-bw-summary-row--addons,
#so-booking-widget[data-template="daypart"] .so-bw-summary-row--duration {
    grid-column: 1;
}
/* WHEN — column 2. */
#so-booking-widget[data-template="daypart"] .so-bw-summary-row--date,
#so-booking-widget[data-template="daypart"] .so-bw-summary-row--time,
#so-booking-widget[data-template="daypart"] .so-bw-summary-row--tz {
    grid-column: 2;
}
/* WHO and WHERE — column 3. Everything unassigned (custom fields, contact
   details) falls to auto placement, which fills the shortest column — the
   correct behaviour for rows whose count is unknown at authoring time. */
#so-booking-widget[data-template="daypart"] .so-bw-summary-row--staff,
#so-booking-widget[data-template="daypart"] .so-bw-summary-row--location {
    grid-column: 3;
}
#so-booking-widget[data-template="daypart"] .so-bw-summary-row--wide {
    grid-column: 1 / -1;
}
#so-booking-widget[data-template="daypart"] .so-bw-summary-row--wide.so-bw-summary-row--service,
#so-booking-widget[data-template="daypart"] .so-bw-summary-row--wide.so-bw-summary-row--addons {
    /* A multi-service list is --wide AND belongs in column 1. Column wins:
       the grouping is the design, and the list wraps happily in a third of
       860px. */
    grid-column: 1;
}
#so-booking-widget[data-template="daypart"] .so-bw-summary-pay {
    margin-top: 18px;
    padding-top: 4px;
    border-top: 2px solid var(--so-bw-text);
}

/* ── N13 · DAYPART MARK ──────────────────────────────────────────────────
   The confirmation names the part of the day, because that is what the
   customer came here to choose. Set as an accent line above the record. */
#so-booking-widget[data-template="daypart"] .so-bw-confirmation-details {
    max-width: 460px;
    margin-inline: auto;
    padding: 0 16px;
    border: 1px solid var(--so-bw-border);
    border-top: 4px solid var(--so-bw-primary);
    border-radius: var(--so-bw-ds-radius-card);
}
#so-booking-widget[data-template="daypart"] .so-bw-confirmation-detail {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, auto);
    gap: 12px;
    padding: var(--so-bw-row-pad-y) 0;
    border-bottom: 1px solid var(--so-bw-ds-n100);
}
#so-booking-widget[data-template="daypart"] .so-bw-confirmation-detail:last-child {
    border-bottom: 0;
}
#so-booking-widget[data-template="daypart"] .so-bw-confirmation-detail-value {
    text-align: right;
}

/* was @media (max-width:768px) — now gated on the CONTAINER width
   via data-cw (see booking-widget.js _wireContainerWidth). :where()
   keeps specificity identical to the media query it replaces. */

    /* Three columns of summary in less than 768px is 240px a column, which
       wraps a service name to four lines. Two, then one. */
    #so-booking-widget:where([data-cw~="768"])[data-template="daypart"] .so-bw-summary-main{
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }
    #so-booking-widget:where([data-cw~="768"])[data-template="daypart"] .so-bw-summary-row--staff,
    #so-booking-widget:where([data-cw~="768"])[data-template="daypart"] .so-bw-summary-row--location{
        grid-column: auto;
    }

/* was @media (max-width: 640px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    #so-booking-widget:where([data-cw~="640"])[data-template="daypart"] .so-bw-summary-main{
        grid-template-columns: 1fr;
    }
    #so-booking-widget:where([data-cw~="640"])[data-template="daypart"] .so-bw-summary-row--service,
    #so-booking-widget:where([data-cw~="640"])[data-template="daypart"] .so-bw-summary-row--addons,
    #so-booking-widget:where([data-cw~="640"])[data-template="daypart"] .so-bw-summary-row--duration,
    #so-booking-widget:where([data-cw~="640"])[data-template="daypart"] .so-bw-summary-row--date,
    #so-booking-widget:where([data-cw~="640"])[data-template="daypart"] .so-bw-summary-row--time,
    #so-booking-widget:where([data-cw~="640"])[data-template="daypart"] .so-bw-summary-row--tz,
    #so-booking-widget:where([data-cw~="640"])[data-template="daypart"] .so-bw-summary-row--wide.so-bw-summary-row--service,
    #so-booking-widget:where([data-cw~="640"])[data-template="daypart"] .so-bw-summary-row--wide.so-bw-summary-row--addons{
        grid-column: auto;
    }
    /* Stacked, the panels are sections rather than columns, so the cap and
       the internal scroll would hide times behind a scrollbar nobody expects
       in a single column. Released. */
    #so-booking-widget:where([data-cw~="640"])[data-template="daypart"] .so-bw-slot-group{
        max-height: none;
    }
    #so-booking-widget:where([data-cw~="640"])[data-template="daypart"] .so-bw-slot-list{
        overflow-y: visible;
    }



/* ==========================================================================
   17.10 · TIMELINE  (timeline-plum, layout key "timeline")
   --------------------------------------------------------------------------
   TIME HAS A SHAPE.

   The day axis in section 14 is the best thing in the catalogue and it only
   appears on one screen. The argument behind it — that a duration is a length,
   not a number — is just as true at the choice step, where a fifteen-minute
   consultation and a two-and-three-quarter-hour balayage are printed in the
   same 12px badge and the customer does the comparing.

   C15 draws them. It is the one thing this template can say at the choice
   step that no other template can, and the reason the duration-bar getter was
   worth the JS.

   THE AXIS GUTTER IS A FIXED GEOMETRY. padding-left minus line-left is 18px,
   and .so-bw-slot-row::before's -22px node offset derives from it. Anything
   new that hangs off the axis derives from the same constant or the nodes
   fall off the line.
   ========================================================================== */

/* ── M1 · THE SHELL — a sidebar and a pane ───────────────────────────────
   Moved here from §8781, where it was stated with 79 `!important`s purely to
   outrank three LATER (1,2,0) rules. Below them, the same selectors win on
   source order. Nothing in this block is `!important` and nothing needs to be.

   THE HEADER IS DISSOLVED, NOT MOVED. `.so-bw-header` holds three children
   that belong in three different cells: the brand block and the numbered step
   bar go in the sidebar, the step title goes at the top of the pane.
   `display: contents` removes the header's own box so all three become direct
   items of the container grid — no element moves in the shared markup, which
   is what keeps this change off the other twenty-one templates.

   Consequence to remember: a dissolved box cannot paint. The header's old
   background and borders are gone, and the sidebar's surface is drawn by the
   container's ::before instead (a pseudo-element of a grid container is itself
   a grid item, so it can be placed in column 1 and span every row).

   `.so-bw-brand` is optional — it renders only when Studio's
   "show brand header" is on AND a title is set (booking-widget.js, x-if). The
   sidebar must read correctly without it, which is why the step bar owns its
   own top padding rather than relying on the brand block above it. */
#so-booking-widget[data-template="timeline"] .so-bw-container {
    --so-bw-tl-sidebar: 244px;
    --so-bw-gutter: 28px;
    display: grid;
    grid-template-columns: var(--so-bw-tl-sidebar) 1fr;
    /* minmax(0, 1fr) on the middle row, never a bare 1fr: a grid row sized to
       1fr has an automatic MINIMUM of its content, so a long service list would
       stretch the shell instead of scrolling inside it and `overflow-y: auto`
       on the body would never engage. */
    grid-template-rows: auto minmax(0, 1fr) auto auto;
    max-height: min(78vh, 720px);
    /* The signature: flat down the left, radiused down the right. See the
       corner note in WidgetTemplateCatalog::RADIUS before changing either. */
    border-left: 4px solid var(--so-bw-primary, #a021b0);
    border-radius: 0 var(--so-bw-r-8) var(--so-bw-r-8) 0;
    overflow: hidden;
}
#so-booking-widget[data-template="timeline"] .so-bw-container::before {
    content: '';
    grid-column: 1;
    grid-row: 1 / -1;
    background: var(--so-bw-ds-n0, #fff);
    border-right: 1px solid var(--so-bw-ds-n100, #ebecf0);
}
#so-booking-widget[data-template="timeline"] .so-bw-header {
    display: contents;
}
/* ── The sidebar's two occupants ───────────────────────────────────────── */
#so-booking-widget[data-template="timeline"] .so-bw-brand {
    grid-column: 1;
    grid-row: 1;
    align-self: start;
    padding: 22px 20px 16px;
}
#so-booking-widget[data-template="timeline"] .so-bw-stepbar {
    grid-column: 1;
    grid-row: 2;
    align-self: start;
    /* The base bar is a horizontal, horizontally-scrolling flex row. Every
       declaration here is undoing one half of that. */
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    width: auto;
    margin: 0;
    padding: 4px 20px 20px;
    overflow: visible;
}
#so-booking-widget[data-template="timeline"] .so-bw-stepbar-item {
    /* The axis contract (see "THE AXIS PRIMITIVE" in §13). The dot here is a
       real element rather than a pseudo, so it sits at the item's own left edge
       and the line's centre is simply half a dot in. */
    --so-bw-axis-node: 26px;
    --so-bw-axis-x: calc(var(--so-bw-axis-node) / 2);
    --so-bw-tl-step-pad: 9px;
    position: relative;
    flex: 0 0 auto;
    flex-direction: row;
    align-items: center;
    justify-content: flex-start;
    gap: 12px;
    min-width: 0;
    padding: var(--so-bw-tl-step-pad) 0;
    text-align: left;
}
/* The connector spans the GAP between one dot and the next. Both ends are
   derived rather than typed: it starts where the dot ends (pad + node) and runs
   one padding into the next item, so changing the dot size or the padding moves
   it automatically instead of leaving the rail as loose beads. */
#so-booking-widget[data-template="timeline"] .so-bw-stepbar-item:not(:last-child)::after {
    content: '';
    position: absolute;
    left: calc(var(--so-bw-axis-x) - var(--so-bw-axis-line-w) / 2);
    top: calc(var(--so-bw-tl-step-pad) + var(--so-bw-axis-node));
    bottom: calc(var(--so-bw-tl-step-pad) * -1);
    width: var(--so-bw-axis-line-w);
    background: var(--so-bw-axis-line);
}
#so-booking-widget[data-template="timeline"] .so-bw-stepbar-item--done:not(:last-child)::after,
#so-booking-widget[data-template="timeline"] .so-bw-stepbar-item--active:not(:last-child)::after {
    background: var(--so-bw-primary, #a021b0);
}
/* The base bar draws its own horizontal connector from each item to its left
   neighbour. In a column that line is a lie. */
#so-booking-widget[data-template="timeline"] .so-bw-stepbar-item:not(:first-of-type)::before {
    display: none;
}
#so-booking-widget[data-template="timeline"] .so-bw-stepbar-dot {
    position: relative;
    z-index: 1;
    flex-shrink: 0;
    width: var(--so-bw-axis-node);
    height: var(--so-bw-axis-node);
    font-size: 12px;
}
#so-booking-widget[data-template="timeline"] .so-bw-stepbar-dot svg {
    width: 12px;
    height: 12px;
}
#so-booking-widget[data-template="timeline"] .so-bw-stepbar-label {
    /* A wrapped label costs HEIGHT in a column, which this sidebar has, rather
       than width, which it does not. The base bar sets nowrap for the opposite
       geometry. */
    min-width: 0;
    white-space: normal;
    overflow-wrap: anywhere;
    text-align: left;
    font-size: 13px;
    line-height: 1.3;
}
/* ── The pane ──────────────────────────────────────────────────────────── */
/* ROW 1 IS SHARED WITH THE BRAND BLOCK, and CSS Grid has no way to give one
   column its own row flow — so row 1 is as tall as the TALLER of the brand
   block and this title, and the shorter one gets the slack. Today the brand
   wins (124px against ~54px) and the slack shows as a gap between the title and
   the step description below it.

   That is a SIZING fix, not a structural one: the design phase brings the brand
   block down to a 36px mark with a 15px title, which lands it within a few
   pixels of this block and closes the gap. Do not try to solve it by pulling
   the title out of the header — `.so-bw-step-indicator` is shared markup and
   moving it relocates the title on all twenty-two templates.

   `align-self: start` so the title sits on row 1's top edge and the slack falls
   below it, rather than the block stretching and floating the title mid-row. */
#so-booking-widget[data-template="timeline"] .so-bw-step-indicator {
    grid-column: 2;
    grid-row: 1;
    align-self: start;
    display: flex;
    flex-direction: row;
    align-items: baseline;
    justify-content: space-between;
    gap: 16px;
    margin: 0;
    padding: 24px var(--so-bw-gutter) 0;
}
#so-booking-widget[data-template="timeline"] .so-bw-body {
    grid-column: 2;
    grid-row: 2;
    /* min-height:0 is what actually lets this scroll — see the row note above. */
    min-height: 0;
    overflow-y: auto;
}
#so-booking-widget[data-template="timeline"] .so-bw-nav {
    grid-column: 2;
    grid-row: 3;
    border-top: 1px solid var(--so-bw-ds-n100, #ebecf0);
}
/* EVERY IN-FLOW CHILD OF THE CONTAINER NEEDS A CELL. `.so-bw-powered` is the
   only one besides the three above — the rest are `display: none`, absolute
   (`.so-bw-loading`, `.so-bw-sr-only`) or fixed (the two `.so-bw-sp` popups),
   and so are not grid items at all. Unplaced, it was auto-flowed into an
   invented fourth row of the SIDEBAR column and rendered 244px wide under the
   step list. Dusk documents the same hazard for its own grid; the list of
   exemptions is identical. */
#so-booking-widget[data-template="timeline"] .so-bw-powered {
    grid-column: 1 / -1;
    grid-row: 4;
    border-top: 1px solid var(--so-bw-ds-n100, #ebecf0);
}
/* The step bar in the sidebar is the only progress signal. The slim bar was a
   second, coarser reading of the same fact and "Step n of N" is a third; the
   count stays because it is the only one that says how many are left. The
   catalogue-wide kill already sets `display: none`, so this template simply
   stops reviving it — there is nothing to declare here, and the absence is
   deliberate rather than an omission. */

/* ── Primitives this architecture reveals ──────────────────────────────────
   Each is emitted for every template and hidden by a base rule; the comp uses
   all three, so each needs a reveal at the specificity its own default scores.
   Styling belongs to the design phase — these turn them ON only. */
#so-booking-widget[data-template="timeline"] .so-bw-catbar {
    display: flex;
}
#so-booking-widget[data-template="timeline"] .so-bw-card-pick {
    display: inline-flex;
}
#so-booking-widget[data-template="timeline"] .so-bw-navstats {
    display: flex;
}
/* Label above value. The markup is <strong> then <small> — value first — and
   it is shared by every template, so the order is corrected in the cascade
   rather than by reordering markup twenty-two templates read. */
#so-booking-widget[data-template="timeline"] .so-bw-navstat__text {
    display: flex;
    flex-direction: column-reverse;
}
/* The selected-service names line. Emitted for every template, revealed here —
   Wide Canvas and Week Board dock this same bar and neither comp has the line,
   so the default has to be off. It sits BELOW the count, which is why it is
   ordered first in a column-reverse stack above. */
#so-booking-widget .so-bw-navstat__names { display: none; }
#so-booking-widget[data-template="timeline"] .so-bw-navstat__names {
    display: block;
    order: -1;
}

/* ── M2 · THE DESIGN PASS ────────────────────────────────────────────────
   M1 put every box in its cell. This gives them their size, weight and states.
   Nothing here moves a box; if a rule does, it belongs in M1.

   Colour comes from exactly two places — the `--so-bw-ds-n*` ramp for greys and
   `--so-bw-primary` for the accent. There is no third source, so setting the
   accent to a red or a navy in Studio produces a coherent template rather than
   a plum-tinted one. */

/* ── The sidebar ─────────────────────────────────────────────────────────
   The brand block is deliberately COMPACT, and that is a layout fact as much as
   a typographic one: it shares row 1 with the pane's title (see the note in
   M1), so every pixel it is taller than that title becomes a gap under the
   title. At a 36px mark and a 15px title it lands within a few pixels. */
#so-booking-widget[data-template="timeline"] .so-bw-brand {
    align-items: flex-start;
    gap: 10px;
    /* Explicit zero: the base brand block carries a bottom margin for the
       stacked headers every other template uses, and in row 1 of this grid that
       margin is pure gap under the pane's title.

       THE PADDING IS LOAD-BEARING, not taste. Row 1 is shared with the pane's
       title and grid has no per-column row flow, so every pixel this block is
       taller than the title block becomes empty space under the title. At a
       34px mark and 18/10 padding it lands within ~15px of the title, which is
       what closes the gap the comp does not have. Grow this and the gap comes
       back. */
    margin: 0;
    padding: 18px 20px 10px;
}
#so-booking-widget[data-template="timeline"] .so-bw-brand__mark {
    width: 34px;
    height: 34px;
    flex: 0 0 34px;
    border-radius: var(--so-bw-ds-radius-ctl);
    background: var(--so-bw-primary);
    color: #fff;
}
#so-booking-widget[data-template="timeline"] .so-bw-brand__mark svg {
    width: 18px;
    height: 18px;
}
#so-booking-widget[data-template="timeline"] .so-bw-brand__title {
    font-size: 15px;
    font-weight: 700;
    line-height: 1.25;
    letter-spacing: -0.01em;
    color: var(--so-bw-text);
}
#so-booking-widget[data-template="timeline"] .so-bw-brand__subtitle {
    margin-top: 4px;
    font-size: 12px;
    font-weight: 500;
    line-height: 1.45;
    color: var(--so-bw-ds-n500, var(--so-bw-text-secondary));
}
/* The step rail. Three states told apart by more than hue, so the current step
   survives a greyscale read: PENDING is a hairline ring on white, DONE is a
   filled disc with a tick, CURRENT is a filled disc with a soft halo — the only
   one carrying a ring around it. The halo is drawn with box-shadow so the disc
   never changes size and the rail's rhythm does not shift as the customer
   advances. */
#so-booking-widget[data-template="timeline"] .so-bw-stepbar-item {
    --so-bw-tl-step-pad: 18px;
}
#so-booking-widget[data-template="timeline"] .so-bw-stepbar-dot {
    border: 1.5px solid var(--so-bw-ds-n300);
    background: var(--so-bw-ds-n0);
    color: var(--so-bw-ds-n500, var(--so-bw-text-secondary));
    font-weight: 600;
}
#so-booking-widget[data-template="timeline"] .so-bw-stepbar-item--active .so-bw-stepbar-dot,
#so-booking-widget[data-template="timeline"] .so-bw-stepbar-item--done .so-bw-stepbar-dot {
    border-color: var(--so-bw-primary);
    background: var(--so-bw-primary);
    color: #fff;
}
#so-booking-widget[data-template="timeline"] .so-bw-stepbar-item--active .so-bw-stepbar-dot {
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--so-bw-primary) 18%, transparent);
}
#so-booking-widget[data-template="timeline"] .so-bw-stepbar-label {
    font-weight: 500;
    color: var(--so-bw-ds-n500, var(--so-bw-text-secondary));
}
#so-booking-widget[data-template="timeline"] .so-bw-stepbar-item--active .so-bw-stepbar-label {
    font-weight: 600;
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}
#so-booking-widget[data-template="timeline"] .so-bw-stepbar-item--done .so-bw-stepbar-label {
    color: var(--so-bw-text);
}

/* ── The pane's measure ──────────────────────────────────────────────────
   THIS IS WHAT LETS FULL WIDTH BE FULL WIDTH. The shell is uncapped, so on a
   1920px page the pane is ~1650px — and a service row stretched to that puts
   the name on the far left and its Select button on the far right, with the
   duration and price adrift in between. A shell can be as wide as the page; a
   ROW cannot be, because a row is read.

   So the shell fills the screen and the content inside it keeps a measure and
   centres. At the natural 1040px width the pane is ~796px, already under this
   cap, so nothing moves — this only engages once there is more width than the
   content can use.

   Applied to the step and the step title together so the heading stays on the
   same left edge as the cards under it. The nav keeps spanning: it is a docked
   bar and its background is meant to reach both edges. */
#so-booking-widget[data-template="timeline"] .so-bw-container {
    --so-bw-tl-measure: 900px;
}
/* PADDING, NOT `max-width` + `margin-inline: auto`. Auto margins switch OFF a
   grid item's stretch, so the header stopped filling its column, shrank to fit
   its own text and centred — which put "Choose a Service" adrift in the middle
   of the pane instead of on the same left edge as the cards below it. Insetting
   full-width items keeps them full-width. */
#so-booking-widget[data-template="timeline"] .so-bw-step,
#so-booking-widget[data-template="timeline"] .so-bw-step-indicator {
    padding-inline: max(
        var(--so-bw-gutter),
        calc((100% - var(--so-bw-tl-measure)) / 2)
    );
}
/* The docked bar follows the same measure, but its inset is stated with the
   rest of its padding in "The docked bar" below — a `padding-inline` here would
   be reset by that block's `padding` shorthand, which is later in source.
   It uses padding rather than a max-width because a max-width would shrink the
   painted bar too, and `justify-content` cannot do it either: the stats carry
   `margin-right: auto` to hold Continue at the far end, and an auto margin
   beats any justify-content. */

/* ── The pane ────────────────────────────────────────────────────────────── */
#so-booking-widget[data-template="timeline"] .so-bw-step-label {
    font-size: 24px;
    font-weight: 700;
    line-height: 1.2;
    letter-spacing: -0.02em;
    color: var(--so-bw-text);
}
#so-booking-widget[data-template="timeline"] .so-bw-step-count {
    font-size: 13px;
    font-weight: 500;
    font-variant-numeric: tabular-nums;
    color: var(--so-bw-ds-n500, var(--so-bw-text-secondary));
}
#so-booking-widget[data-template="timeline"] .so-bw-step-desc {
    margin: 0 0 18px;
    font-size: 14px;
    line-height: 1.5;
    color: var(--so-bw-ds-n500, var(--so-bw-text-secondary));
}
/* The search field stops stretching. A 790px input for a two-word query reads
   as a form the step is built around rather than a tool beside the list. */
#so-booking-widget[data-template="timeline"] .so-bw-search {
    max-width: 100%;
    margin-bottom: 14px;
}
#so-booking-widget[data-template="timeline"] .so-bw-search input {
    height: 48px;
    border-radius: var(--so-bw-ds-radius-ctl);
    border: 1px solid var(--so-bw-ds-n200);
    font-size: 14px;
}
#so-booking-widget[data-template="timeline"] .so-bw-catbar {
    flex-wrap: wrap;
    gap: 8px;
    margin-bottom: 16px;
}
#so-booking-widget[data-template="timeline"] .so-bw-catchip {
    height: 36px;
    padding: 0 14px;
    border-radius: 999px;
    border: 1px solid var(--so-bw-ds-n200);
    background: var(--so-bw-ds-n0);
    font-size: 13px;
    font-weight: 500;
    color: var(--so-bw-text);
}
#so-booking-widget[data-template="timeline"] .so-bw-catchip--on {
    border-color: transparent;
    background: var(--so-bw-primary);
    color: #fff;
}

/* ── The service card ────────────────────────────────────────────────────── */
#so-booking-widget[data-template="timeline"] .so-bw-card-media {
    --so-bw-svc-avatar: 44px;
}
#so-booking-widget[data-template="timeline"] .so-bw-card-title {
    font-size: 15px;
    font-weight: 700;
}
#so-booking-widget[data-template="timeline"] .so-bw-card-meta {
    font-size: 13px;
    line-height: 1.45;
    color: var(--so-bw-ds-n500, var(--so-bw-text-secondary));
}
/* The commit control is REVEALED by M1 and sized nowhere near here. Its size,
   border, type and states belong to the service-row section further down,
   whose selector scores (1,4,0) — anything written at this (1,2,0) would lose
   silently and leave two descriptions of one button. Search
   "The commit control holds its own column".

   The focus ring stays, because nothing downstream states one: the UA default
   was the only thing marking focus, which is a different treatment from every
   other secondary control in the file. */
#so-booking-widget[data-template="timeline"] .so-bw-card-pick:focus-visible {
    outline: 2px solid var(--so-bw-primary);
    outline-offset: 2px;
}

/* ── The docked bar ──────────────────────────────────────────────────────── */
#so-booking-widget[data-template="timeline"] .so-bw-nav {
    gap: 20px;
    /* The block padding and the measure inset in ONE declaration. Stated as the
       `padding` shorthand further up and `padding-inline` here, the shorthand
       won on source order and silently reset the inset to the plain gutter —
       measured 28px where the measure asked for 388px. */
    padding: 14px max(
        var(--so-bw-gutter),
        calc((100% - var(--so-bw-tl-measure)) / 2)
    );
    background: var(--so-bw-ds-n25);
}
/* Stats hard left, Continue hard right — the comp reads them as one bar with
   the summary on the reading edge, not as a centred cluster. `margin-right:
   auto` rather than `justify-content` on the nav, because the nav also holds
   Back on later steps and that must stay on the left of the stats. */
#so-booking-widget[data-template="timeline"] .so-bw-navstats {
    align-items: center;
    justify-content: flex-start;
    gap: 0;
    margin-right: auto;
}
/* One hairline between stat groups, drawn on the gap rather than as an extra
   element, so a hidden stat takes its divider with it. */
#so-booking-widget[data-template="timeline"] .so-bw-navstat {
    gap: 10px;
    padding: 2px 22px;
}
#so-booking-widget[data-template="timeline"] .so-bw-navstat + .so-bw-navstat {
    border-left: 1px solid var(--so-bw-ds-n200);
}
#so-booking-widget[data-template="timeline"] .so-bw-navstat:first-child {
    padding-left: 0;
}
/* ONE icon on the bar, not three. The comp marks the selection with the bag
   tile and leaves the two figures to their labels — three accent tiles in a row
   read as three buttons, and the labels ("Total duration", "Total price")
   already say what each number is. */
#so-booking-widget[data-template="timeline"] .so-bw-navstat + .so-bw-navstat .so-bw-navstat__icon {
    display: none;
}
#so-booking-widget[data-template="timeline"] .so-bw-navstat__icon {
    /* The base icon is an inline span, and width/height do nothing to an inline
       box — it measured 40px in the computed style and still drew at glyph size. */
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    flex: 0 0 40px;
    border-radius: var(--so-bw-ds-radius-ctl);
    background: color-mix(in srgb, var(--so-bw-primary) 10%, transparent);
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}
#so-booking-widget[data-template="timeline"] .so-bw-navstat__text strong {
    font-size: 15px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    color: var(--so-bw-text);
}
#so-booking-widget[data-template="timeline"] .so-bw-navstat__text small,
#so-booking-widget[data-template="timeline"] .so-bw-navstat__names {
    font-size: 12px;
    font-weight: 500;
    color: var(--so-bw-ds-n500, var(--so-bw-text-secondary));
}

/* ── M3 · THE COLLAPSE ───────────────────────────────────────────────────
   A 244px sidebar beside a pane is a desktop composition. Held below ~900px it
   leaves 328px of pane at 640 and 112px at 380 — measured, and unusable — so
   the two columns become one and the step rail lies down across the top.

   A NOTE ON THE AUDIT'S D8, WHICH THIS DELIBERATELY DOES NOT FOLLOW.
   That finding said the vertical rail should STAY vertical on a phone, on the
   grounds that height is free by scrolling and width is not. That reasoning
   holds for the day axis and is applied there — see the axis tier below, which
   keeps the axis vertical and only re-homes its period label. It does NOT hold
   for the step rail: six stacked rows at a 62px pitch is ~370px of chrome above
   the first service, i.e. a whole phone screen spent before the customer sees
   anything to choose. Height being free does not make it worth spending. The
   rail lies down; the axis does not.

   Gated on the container, not the viewport, so a widget in a narrow column on a
   wide page collapses too. The ID stays outside :where() so the selector still
   scores (1,0,0). */
    #so-booking-widget:where([data-cw~="900"])[data-template="timeline"] .so-bw-container{
        grid-template-columns: 1fr;
        /* brand · rail · title · body · nav · powered */
        grid-template-rows: auto auto auto minmax(0, 1fr) auto auto;
        /* The fixed-height card is a desktop affordance: on a phone the page is
           the scroll region and a card that scrolls inside a scrolling page
           traps the flow. */
        max-height: none;
    }
    /* The sidebar surface has no column to paint any more. */
    #so-booking-widget:where([data-cw~="900"])[data-template="timeline"] .so-bw-container::before{
        display: none;
    }
    #so-booking-widget:where([data-cw~="900"])[data-template="timeline"] .so-bw-brand{
        grid-column: 1;
        grid-row: 1;
        padding: 16px var(--so-bw-gutter) 12px;
    }
    #so-booking-widget:where([data-cw~="900"])[data-template="timeline"] .so-bw-stepbar{
        grid-column: 1;
        grid-row: 2;
        flex-direction: row;
        align-items: flex-start;
        /* Zero, and load-bearing: the horizontal connector below is measured in
           percentages of an item's own width, which only lines up with the next
           dot if the items are flush. */
        gap: 0;
        padding: 4px var(--so-bw-gutter) 14px;
        overflow-x: auto;
    }
    #so-booking-widget:where([data-cw~="900"])[data-template="timeline"] .so-bw-stepbar-item{
        flex: 1 1 0;
        flex-direction: column;
        align-items: center;
        gap: 6px;
        padding: 0;
        text-align: center;
    }
    /* The connector turns through 90°, and both ends still come off the axis
       tokens: it starts at this dot's right edge and ends at the next dot's
       left edge, which — with equal-width items and no gap — is
       `-50% + node / 2` from this item's right edge. */
    #so-booking-widget:where([data-cw~="900"])[data-template="timeline"] .so-bw-stepbar-item:not(:last-child)::after{
        left: calc(50% + var(--so-bw-axis-node) / 2);
        right: calc(var(--so-bw-axis-node) / 2 - 50%);
        top: calc(var(--so-bw-axis-node) / 2 - var(--so-bw-axis-line-w) / 2);
        bottom: auto;
        width: auto;
        height: var(--so-bw-axis-line-w);
    }
    #so-booking-widget:where([data-cw~="900"])[data-template="timeline"] .so-bw-stepbar-label{
        text-align: center;
        font-size: 11px;
        line-height: 1.25;
    }
    #so-booking-widget:where([data-cw~="900"])[data-template="timeline"] .so-bw-step-indicator{
        grid-column: 1;
        grid-row: 3;
        padding: 0 var(--so-bw-gutter);
    }
    #so-booking-widget:where([data-cw~="900"])[data-template="timeline"] .so-bw-body{
        grid-column: 1;
        grid-row: 4;
        /* The page scrolls now, not the body. */
        overflow-y: visible;
    }
    #so-booking-widget:where([data-cw~="900"])[data-template="timeline"] .so-bw-nav{
        grid-column: 1;
        grid-row: 5;
    }
    #so-booking-widget:where([data-cw~="900"])[data-template="timeline"] .so-bw-powered{
        grid-row: 6;
    }

    /* 640: the step labels stop fitting side by side long before the dots do.
       Keeping the dots and dropping the words leaves a bar that still says
       where you are and how many are left, which is what it is for. */
    #so-booking-widget:where([data-cw~="640"])[data-template="timeline"] .so-bw-stepbar-label{
        display: none;
    }
    #so-booking-widget:where([data-cw~="640"])[data-template="timeline"] .so-bw-step-label{
        font-size: 20px;
    }

    /* 480: the shared narrow gutter, so the header, the cards and the nav all
       sit on ONE left edge rather than three. */
    #so-booking-widget:where([data-cw~="480"])[data-template="timeline"] .so-bw-container{
        --so-bw-gutter: 16px;
    }
    #so-booking-widget:where([data-cw~="480"])[data-template="timeline"] .so-bw-nav{
        flex-wrap: wrap;
        gap: 12px;
    }
    /* The docked stats and the Continue button stop competing for one line. */
    #so-booking-widget:where([data-cw~="480"])[data-template="timeline"] .so-bw-navstats{
        flex: 1 1 100%;
        gap: 14px;
    }

/* ── C15 · DURATION BARS ─────────────────────────────────────────────────
   A bar per service, on one shared scale, floored at 6% so the shortest
   service is a mark rather than an empty track that reads as missing data.
   The percentage arrives as a custom property from serviceDurationPct(). */
/* One column of discrete cards, not a hairline-separated list. The list was
   right for a 680px shell where a border round every row read as noise; at
   1040px with a sidebar beside it, an unbordered row has nothing to sit in and
   the "Select" control on the far right floats away from the name on the left.
   `grid-auto-rows: auto` opts out of the catalogue's `1fr` levelling — hazard 4:
   right for a multi-column grid, wrong for a one-column list. */
#so-booking-widget[data-template="timeline"] .so-bw-card-grid {
    grid-template-columns: 1fr;
    gap: 12px;
}
#so-booking-widget[data-template="timeline"] .so-bw-card-grid:not(:has(.so-bw-card--has-extras-open)) {
    grid-auto-rows: auto;
}
/* The CARD's own surface — border, radius, background, hover and selected — is
   NOT stated here. `.so-bw-card--service` scores (1,3,0) against this rule's
   (1,2,0), and the service row's dedicated section further down already owns
   all five. Restating them here would lose to it silently and leave two
   descriptions of one card, which is the exact fault this file keeps finding.
   Search "The row · a card, not a hairline". */
/* The avatar is SHOWN. This block used to hide it, and the rule was dead: three
   later blocks at the same (1,2,0) — the service-card grid areas and the
   `--so-bw-svc-avatar: 36px` sizing — re-show it, and later source wins. So the
   hide never took effect and the comment above it described a card the widget
   has not rendered for a long time. Removed rather than "fixed": the comp has
   the avatar, and 36px is already the measured size. */
/* Emitted on every template, shown only here — x-show already returns false
   elsewhere, and this is the second gate so an unstyled bar can never appear
   if that getter is ever widened by accident. */
#so-booking-widget .so-bw-card-bar { display: none; }
#so-booking-widget[data-template="timeline"] .so-bw-card-bar {
    display: block;
    height: 4px;
    margin: 8px 0 2px;
    border-radius: 999px;
    background: var(--so-bw-ds-n100);
    overflow: hidden;
}
#so-booking-widget[data-template="timeline"] .so-bw-card-bar-fill {
    display: block;
    height: 100%;
    width: var(--so-bw-bar-pct, 0%);
    border-radius: 999px;
    background: var(--so-bw-primary);
    /* The bar is decorative — the duration badge below states the same fact in
       words, so nothing here is the only source of anything. */
    transition: width var(--so-bw-ds-motion);
}
#so-booking-widget[data-template="timeline"] .so-bw-badge--duration {
    background: none;
    border: 0;
    padding: 0;
    color: var(--so-bw-text);
    font-family: var(--so-bw-mono);
    font-variant-numeric: tabular-nums;
    font-weight: 600;
}
#so-booking-widget[data-template="timeline"] .so-bw-badge--price,
#so-booking-widget[data-template="timeline"] .so-bw-badge--free {
    background: none;
    border: 0;
    padding: 0;
    color: var(--so-bw-text-secondary);
    font-family: var(--so-bw-mono);
}

/* ── F11 · AXIS FIELDS and R9 · ITINERARY ────────────────────────────────
   Both shared with Agenda and both stated once, in 17.6. Timeline's selectors
   are in those rules; nothing is repeated here. */

/* ── N14 · AXIS PIN ──────────────────────────────────────────────────────
   The booking pinned on a short axis, so the customer sees where in the day
   it falls rather than only what o'clock it is. Same node-and-line vocabulary
   as the time step, at confirmation scale. */
#so-booking-widget[data-template="timeline"] .so-bw-confirmation-details {
    /* The pin's line centre and the inset it hangs off. Stated once; the rows'
       nodes derive from the pair. The old hand-typed -23px node offset
       disagreed with this arithmetic by 1px — that drift is what the tokens
       exist to make impossible. */
    --so-bw-tl-pin-x: 8px;
    --so-bw-tl-pin-pad: 26px;
    position: relative;
    max-width: 420px;
    margin-inline: auto;
    padding: 4px 0 4px var(--so-bw-tl-pin-pad);
    border: 0;
    background: none;
    text-align: left;
}
#so-booking-widget[data-template="timeline"] .so-bw-confirmation-details::before {
    content: '';
    position: absolute;
    left: calc(var(--so-bw-tl-pin-x) - var(--so-bw-axis-line-w) / 2);
    top: 14px;
    bottom: 14px;
    width: var(--so-bw-axis-line-w);
    background: var(--so-bw-axis-ring);
}
/* REPORTED: inner-overflow-x, span.so-bw-confirmation-detail-label, 12 > 4,
   on Timeline at every width from tablet-portrait up — a 4px label column
   holding 12px of text.

   `minmax(0, auto)` on the value resolves to MAX-CONTENT, so the longest
   value on the confirmation — the full localised date and time, set in mono
   with tabular figures — took the whole row and left the label a column too
   narrow for one character. Identical in cause and in fix to the Dual Month
   summary row in 17.19; the value is right-aligned there and here, so
   capping the track changes nothing about a short value. */
#so-booking-widget[data-template="timeline"] .so-bw-confirmation-detail {
    position: relative;
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 62%);
    gap: 12px;
    padding: var(--so-bw-row-pad-y) 0;
}
#so-booking-widget[data-template="timeline"] .so-bw-confirmation-detail::before {
    content: '';
    position: absolute;
    left: calc(var(--so-bw-tl-pin-x) - var(--so-bw-tl-pin-pad) - var(--so-bw-axis-node) / 2);
    top: 50%;
    width: var(--so-bw-axis-node);
    height: var(--so-bw-axis-node);
    margin-top: calc(var(--so-bw-axis-node) / -2);
    border-radius: 50%;
    background: var(--so-bw-card-bg);
    border: var(--so-bw-axis-line-w) solid var(--so-bw-ds-n300);
}
#so-booking-widget[data-template="timeline"] .so-bw-confirmation-detail:first-of-type::before {
    background: var(--so-bw-primary);
    border-color: var(--so-bw-primary);
}
#so-booking-widget[data-template="timeline"] .so-bw-confirmation-detail-value {
    text-align: right;
    font-family: var(--so-bw-mono);
    font-variant-numeric: tabular-nums;
}

/* was @media (max-width: 560px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    /* The existing axis collapse tier. The bar follows it: under the name
       rather than beside anything, which it already is — what changes is that
       the badges stop competing with it for the row. */
    #so-booking-widget:where([data-cw~="560"])[data-template="timeline"] .so-bw-card-bar{
        margin-top: 6px;
    }
    /* One token, and the nodes follow. The companion `left: -19px` rule that
       used to sit here carried the same 1px drift as the desktop tier. */
    #so-booking-widget:where([data-cw~="560"])[data-template="timeline"] .so-bw-confirmation-details{
        --so-bw-tl-pin-pad: 22px;
    }



/* ==========================================================================
   17.12 · FROST  (glass-frost, layout key "frost")
   --------------------------------------------------------------------------
   THE MATERIAL IS THE DESIGN.

   Separate frosted sheets floating on a gradient, evenly spaced, nothing
   overlapping and nothing opaque. Section 14 built the shell that way. The
   steps inside it did not follow, and a card with a solid white fill sitting
   on a frosted sheet is the one thing this template cannot have — it says the
   material is a paint job.

   TWO HARD CONSTRAINTS, both about the material:

   1. NOTHING NESTS. A frosted sheet on a frosted sheet is mud: two 55% whites
      composite to 80% and the layering stops reading. So where a child
      becomes a sheet, the parent stops being one.

   2. CONTRAST IS MEASURED AGAINST THE COMPOSITE, NOT THE SHEET. The backdrop
      is a gradient (#dbe6f5 → #eef3fb → #d7e3f4), so the effective background
      behind a 55% white sheet varies across the page. Body text on the
      DARKEST point of that composite is what has to clear 4.5:1, and the
      sheet fill below is set at the value that does — not at the value that
      looks best on the light end.

   THE FALLBACK IS OPAQUE, NOT TRANSPARENT. backdrop-filter does nothing under
   forced-colors and is unsupported in several in-app browsers. Without the
   @supports floor below, those users get 55% white over a gradient with no
   blur — text directly on a photograph of a sky. The fallback raises the fill
   to 92%, which is a slightly duller template rather than an unreadable one.
   ========================================================================== */

@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
    #so-booking-widget[data-template="frost"] .so-bw-header,
    #so-booking-widget[data-template="frost"] .so-bw-body,
    #so-booking-widget[data-template="frost"] .so-bw-nav {
        background: rgba(255, 255, 255, 0.92);
    }
}
@media (forced-colors: active) {
    #so-booking-widget[data-template="frost"] .so-bw-header,
    #so-booking-widget[data-template="frost"] .so-bw-body,
    #so-booking-widget[data-template="frost"] .so-bw-nav {
        background: Canvas;
        border-color: CanvasText;
    }
}

/* ── C17 · FLOATING TILES ────────────────────────────────────────────────
   Each card is its own sheet. Constraint 1 applies: .so-bw-body is a sheet
   already, so where the cards become sheets the body stops being one — the
   step region goes clear and the tiles float on the gradient directly. */
#so-booking-widget[data-template="frost"] .so-bw-card-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 12px;
}
#so-booking-widget[data-template="frost"] .so-bw-card:not(.so-bw-sp__panel) {
    /* 0.55 is the shell's value and it is measured against the gradient's
       LIGHT end. A card carries body copy at 13px, so it takes 0.66 — the
       value that clears 4.5:1 against the darkest point of the composite. */
    background: rgba(255, 255, 255, 0.66);
    -webkit-backdrop-filter: blur(14px) saturate(150%);
    backdrop-filter: blur(14px) saturate(150%);
    border: 1px solid rgba(255, 255, 255, 0.78);
    border-radius: var(--so-bw-r-16);
    box-shadow: 0 6px 24px -14px rgba(20, 40, 80, 0.4);
}
/* Constraint 3, and the one that is easy to get wrong: selection must not
   become a SOLID fill, or the selected card leaves the material while every
   other card stays in it. Opacity rises, the accent ring appears, the glass
   holds. (Hazard 3 says the 15% wash cannot be repainted anyway — this works
   with it rather than against it.) */
#so-booking-widget[data-template="frost"] .so-bw-card--selected:not(.so-bw-sp__panel) {
    background: rgba(255, 255, 255, 0.82);
    border-color: var(--so-bw-primary);
}
#so-booking-widget[data-template="frost"] .so-bw-card-media {
    border-radius: var(--so-bw-r-15) var(--so-bw-r-15) 0 0;
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
    #so-booking-widget[data-template="frost"] .so-bw-card:not(.so-bw-sp__panel) {
        background: rgba(255, 255, 255, 0.94);
    }
}

/* ── Frost · DATE & TIME ─────────────────────────────────────────────────
   The month on its own sheet and the times on a second one below it, evenly
   spaced — sheets, plural, which is the whole vocabulary. */
#so-booking-widget[data-template="frost"] .so-bw-datetime {
    grid-template-columns: 1fr;
    gap: 12px;
}
#so-booking-widget[data-template="frost"] .so-bw-calendar {
    max-width: none;
    margin-inline: 0;
    padding: 16px;
    background: rgba(255, 255, 255, 0.66);
    -webkit-backdrop-filter: blur(14px) saturate(150%);
    backdrop-filter: blur(14px) saturate(150%);
    border: 1px solid rgba(255, 255, 255, 0.78);
    border-radius: var(--so-bw-r-16);
}
#so-booking-widget[data-template="frost"] .so-bw-calendar-grid,
#so-booking-widget[data-template="frost"] .so-bw-calendar-weekdays {
    max-width: 460px;
    margin-inline: auto;
}
#so-booking-widget[data-template="frost"] .so-bw-calendar-day {
    background: rgba(255, 255, 255, 0.5);
}
#so-booking-widget[data-template="frost"] .so-bw-calendar-day--disabled,
#so-booking-widget[data-template="frost"] .so-bw-calendar-day--disabled:hover {
    background: transparent;
}
#so-booking-widget[data-template="frost"] .so-bw-slot-panel {
    padding: 16px;
    background: rgba(255, 255, 255, 0.66);
    -webkit-backdrop-filter: blur(14px) saturate(150%);
    backdrop-filter: blur(14px) saturate(150%);
    border: 1px solid rgba(255, 255, 255, 0.78);
    border-radius: var(--so-bw-r-16);
}
#so-booking-widget[data-template="frost"] .so-bw-slot-list {
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 104px), 1fr));
    gap: 8px;
}
#so-booking-widget[data-template="frost"] .so-bw-slot-row {
    justify-content: center;
    min-height: var(--so-bw-ds-tap);
    /* The chip has to be visible ON the sheet it sits on. The panel behind it is
       rgba(255,255,255,0.55); this was 0.62 fill with an 0.8 WHITE border — a
       fill 7% off its own background and a border lighter than both. The times
       read as bare floating text with no button edge at all, which on the one
       step where the customer is choosing between six things is a real failure,
       not a soft look.
       Frost's identity is translucency, so the fix keeps the glass and borrows
       the accent for the edge instead of reaching for a grey: the fill goes up
       enough to separate from the sheet, and the border becomes a tinted rule
       that reads at any backdrop lightness. Selected state below is unchanged
       (solid primary), so the two remain clearly different. */
    background: rgba(255, 255, 255, 0.82);
    border: 1px solid rgba(var(--so-bw-primary-rgb, 99, 102, 241), 0.28);
    font-size: 13px;
}
#so-booking-widget[data-template="frost"] .so-bw-slot-row--selected {
    background: var(--so-bw-primary);
    border-color: var(--so-bw-primary);
    color: #fff;
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
    #so-booking-widget[data-template="frost"] .so-bw-calendar,
    #so-booking-widget[data-template="frost"] .so-bw-slot-panel {
        background: rgba(255, 255, 255, 0.94);
    }
}

/* ── F8 · SHEET FIELDS ───────────────────────────────────────────────────
   The fields sit on one sheet, two-up where there is room. The inputs
   themselves are translucent, so the material runs all the way down — an
   opaque white input on a frosted sheet is the nesting fault in miniature. */
#so-booking-widget[data-template="frost"] .so-bw-form {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 14px 18px;
    align-items: start;
}
#so-booking-widget[data-template="frost"] .so-bw-form-row {
    display: contents;
}
#so-booking-widget[data-template="frost"] .so-bw-form-group:has(.so-bw-textarea) {
    grid-column: 1 / -1;
}
#so-booking-widget[data-template="frost"] .so-bw-input,
#so-booking-widget[data-template="frost"] .so-bw-select,
#so-booking-widget[data-template="frost"] .so-bw-textarea {
    background: rgba(255, 255, 255, 0.58);
    border-color: rgba(255, 255, 255, 0.85);
}
#so-booking-widget[data-template="frost"] .so-bw-input:focus,
#so-booking-widget[data-template="frost"] .so-bw-select:focus,
#so-booking-widget[data-template="frost"] .so-bw-textarea:focus {
    background: rgba(255, 255, 255, 0.86);
    border-color: var(--so-bw-primary);
}
/* was @media (max-width:768px) — now gated on the CONTAINER width
   via data-cw (see booking-widget.js _wireContainerWidth). :where()
   keeps specificity identical to the media query it replaces. */

    #so-booking-widget:where([data-cw~="768"])[data-template="frost"] .so-bw-form{
        grid-template-columns: 1fr;
    }


/* ── R11 · STACKED SHEETS ────────────────────────────────────────────────
   One sheet per section, evenly spaced, never nested. Constraint 1 again:
   .so-bw-summary stops being a container with a surface and becomes a stack
   of separate objects. */
#so-booking-widget[data-template="frost"] .so-bw-summary {
    display: flex;
    flex-direction: column;
    gap: 12px;
    border: 0;
    background: none;
    padding: 0;
}
#so-booking-widget[data-template="frost"] .so-bw-summary-main,
#so-booking-widget[data-template="frost"] .so-bw-summary-pay {
    display: flex;
    flex-direction: column;
    gap: 12px;
}
#so-booking-widget[data-template="frost"] .so-bw-summary-section,
#so-booking-widget[data-template="frost"] .so-bw-summary-block {
    padding: 14px 16px;
    background: rgba(255, 255, 255, 0.66);
    -webkit-backdrop-filter: blur(14px) saturate(150%);
    backdrop-filter: blur(14px) saturate(150%);
    border: 1px solid rgba(255, 255, 255, 0.78);
    border-radius: var(--so-bw-r-16);
}
#so-booking-widget[data-template="frost"] .so-bw-summary-block {
    gap: 6px;
    /* 17.0's top rule is what SEPARATED blocks when they were flush. They are
       separate objects now, with 12px of gradient between them, so the rule
       is a line inside a floating panel that divides nothing. */
    border-top-width: 1px;
}
#so-booking-widget[data-template="frost"] .so-bw-summary-row--rule,
#so-booking-widget[data-template="frost"] .so-bw-summary-row--deposit {
    border-top: 0;
    margin-top: 0;
    padding-top: 0;
}
#so-booking-widget[data-template="frost"] .so-bw-summary-row {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, auto);
    gap: 14px;
    align-items: baseline;
    padding: var(--so-bw-row-pad-y) 0;
}
#so-booking-widget[data-template="frost"] .so-bw-summary-row--wide {
    grid-template-columns: 1fr;
    gap: 4px;
}
#so-booking-widget[data-template="frost"] .so-bw-summary-value {
    text-align: right;
}
#so-booking-widget[data-template="frost"] .so-bw-summary-row--wide .so-bw-summary-value {
    text-align: left;
}
#so-booking-widget[data-template="frost"] .so-bw-summary-row--total {
    margin-top: 4px;
    padding-top: 12px;
    border-top: 1px solid rgba(255, 255, 255, 0.9);
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
    #so-booking-widget[data-template="frost"] .so-bw-summary-section,
    #so-booking-widget[data-template="frost"] .so-bw-summary-block {
        background: rgba(255, 255, 255, 0.94);
    }
}

/* ── N16 · SHEET ─────────────────────────────────────────────────────────
   One sheet, centred, floating on the gradient. The simplest possible ending
   for a template whose whole idea is a single pane of glass. */
#so-booking-widget[data-template="frost"] .so-bw-confirmation-details {
    max-width: 420px;
    margin-inline: auto;
    padding: 6px 18px;
    background: rgba(255, 255, 255, 0.66);
    -webkit-backdrop-filter: blur(14px) saturate(150%);
    backdrop-filter: blur(14px) saturate(150%);
    border: 1px solid rgba(255, 255, 255, 0.78);
    border-radius: var(--so-bw-r-16);
}
#so-booking-widget[data-template="frost"] .so-bw-confirmation-detail {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, auto);
    gap: 12px;
    padding: var(--so-bw-row-pad-y) 0;
    border-bottom: 1px solid rgba(255, 255, 255, 0.85);
}
#so-booking-widget[data-template="frost"] .so-bw-confirmation-detail:last-child {
    border-bottom: 0;
}
#so-booking-widget[data-template="frost"] .so-bw-confirmation-detail-value {
    text-align: right;
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
    #so-booking-widget[data-template="frost"] .so-bw-confirmation-details {
        background: rgba(255, 255, 255, 0.94);
    }
}

/* was @media (max-width: 640px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    #so-booking-widget:where([data-cw~="640"])[data-template="frost"] .so-bw-card-grid{
        grid-template-columns: 1fr;
    }

/* was @media (max-width: 480px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    /* The gradient field keeps a small inset so the sheets still read as
       floating ON something. Taking it to zero makes them panels again, which
       is the one thing this template must not become. */
    #so-booking-widget:where([data-cw~="480"])[data-template="frost"]{
        padding: 12px 8px;
    }
    #so-booking-widget:where([data-cw~="480"])[data-template="frost"] .so-bw-calendar,
    #so-booking-widget:where([data-cw~="480"])[data-template="frost"] .so-bw-slot-panel{
        padding: 12px;
    }



/* ==========================================================================
   17.15 · SHARED CORRECTIONS THE FOURTEEN EXPOSED
   --------------------------------------------------------------------------
   Nothing here is per-template design. These are faults in SHARED components
   that were invisible while thirteen templates rendered the same 2-up card
   grid, and that the new primitives put on screen. Each is fixed once, for all
   nineteen, rather than fourteen more times.
   ========================================================================== */

/* ── A · The flattened slot panel ────────────────────────────────────────
   .so-bw-slot-panel-head and .so-bw-slot-scroll are built to BLEED to the
   panel's edges: the head carries `margin: -14px -16px 0` against the panel's
   own `clamp(12px, calc(var(--so-bw-cw, 100vw) * 0.013999999999999999), 16px)` inline padding, and the scroll carries
   `margin-inline: -6px` against its scrollbar gutter. Correct while the panel
   has that padding.

   Six of the fourteen flatten the panel to `padding: 0` — the hairline and
   sheet templates, for which a padded, bordered box around the times is the
   opposite of the point. The negative margins then bleed OUTSIDE the panel:
   measured on Dusk at 1280px, the head rendered 802px inside a 770px panel and
   the audit reported it as `inner-overflow-x`, which is exactly what it was.

   The three pilots that flatten the panel (Conversation, Editorial, Week
   Board) already carry this correction. This adds the rest rather than leaving
   a list that is right for three templates and wrong for six.

   Tabbed Rail is here too: its docked strip is `padding: 10px 0 12px`, which
   is zero on the inline axis for the same reason. */
#so-booking-widget[data-template="dusk"] .so-bw-slot-panel-head,
#so-booking-widget[data-template="deck"] .so-bw-slot-panel-head,
#so-booking-widget[data-template="tabbed"] .so-bw-slot-panel-head,
#so-booking-widget[data-template="timeline"] .so-bw-slot-panel-head,
#so-booking-widget[data-template="daypart"] .so-bw-slot-panel-head,
#so-booking-widget[data-template="dusk"] .so-bw-slot-scroll,
#so-booking-widget[data-template="deck"] .so-bw-slot-scroll,
#so-booking-widget[data-template="tabbed"] .so-bw-slot-scroll,
#so-booking-widget[data-template="timeline"] .so-bw-slot-scroll,
#so-booking-widget[data-template="daypart"] .so-bw-slot-scroll {
    margin-inline: 0;
    padding-inline: 0;
}
/* The head is `position: sticky` so it can hold the timezone control above a
   scrolling list. On a flattened panel there is no box for it to stick to and
   it simply floats over the times. */
#so-booking-widget[data-template="dusk"] .so-bw-slot-panel-head,
#so-booking-widget[data-template="deck"] .so-bw-slot-panel-head,
#so-booking-widget[data-template="timeline"] .so-bw-slot-panel-head,
#so-booking-widget[data-template="daypart"] .so-bw-slot-panel-head {
    position: static;
}

/* ── B · Target floors, for all nineteen ─────────────────────────────────
   Five secondary controls have always been under the floor:

     .so-bw-card-more        62 x 20   "Read more"
     .so-bw-card-profile     85 x 20   "View profile"
     .so-bw-slot-back       770 x 20   "Change date" / "Back to days"
     .so-bw-calendar-nav     28 x 28   the month arrows
     .so-bw-tz-combo-trigger  * x 29   the display-timezone control

   They were invisible for two reasons. The month arrows and the timezone
   control only render on the date step, which most audited templates replaced
   with a board or a list; and `.so-bw-card-more` sat inside a card grid whose
   `grid-auto-rows: 1fr` stretched every card to the tallest, so the button was
   incidentally taller than its own content. Every one-column primitive in this
   section opts out of that stretching — correctly, a one-column list levels
   nothing — and the real height of these controls appeared.

   So this is a pre-existing accessibility fault surfaced by the redesign, not
   one it introduced. Fixed at the shared level: the five pilots each carried a
   per-template `min-height` for the timezone trigger, and adding fourteen more
   names to that list would be the wrong answer to the question it asks.

   Sizes: 36px for the compact controls, which clears the design system's
   `.so-bw-calendar-day` precedent (40px) in spirit and WCAG 2.2's 24px
   minimum with room to spare, without turning a month header into a toolbar.
   The label typography is untouched — only the box grows. */
#so-booking-widget .so-bw-card-more,
#so-booking-widget .so-bw-card-profile {
    display: inline-flex;
    align-items: center;
    min-height: 32px;
    padding-block: 6px;
}
#so-booking-widget .so-bw-slot-back {
    min-height: 36px;
    padding-block: 6px;
}
#so-booking-widget .so-bw-calendar-nav {
    min-width: 36px;
    min-height: 36px;
}
#so-booking-widget .so-bw-tz-combo-trigger {
    min-height: 36px;
}
/* A custom-field checkbox is a square control, and WCAG 2.2's target-size
   minimum for one is 24 x 24 CSS px — which it was not: it rendered at the
   browser default of 18. The whole row is a <label>, so the label text is a
   second, larger target for a pointer; the input still has to be reachable on
   its own for anyone tapping the box itself. */
#so-booking-widget .so-bw-checkbox-input {
    width: 24px;
    height: 24px;
    flex-shrink: 0;
    accent-color: var(--so-bw-primary);
}
#so-booking-widget .so-bw-checkbox-label {
    display: flex;
    align-items: center;
    gap: 10px;
    min-height: var(--so-bw-ds-tap);
    cursor: pointer;
}

/* ── C · Dusk's input underline, measured rather than guessed ────────────
   F9 replaces the field box with a single rule, which makes that rule the
   control's entire boundary — and WCAG 2.2 wants 3:1 for the boundary of a
   control against its canvas.

   The first attempt used `--so-bw-ds-n300`, which on this template is #4a4f5e.
   Against the #14161d canvas that measures 2.21:1, and the audit said so. This
   is the value that clears it: #5f6577 measures 3.11:1. It is stated as a
   literal with the number beside it precisely because it is a threshold — a
   ramp token that gets retuned later would move it silently, and the whole
   point of F9 is that this one line is the field.

   Only the RESTING state needs the floor; focus is already accent at 2px. */
#so-booking-widget[data-template="dusk"] .so-bw-input,
#so-booking-widget[data-template="dusk"] .so-bw-textarea,
#so-booking-widget[data-template="dusk"] .so-bw-select {
    border-bottom-color: #5f6577;   /* 3.11:1 on #14161d */
}


/* ==========================================================================
   17.16 · SERVICE IMAGERY, ON ALL NINETEEN
   --------------------------------------------------------------------------
   Every template shows the service's picture. Two treatments, chosen by what
   the choice step already is:

     COVER   a full-width band at the head of the card, for the templates
             whose choice step is a multi-column GRID of cards. Eight of them.
     AVATAR  a circular thumbnail beside the service name, for the templates
             whose choice step is a one-column LIST. Eleven of them.

   THE FALLBACK ALREADY EXISTS AND IS NOT NEW WORK. booking-widget.js renders
   .so-bw-card-media for every service either way: an <img> when the service
   has imagery, and a coloured tile of the service's INITIALS
   (serviceIconHtml → serviceInitials) when it does not, marked
   .so-bw-card-media--placeholder. That is why a catalogue with photos on some
   services and not others does not render as two different card designs — the
   band is always there and always the same size. Both treatments below inherit
   that, so "no image" is a designed state rather than a hole.

   WHY THIS SECTION EXISTS AT ALL: thirteen templates were hiding the band
   outright. That was defensible per template — a 16:10 photograph is 420px of
   image before the first price in a 760px dialog, and a lit rectangle is noise
   on a dark canvas — but the conclusion drawn from it was wrong. The problem
   was never the picture; it was the SHAPE of the picture. A cover band suits a
   grid and nothing else. Turned on its side into a 40px circle it costs one
   row of height, anchors the left edge of a list, and tells the customer what
   they are booking.

   -- The two mechanisms, and why there are two ----------------------------

   A service card is a COLUMN by force: `.so-bw-card--service` is
   `flex-direction: column !important` at (1,2,0), protecting the card's two
   rows (body above, extras strip below) from themed templates that flatten
   everything to a row. Two templates have already fought that and won —
   Wide Canvas and Conversation both re-declare it as a row with !important at
   higher specificity.

   So: MECHANISM A re-lays the card as a GRID with named areas, which expresses
   "media beside body, extras full width beneath" without touching the
   direction at all. MECHANISM B is for the two cards that are already rows,
   where the media is simply a flex item pulled to the front with `order`.

   Neither reorders the DOM, so the accessible name is still title-then-price
   and the media stays aria-hidden decoration with the service name in text
   directly beside it.
   ========================================================================== */

/* ── The avatar primitive ────────────────────────────────────────────────
   Size is a token so each template can pick one that suits its density
   without restating the geometry. The inner tile pins all six of
   width/height/min-/max- to --so-bw-icon-size (see the .so-bw-card-icon
   primitive), so the token has to drive THAT rather than a bare width — a
   height alone gets clamped straight back to 48px. */
#so-booking-widget .so-bw-card-media {
    --so-bw-svc-avatar: 44px;
    --so-bw-svc-avatar-inset: 14px;
}
#so-booking-widget[data-template="centred"] .so-bw-card-media,
#so-booking-widget[data-template="agenda"] .so-bw-card-media,
#so-booking-widget[data-template="timeline"] .so-bw-card-media,
#so-booking-widget[data-template="dusk"] .so-bw-card-media,
#so-booking-widget[data-template="soft"] .so-bw-card-media,
#so-booking-widget[data-template="editorial"] .so-bw-card-media,
#so-booking-widget[data-template="wide"] .so-bw-card-media{
    /* Beats the thirteen `display: none` rules this section supersedes: same
       (1,2,0), later in source. Flex + centring so the <img> and the initials
       tile both sit in the middle of the circle. */
    display: flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
    width: var(--so-bw-svc-avatar);
    height: var(--so-bw-svc-avatar);
    /* The cover band is 16:10 and its aspect-ratio would fight a square. */
    aspect-ratio: 1;
    border-radius: 999px;
    overflow: hidden;
}
#so-booking-widget[data-template="centred"] .so-bw-card-media-avatar,
#so-booking-widget[data-template="agenda"] .so-bw-card-media-avatar,
#so-booking-widget[data-template="timeline"] .so-bw-card-media-avatar,
#so-booking-widget[data-template="dusk"] .so-bw-card-media-avatar,
#so-booking-widget[data-template="soft"] .so-bw-card-media-avatar,
#so-booking-widget[data-template="editorial"] .so-bw-card-media-avatar,
#so-booking-widget[data-template="wide"] .so-bw-card-media-avatar{
    --so-bw-icon-size: var(--so-bw-svc-avatar);
    /* Initials scale WITH the circle. A 25px glyph in a 24px chip avatar is
       not a fallback, it is an overflow — and the chip is the smallest of the
       eleven, so a single literal could never have served all of them. */
    --so-bw-icon-svg-size: calc(var(--so-bw-svc-avatar) * 0.46);
    --so-bw-icon-text-size: calc(var(--so-bw-svc-avatar) * 0.38);
    box-shadow: none;
}
/* The carousel is a cover-band control: arrows and dots inside a 40px circle
   are unusable and would cover the picture they page through. A service with
   several images shows its primary one here; the rest stay reachable through
   the description dialog, which is where the gallery belongs on a list. */
#so-booking-widget[data-template="centred"] .so-bw-card-media-nav,
#so-booking-widget[data-template="agenda"] .so-bw-card-media-nav,
#so-booking-widget[data-template="timeline"] .so-bw-card-media-nav,
#so-booking-widget[data-template="dusk"] .so-bw-card-media-nav,
#so-booking-widget[data-template="soft"] .so-bw-card-media-nav,
#so-booking-widget[data-template="editorial"] .so-bw-card-media-nav,
#so-booking-widget[data-template="wide"] .so-bw-card-media-nav,
#so-booking-widget[data-template="centred"] .so-bw-card-media-dots,
#so-booking-widget[data-template="agenda"] .so-bw-card-media-dots,
#so-booking-widget[data-template="timeline"] .so-bw-card-media-dots,
#so-booking-widget[data-template="dusk"] .so-bw-card-media-dots,
#so-booking-widget[data-template="soft"] .so-bw-card-media-dots,
#so-booking-widget[data-template="editorial"] .so-bw-card-media-dots,
#so-booking-widget[data-template="wide"] .so-bw-card-media-dots{
    display: none;
}

/* ── MECHANISM A · the avatar row ────────────────────────────────────────
   Grid with named areas. Says "media beside body, extras full width beneath"
   without arguing with `flex-direction: column !important` — the direction
   simply stops applying once the card is a grid.

   Split Canvas already carried this shape for its 88px media row (17.2) and
   keeps its own column width; everything else takes the token. */
#so-booking-widget[data-template="centred"] .so-bw-card--service:not(.so-bw-sp__panel),
#so-booking-widget[data-template="agenda"] .so-bw-card--service:not(.so-bw-sp__panel),
#so-booking-widget[data-template="timeline"] .so-bw-card--service:not(.so-bw-sp__panel),
#so-booking-widget[data-template="dusk"] .so-bw-card--service:not(.so-bw-sp__panel),
#so-booking-widget[data-template="soft"] .so-bw-card--service:not(.so-bw-sp__panel),
#so-booking-widget[data-template="editorial"] .so-bw-card--service:not(.so-bw-sp__panel) {
    display: grid !important;
    grid-template-columns: auto minmax(0, 1fr);
    grid-template-areas:
        "media body"
        "extras extras"
        "panel panel";
    align-items: start !important;
    gap: 0 !important;
}
#so-booking-widget[data-template="centred"] .so-bw-card-media,
#so-booking-widget[data-template="agenda"] .so-bw-card-media,
#so-booking-widget[data-template="timeline"] .so-bw-card-media,
#so-booking-widget[data-template="dusk"] .so-bw-card-media,
#so-booking-widget[data-template="soft"] .so-bw-card-media,
#so-booking-widget[data-template="editorial"] .so-bw-card-media {
    grid-area: media;
    /* The card's own padding is forced to 0 by the (1,2,0) service-card rule,
       so the inset is the avatar's to carry. The gap to the text comes from
       .so-bw-card-top's left padding, which is already there. */
    margin: var(--so-bw-svc-avatar-inset) 0 0 var(--so-bw-svc-avatar-inset);
}
#so-booking-widget[data-template="centred"] .so-bw-card-top,
#so-booking-widget[data-template="agenda"] .so-bw-card-top,
#so-booking-widget[data-template="timeline"] .so-bw-card-top,
#so-booking-widget[data-template="dusk"] .so-bw-card-top,
#so-booking-widget[data-template="soft"] .so-bw-card-top,
#so-booking-widget[data-template="editorial"] .so-bw-card-top {
    grid-area: body;
}
#so-booking-widget[data-template="centred"] .so-bw-card-extras-toggle,
#so-booking-widget[data-template="agenda"] .so-bw-card-extras-toggle,
#so-booking-widget[data-template="timeline"] .so-bw-card-extras-toggle,
#so-booking-widget[data-template="dusk"] .so-bw-card-extras-toggle,
#so-booking-widget[data-template="soft"] .so-bw-card-extras-toggle,
#so-booking-widget[data-template="editorial"] .so-bw-card-extras-toggle {
    grid-area: extras;
}
#so-booking-widget[data-template="centred"] .so-bw-card-extras,
#so-booking-widget[data-template="agenda"] .so-bw-card-extras,
#so-booking-widget[data-template="timeline"] .so-bw-card-extras,
#so-booking-widget[data-template="dusk"] .so-bw-card-extras,
#so-booking-widget[data-template="soft"] .so-bw-card-extras,
#so-booking-widget[data-template="editorial"] .so-bw-card-extras {
    grid-area: panel;
}

/* ── MECHANISM B · the inline avatar ─────────────────────────────────────
   Wide Canvas and Conversation already re-declare the service card as a ROW
   with !important at higher specificity, so the media only needs to become a
   flex item and move to the front. */
#so-booking-widget[data-template="wide"] .so-bw-card-media{
    order: -1;
    align-self: center;
}

/* ── Per-template sizes ──────────────────────────────────────────────────
   Chosen against each template's density rather than standardised: the same
   circle that anchors a 56px Coral Pop pillow is a lump in a compact row.
   Editorial's is deliberately the quietest of them — type does the work on
   that template, and the avatar is there to identify, not to decorate. */
#so-booking-widget[data-template="centred"] .so-bw-card-media  { --so-bw-svc-avatar: 44px; }
/* 16px, and the number is taken from the row rather than chosen: it is the
   `padding-left` that `.so-bw-card-top` already puts in front of the TEXT, so
   the avatar and the name are held off the card's edge by the same amount and
   the staff list — whose avatar is inside that padding — starts at the same x.

   It was 0 for one release and the reason expired. Agenda's row was then
   `border: 0; background: transparent`, a bare line in a hairline-separated
   list, and an inset only pushed the row's first element 14px right of the
   separator while everything above it sat flush. Section 17.6 C11 now draws
   the row as a bordered white card, so there IS an edge to be held off — and
   at 0 the avatar sat directly ON that border, 1px inside the card, touching
   the 3px accent bar of any selected row. Reported as "the card and the border
   are not fixed properly", and that is exactly what it looked like. */
#so-booking-widget[data-template="agenda"] .so-bw-card-media   { --so-bw-svc-avatar: 40px; --so-bw-svc-avatar-inset: 16px; }
#so-booking-widget[data-template="timeline"] .so-bw-card-media { --so-bw-svc-avatar: 36px; }
#so-booking-widget[data-template="dusk"] .so-bw-card-media     { --so-bw-svc-avatar: 40px; }
#so-booking-widget[data-template="soft"] .so-bw-card-media     { --so-bw-svc-avatar: 52px; --so-bw-svc-avatar-inset: 16px; }
#so-booking-widget[data-template="editorial"] .so-bw-card-media{ --so-bw-svc-avatar: 40px; --so-bw-svc-avatar-inset: 2px; }
#so-booking-widget[data-template="wide"] .so-bw-card-media     { --so-bw-svc-avatar: 36px; }

/* Editorial keeps its own card padding (`16px 2px !important`), so the avatar
   aligns to the text column rather than carrying a second inset of its own —
   and the hairline index keeps its left edge. */
#so-booking-widget[data-template="editorial"] .so-bw-card--service:not(.so-bw-sp__panel) {
    align-items: center !important;
}
#so-booking-widget[data-template="editorial"] .so-bw-card-media {
    margin-top: 0;
}
/* 17.2 reserved the media column with a :not(:has(...)) fallback for a
   catalogue where only some services had a photo. The band is emitted for
   every service now — a picture or its initials — so the fallback can never
   match, and leaving it would be a rule that quietly means nothing. */

/* ── COVER · the eight grid templates ────────────────────────────────────
   Nothing to add for Hero, Week Board, Frost and Dual Month: the band is the
   base component and they never hid it. The three that DID are restored here,
   each to the radius its cards use, so the picture caps the tile instead of
   sitting in a square corner inside a rounded one. (Accordion was a fourth,
   until it was withdrawn from the catalogue.) */
#so-booking-widget[data-template="tabbed"] .so-bw-card-media,
#so-booking-widget[data-template="deck"] .so-bw-card-media,
#so-booking-widget[data-template="daypart"] .so-bw-card-media {
    display: block;
}
/* ...BUT NOT WHEN THE BAND IS HOLDING THE INITIALS MARK.
   `display: block` above exists to undo a `display: none`, and it does that.
   It also outranks `.so-bw-card-media--placeholder` (1,1,0), which is the rule
   that CENTRES the fallback mark in the band — so on these four the mark went
   to the top-left corner of an otherwise empty cover, half over the card's
   rounded edge. It read as a broken image, not a placeholder.

   Measured across all nineteen: Hero, Week Board, Frost and Dual Month centre
   their mark (inset 87/46/87/46 and the like) because they never touched the
   display; Tabbed Rail, Card Deck and Daypart reported 0/0/278/93, 0/0/196/64
   and 0/0/211/64 — hard against the top-left in every case. One component,
   two behaviours, decided by a declaration that was only ever meant to mean
   "visible".

   So the band stays `block` when it holds a photograph and goes back to being
   a centring flex box when it holds the mark. The mark sizes deliberately stay
   as they are: 46px on the two with a 21:9 band and 68px where the band is
   deeper, which is proportion, not drift. */
#so-booking-widget[data-template="deck"] .so-bw-card-media--placeholder,
#so-booking-widget[data-template="daypart"] .so-bw-card-media--placeholder {
    display: flex;
    align-items: center;
    justify-content: center;
}
/* Tabbed Rail is off that list, and it is the one case where the band's whole
   reason for existing does not apply. The placeholder band exists to give a
   photoless service the same height as a photographed one, with the mark
   centred in it. On this template the mark is no longer IN the band — it is a
   rounded tile under it, so the card reads cover / mark / name (see section
   17.4 C8b, and the lead tile in booking-widget.js). With the mark moved out,
   a photoless service's band is 135px of flat tint above a card that is
   already complete without it. So there is no band. The content below starts
   identically either way — same tile, same left edge, same order — and the
   pills sitting on the card's floor keep a row aligned across the difference,
   which is what the equal-height band was for. */
#so-booking-widget[data-template="tabbed"] .so-bw-card-media--placeholder {
    display: none;
}
/* …and the same goes for a band that DOES hold a photograph, which is the
   other half of the same decision rather than a second one.

   Hiding only the placeholder left the two cases disagreeing about the card's
   shape: a photographed service got a 76px band, the eleven without one got
   none, so one card in the catalogue was 76px taller than the rest and — with
   the grid levelling rows — dragged its whole row up with it. MEASURED on the
   live install at 1440: eleven cards at 175px, one at 251px.

   Reserving the band on all twelve instead would also have made them agree,
   at 251px each: 76px of flat service colour on eleven cards, above a rounded
   tile in the same colour, to give one card its photograph. This template's
   card leads with that tile by design (section 17.4 C8b) — the mark is no
   longer IN the band — so the band is decoration competing with the thing it
   used to contain, and 76px is a third of the card.

   The cost is real and worth naming: on this template an uploaded service
   photo does not appear on the browse card. It still reaches the customer
   through the service's own colour on the lead tile, and the other templates
   that show a media band (Deck, Daypart and the base 16:10) are
   untouched — a catalogue built around photography should be on one of those.
   Scoped to the service card so the staff and location cards keep their own. */
#so-booking-widget[data-template="tabbed"] .so-bw-card--service .so-bw-card-media {
    display: none;
}
#so-booking-widget[data-template="deck"] .so-bw-card-media {
    border-radius: var(--so-bw-r-13) var(--so-bw-r-13) 0 0;
}
/* The panel and the tile are short on these two, so the band takes a
   shallower crop than the base 16:10 — a half-height photograph on a 2-up
   tile pushes the next row off the screen. */
#so-booking-widget[data-template="tabbed"] .so-bw-card-media,
#so-booking-widget[data-template="daypart"] .so-bw-card-media {
    aspect-ratio: 21 / 9;
}
/* ...and Tabbed Rail takes its own crop. It shares the "short panel" reason
   with the other two but not the WIDTH: its pane is two thirds of the shell
   because the rail has the other third, so a tile is ~300px.

   24:9 puts that at ~113px. It is the one dimension on this card with a floor
   underneath it rather than slack: the band has to stay deep enough to be a
   photograph OF something, since telling two services apart at a glance is the
   only reason to carry a picture at all. 20:9 (135px) was comfortable and 28:9
   (~97px) is a decorative stripe. This is the last stop before that.

   So it is also the dial. If the card needs to be shorter still, this number
   is where the height is — everything else on the card has already given up
   what it can (section 17.4 C8b). */
#so-booking-widget[data-template="tabbed"] .so-bw-card-media {
    aspect-ratio: 24 / 9;
}
/* CARD DECK joins them, at the same 21:9. Deck was still on the shared 2:1 and
   it is the layout that could least afford it: its tile carries a photo, a
   name and two badges and NOTHING else — the description is hidden here by
   design ("a card in a stack states its options; it does not explain them") —
   so the picture was the tile. Measured at a 1000px embed: a 225px card made
   of a 130px band and a 94px text block, i.e. 58% of a service card spent on
   the photograph, and a six-service catalogue running 883px down the page.

   21:9 takes the band to 111px on the same 259px tile. It is the crop this
   stylesheet already uses wherever a tile is short, so Deck now reads in the
   same register as Tabbed Rail and Daypart rather than carrying a crop of its
   own; object-fit: cover means it crops tighter, never squashes; and the photo
   is still the first and largest thing in the card, which is the template's
   whole proposition.

   AND A DEPTH, not just a ratio — this is the half that was missing, and it is
   why the tile still measured tall on a real embed. A ratio makes the band a
   function of the TILE WIDTH, and Deck's tile widens with the embed because
   the grid keeps its column count and stretches the columns instead. So the
   header was never a fixed thing: measured across the four embed sizes at the
   same viewport, the band ran 111px / 143px / 175px / 176px and the card
   201px / 233px / 265px / 266px. The 21:9 fix landed on the 600px container I
   measured and did almost nothing at `large` and `full`, where the band simply
   grew until it hit the shared 176px ceiling — a ceiling that also silently
   overrides the ratio, so the crop drifts to 2.8:1 on the widest tile.

   Because the header floats, so does everything under it: the title and the
   badge row sit at a different distance from the top of the card at every
   embed size, which is the "header and footer are not fixed in the card"
   report.

   The depth is held steady at the GRID now, not here. A hard 112px cap was
   tried in this slot and it fixed the height by breaking the picture: on the
   411px tile the full-size embed used to produce, 112px is a 3.67:1 slot, and
   `object-fit: cover` answers a ratio that extreme by throwing away 75-82% of
   the photograph's height and centre-cropping through the subject. Bounding
   the TILE (see Deck's card-grid, C12) keeps the tile between 240px and 290px
   at every embed size, so 21:9 lands between 103px and 124px on its own and
   the crop stays one a photograph survives.

   The cap stays as a BACKSTOP. Across the grid's whole range it is idle — the
   ratio lands the band at 84px, 93px, 102px and 111px on its own — with one
   exception: the phone, where a single column is the widest tile of the lot at
   ~298px and 21:9 gives it 128px. That is the same "deeper band on the smaller
   screen" inversion this template keeps producing, so the number is set to
   Deck's own widest honest depth instead of to a round figure. The crop opens
   to 2.6:1 there rather than 2.33:1, which a photograph still survives. */
#so-booking-widget[data-template="deck"] .so-bw-card-media {
    aspect-ratio: 21 / 9;
    max-height: 116px;
}
/* The rest of the tile's slack, now that the band is not hiding it. 14px of
   padding around a two-line name and a badge row, plus a 6px gap above the
   badges, is generous for a primitive whose brief is "big targets" — the
   target is the whole tile, not its inset. 12px and 4px keep the tile
   comfortably tappable and give the grid back another 6px per card.
   min-height stays: it is a floor for a one-line name, not a spender — the
   text block already measures 94px against it. */
#so-booking-widget[data-template="deck"] .so-bw-card--service .so-bw-card-top {
    padding: 12px;
}
#so-booking-widget[data-template="deck"] .so-bw-card-badges {
    padding-top: 4px;
}
#so-booking-widget[data-template="tabbed"] .so-bw-card-media-avatar,
#so-booking-widget[data-template="daypart"] .so-bw-card-media-avatar {
    --so-bw-icon-size: 46px;
    --so-bw-icon-svg-size: 22px;
    --so-bw-icon-text-size: 17px;
}
/* Nothing for Tabbed Rail here on purpose: its band never holds the mark now
   (the rule above hides the placeholder band, and a band WITH a photograph has
   no avatar in it), so a size for `.so-bw-card-media-avatar` would be sizing
   an element this template does not render. The tile it does render is
   `.so-bw-card-lead`, sized in section 17.4 C8b. */

/* ── Responsive ──────────────────────────────────────────────────────────
   The avatars are already fluid — they are a fixed square beside a
   `minmax(0, 1fr)` text column, so the text takes every pixel the viewport
   gives and the circle never changes. Two tiers still need saying.

   At the phone tier the largest two come down: 72px of Split Canvas avatar
   plus its inset leaves 230px for a service name on a 360px screen, which
   wraps a real one to five lines. */
/* was @media (max-width: 480px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */
    #so-booking-widget:where([data-cw~="480"])[data-template="soft"] .so-bw-card-media{ --so-bw-svc-avatar: 44px; }
    #so-booking-widget:where([data-cw~="480"])[data-template="centred"] .so-bw-card-media{ --so-bw-svc-avatar: 40px; }

/* Agenda stacks its duration column above the name below 480 (17.6), which
   puts the avatar beside a two-row block rather than a one-row one. Centred
   on that block rather than pinned to its top. */
/* was @media (max-width: 480px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    #so-booking-widget:where([data-cw~="480"])[data-template="agenda"] .so-bw-card--service:not(.so-bw-sp__panel){
        align-items: center !important;
    }
    #so-booking-widget:where([data-cw~="480"])[data-template="agenda"] .so-bw-card-media{
        /* align-self, not the margin alone. 17.20 pins the avatar to the top
           of the row, and align-self on the CHILD beats align-items on the
           parent — so this tier's centring was being overridden and the circle
           sat 21px above the name, aligned to the duration instead. */
        align-self: center;
        margin-top: 0;
    }

/* Inline is the one column narrow enough that the avatar has to justify its
   width at every breakpoint. It keeps it — 30px is one line of the mono type
   it is set in — but the leader that used to run between the name and the
   price is gone (see 17.17), so the row is avatar, name, price and nothing
   competing for the space between them. */

/* ── Forced colours ──────────────────────────────────────────────────────
   The initials tile paints white on a solid brand fill via inline style. In a
   forced-colours palette that fill is replaced and the white text can land on
   white. The system pair is restored explicitly. */
@media (forced-colors: active) {
    #so-booking-widget .so-bw-card-media-avatar {
        forced-color-adjust: none;
        background: ButtonFace !important;
        color: ButtonText !important;
        border: 1px solid ButtonText;
    }
    #so-booking-widget .so-bw-card-media-avatar span {
        color: ButtonText !important;
    }
}


/* ==========================================================================
   17.17 · DEFECTS THE FULL SWEEP FOUND
   --------------------------------------------------------------------------
   Every entry here was reported by tests/visual/pilot-layout-audit.mjs across
   19 templates x 8 devices x 10 steps, with the measurement that found it.
   ========================================================================== */

/* ── Tabbed Rail · the docked strip overflowed its own scroller ─────────
   REPORTED: inner-overflow-x on div.so-bw-slot-group and div.so-bw-slot-scroll
   at every width from 1024 up (695 > 669, 647 > 573).

   The strip is `.so-bw-slot-group` as a flex row: period label, then
   `.so-bw-slot-list` as a horizontal scroller. `min-width: 0` on the list was
   correct and not enough — the group sits inside `.so-bw-slot-scroll`, which
   is `display: flex; flex-direction: column`, and a flex item's automatic
   minimum size applies on the CROSS axis too. So the group refused to shrink
   below its content, and the content is a row of slots that is deliberately
   wider than the container.

   Both boxes need the release: the item (the group) and the container it
   overflows out of (the scroller). */
#so-booking-widget[data-template="tabbed"] .so-bw-slot-scroll,
#so-booking-widget[data-template="tabbed"] .so-bw-slot-group {
    min-width: 0;
    max-width: 100%;
}
#so-booking-widget[data-template="tabbed"] .so-bw-slot-list {
    /* Belt and braces on the one that actually scrolls: `flex: 1 1 auto` with
       a zero BASIS, so the track is sized by the space left over rather than
       by the slots inside it. */
    flex: 1 1 0;
}
/* AND THE DIRECTION, which is the fault the two rules above only half fixed.
   `.so-bw-slot-group` is a COLUMN flex in the base component — period label
   above its times, which is what every other template wants. 17.4 set
   `display: flex; align-items: center` on it and stopped there, so the strip
   inherited `flex-direction: column`: `align-items: center` then centred the
   period label horizontally instead of putting it beside the times, and
   `flex: 1 1 0` on the list sized its HEIGHT rather than its width, leaving
   the list 4px tall, 720px wide and hanging 25px off the left edge of a 669px
   scroller.

   Measured, not reasoned about — the probe printed the label at x=445 in a
   box starting at x=145, which is not a thing a row can do. A layout that
   REPLACES a primitive's direction has to say so; inheriting half of one is
   the same mistake section 16 records against Conversation's chip. */
#so-booking-widget[data-template="tabbed"] .so-bw-slot-group {
    flex-direction: row;
    flex-wrap: nowrap;
}

/* ── Dialog · the reserve was measured against the wrong nav ────────────
   REPORTED: structure — "scroll-padding-bottom 77px does not clear the 139px
   commit bar", phone-small, on both form steps.

   The calc assumed a one-row commit bar: 44px control plus 33px of padding
   and border. At 360px the bar wraps — Back above, Continue below — and is
   139px tall. A field focused under it is WCAG 2.2 2.4.11 Focus Not Obscured,
   which is a failure, not a nicety.

   scroll-padding only affects where scroll-into-view stops; it consumes no
   layout, so over-reserving costs nothing but under-reserving costs the
   field. Sized for the wrapped bar at the tier where it wraps. */
/* was @media (max-width: 640px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */


/* ── Agenda · two sticky regions, one short viewport ────────────────────
   REPORTED: sticky-overlap on phone-small — button.so-bw-slot-back over
   div.so-bw-nav, and div.so-bw-slot-panel-head over div.so-bw-nav.

   "Back to days" sticks to the top of the scrolling body so a long day can be
   left without scrolling back up. On a 640px-tall phone the body is short
   enough that the sticky top and the pinned commit bar meet, and the head —
   which carries the timezone control — lands on top of the nav.

   The sticky earns its place on a laptop and loses it on a phone, where the
   list is one screen anyway. Released below the wide-phone tier; the commit
   bar is the thing that has to stay pinned there. */
/* was @media (max-width: 640px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    #so-booking-widget:where([data-cw~="640"])[data-template="agenda"] .so-bw-slot-back,
    #so-booking-widget:where([data-cw~="640"])[data-template="agenda"] .so-bw-slot-panel-head{
        position: static;
    }



/* ==========================================================================
   17.18 · SECOND SWEEP  —  what 1,520 checkpoints found
   ========================================================================== */

/* ── Wide Canvas already had a thumbnail, and 17.16 shrank the wrong box ──
   REPORTED: inner-overflow-x on div.so-bw-card-media--placeholder, 48 times,
   at every width from 360 up — 40 > 36.

   C9 shipped with a 44px identity thumbnail in the table's first column, and
   it sizes the tile INSIDE it with
   `[data-template="wide"] .so-bw-card-media .so-bw-card-icon` — (1,3,0). The
   avatar element carries both .so-bw-card-media-avatar and .so-bw-card-icon,
   so 17.16's (1,2,0) token rule lost to it: the outer circle came down to
   36px while the tile inside stayed 44px and spilled out of it.

   The template was right and the new token was wrong — 44px is the size that
   column was designed around. Matching it rather than fighting it, and the
   circle from 17.16 still applies, so Wide Canvas gets the same round avatar
   as the other ten list templates at the size it already used. */
#so-booking-widget[data-template="wide"] .so-bw-card-media {
    --so-bw-svc-avatar: 44px;
}

/* ── Daypart's three panels were never side by side ──────────────────────
   REPORTED: structure — "dayparts are stacked, not side by side" at
   tablet-landscape, and "triptych expected 3 columns" at laptop.

   Section 14 makes .so-bw-slot-panel a three-track grid and the groups its
   items. That was true when it was written. .so-bw-slot-scroll has since been
   inserted between them — a `display: flex; flex-direction: column` wrapper
   that gives the slot list its own scroll region — so the panel's grid has
   exactly one item (the wrapper) and the three parts of the day have been
   stacking ever since, on the one template whose entire proposition is that
   they do not.

   Nothing in the geometric invariants could see this: a stacked column of
   panels overflows nothing, clips nothing and has no ragged rows. It took an
   assertion about what the design CLAIMS to be true. That is the argument for
   the structural checks in one defect.

   The grid moves onto the wrapper. The panel keeps its own so the layout is
   still correct if the wrapper is ever removed again. */
#so-booking-widget[data-template="daypart"] .so-bw-slot-scroll {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 190px), 1fr));
    gap: 12px;
    align-items: start;
    /* The wrapper's own max-height + overflow-y belong to a single scrolling
       column of times. Three self-contained panels each cap and scroll
       themselves (17.9), so the outer cap would be a second scrollbar around
       the first. */
    max-height: none !important;
    overflow-y: visible;
}
#so-booking-widget[data-template="daypart"] .so-bw-slot-scroll > *:not(.so-bw-slot-group) {
    grid-column: 1 / -1;
}
/* was @media (max-width: 640px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    #so-booking-widget:where([data-cw~="640"])[data-template="daypart"] .so-bw-slot-scroll{
        grid-template-columns: 1fr;
    }


/* ── Daypart · the triptych floor was set for TIMES, not names ───────────
   REPORTED: inner-overflow-x on div.so-bw-card-body, 53 > 49, at every width
   from tablet-landscape up.

   C14 borrowed the slot panel's track floor — `minmax(min(100%, 190px), 1fr)`
   — on the reasoning that the two steps should break at the same widths. The
   reasoning was right and the number was not: 190px is sized for a column of
   times, and a STAFF card spends most of a track on furniture before the name
   gets any. Measured at 1280px: a 205px track, a 68px avatar, a 28px check and
   the paddings between them left the body 49px, so "Maya
   Krishnamurthy-Whitfield" was rendering roughly one syllable per line and its
   longest unbreakable fragment overflowed by 4px.

   The floor goes to 240px, which still gives three tracks at this template's
   860px container and leaves the body ~120px. The slot panel keeps 190px: a
   time is eight characters and does not want the extra.

   NB the `min-width: 0` is not the fix and was never the fault — the body was
   already 49px before it. It stays because a flex column holding
   admin-authored text should always be allowed to wrap. */
#so-booking-widget[data-template="daypart"] .so-bw-card-grid {
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr));
}
#so-booking-widget[data-template="daypart"] .so-bw-card-body,
#so-booking-widget[data-template="daypart"] .so-bw-card-top {
    min-width: 0;
}

/* ── The slot-panel head stops being sticky ──────────────────────────────
   REPORTED: sticky-overlap against div.so-bw-nav on Centred, Split Canvas and
   Accordion, at four widths.

   The head is `position: sticky` so the timezone control stays reachable
   while a long slot list scrolls. Both it and the commit bar stick inside
   .so-bw-body, so on a short viewport they meet — and the commit bar is the
   one that must win, because it is how the customer moves forward.

   Every pilot had already turned this off for itself (chat, editorial, hero,
   weekboard, wide), and 17.15 turned it off for eight more. Three templates
   were left carrying it, and all three reported the overlap. A property that
   nineteen out of nineteen templates end up disabling is not a default worth
   keeping: made static for the whole catalogue, in one place, rather than
   maintained as a list of exceptions that is always one template behind.

   The head still leads the panel; it simply scrolls with it. The timezone
   control is a set-once affordance, not something a customer needs pinned
   while scanning a day. */
#so-booking-widget .so-bw-slot-panel-head {
    position: static;
}

/* ── Split Canvas · the search bar met the commit bar — RETIRED ──────────
   REPORTED, once: sticky-overlap, div.so-bw-search / div.so-bw-nav, 516x4.

   Four pixels, and real at the time: the pane was the shortest scroll region
   in the catalogue (the rail fixes the container's height), so the sticky
   search bar and the pinned footer touched on a catalogue barely longer than
   the pane. Two rules used to sit here — one container-query-gated, one
   flat — releasing `.so-bw-search` to `position: static` on Split.

   Both are gone, for two reasons, and NEITHER should be restored here:

     1. They had not worked for a long time. Both scored (1,2,0), and section
        23 rule 3 re-declares `#so-booking-widget[data-template] .so-bw-search`
        at the same (1,2,0) some three thousand lines later, so source order
        gave it the cascade. Split's search field computed `sticky / z-index:4`
        the whole time these rules claimed otherwise — which is worse than
        either behaviour, because the file documented a fix it was not
        applying.

     2. The overlap they were written for cannot happen now. Under the bounded
        shell the commit bar is OUTSIDE the body's scrollport (section 23), so
        a field sticking to the top of that port has nothing left to meet.

   If a future template genuinely needs its search field released, the rule has
   to go BELOW section 23 or outscore it — `[data-template="split"][data-cw]`
   or similar. Putting it back here would be dead on arrival. */


/* ==========================================================================
   17.19 · THIRD SWEEP
   ========================================================================== */

/* ── Three two-column forms never became two columns ─────────────────────
   REPORTED: tap-target input.so-bw-input 11x49 (Dusk, both phones);
   inner-overflow-x div.so-bw-form 250 > 246 (Frost, phone-small).

   `.so-bw-form` is a TWELVE-column grid in the base component, and
   `.so-bw-form-group` spans all twelve. F9 (Dusk), F3 (Accordion) and F8
   (Frost) each re-declared the grid as two columns and stopped there — so
   every group still asked for `span 12` inside a 2-track grid, which creates
   ten implicit `auto` tracks and sizes them by content. Measured on Dusk at
   360px: an 11px-wide field with its label set one character per line, and
   the 44px floor reported as 11x49.

   F2 already carried the reset (`grid-column: auto`) for Week Board and Wide
   Canvas; the three new two-column forms did not inherit it because each
   declares its own grid rather than selecting F2. Stated once for all three.

   This is the same shape of mistake as Conversation's chip, which section 16
   records: a rule that REPLACES a primitive's layout has to own every
   declaration that layout depends on, and `grid-column` is one of them. */
#so-booking-widget[data-template="dusk"] .so-bw-form-group,
#so-booking-widget[data-template="frost"] .so-bw-form-group {
    grid-column: auto;
}
#so-booking-widget[data-template="dusk"] .so-bw-form-group:has(.so-bw-textarea),
#so-booking-widget[data-template="frost"] .so-bw-form-group:has(.so-bw-textarea) {
    grid-column: 1 / -1;
}

/* ── Frost · a selected card was leaving the material ────────────────────
   Hazard 3 says a per-template selected `background` loses to the global 15%
   wash, which is `!important` at (1,3,0), and that the answer is to work with
   the wash rather than repaint. That is right for eighteen templates and
   wrong for exactly one: here the sheet's translucent WHITE is the material,
   and the wash does not sit on top of it — it replaces it. A selected card
   was dropping out of the glass entirely, and its body copy was then reading
   against a 15% accent over a gradient rather than against a sheet, which is
   the contrast this template is most exposed on.

   So this is the documented exception, at (1,4,0) with !important, and it is
   deliberately the only one in section 17. Selection still reads: the accent
   ring the platform draws in ::after is untouched, and the sheet lifts from
   66% to 82% opacity, which is visible against its neighbours precisely
   because they are translucent.

   The probe that found it printed `color(srgb 0.184 0.431 0.659 / 0.15)` as
   the card's computed background — the wash, where the sheet should have
   been. */
#so-booking-widget[data-template="frost"] .so-bw-card.so-bw-card--selected:not(.so-bw-sp__panel) {
    background: rgba(255, 255, 255, 0.82) !important;
    border-color: var(--so-bw-primary);
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
    #so-booking-widget[data-template="frost"] .so-bw-card.so-bw-card--selected:not(.so-bw-sp__panel) {
        background: rgba(255, 255, 255, 0.97) !important;
    }
}

/* ── Coral Pop · the stacked review row would not let its value wrap ─────
   REPORTED: inner-overflow-x on div.so-bw-summary-row--service (282 > 239),
   and on the section and summary above it, at every width from
   tablet-portrait up.

   R13 stacks label over value so the pair reads as a sentence. The row is a
   flex COLUMN, and it was `align-items: flex-start` — which shrink-wraps every
   child to its own content width, so a long service name sized the value box
   to max-content and it ran straight out of the panel. The base
   `.so-bw-summary-label { flex: 0 1 40% }` is sized for the ROW layout and
   means nothing here, which is why the label looked fine while the value did
   not.

   `stretch` is what a stacked pair wants: full width, text left. The
   left-alignment R13 was reaching for comes from text-align, not from
   shrink-wrapping the boxes. */
#so-booking-widget[data-template="soft"] .so-bw-summary-row {
    align-items: stretch;
}
#so-booking-widget[data-template="soft"] .so-bw-summary-label,
#so-booking-widget[data-template="soft"] .so-bw-summary-value {
    flex: 0 0 auto;
    max-width: 100%;
    min-width: 0;
}
/* The total keeps its two-column shape, so it keeps baseline alignment. */
#so-booking-widget[data-template="soft"] .so-bw-summary-row--total {
    align-items: baseline;
}

/* ── Accordion · the search bar met the commit bar — RETIRED ─────────────
   Same four pixels as Split Canvas, same cause, and gone for the same two
   reasons: the release scored (1,2,0) and lost to section 23 rule 3 on source
   order (so Accordion's field has been sticky all along, whatever this said),
   and the bounded shell put the commit bar outside the scrollport where the
   field can no longer reach it. See the Split note above for the full
   reasoning and for how to write the rule if it is ever wanted again. */


/* ==========================================================================
   17.20 · POLISH PASS
   --------------------------------------------------------------------------
   Visual consistency only. No layout moves, no behaviour, no new primitives —
   every entry below is two boxes that were supposed to line up and did not,
   found by the POLISH checks in tests/visual/pilot-layout-audit.mjs and quoted
   with the measurement that found it.
   ========================================================================== */

/* ── 0 · THE ROW RHYTHM TOKEN ────────────────────────────────────────────
   REPORTED, by reading rather than by rendering: the review step and the
   confirmation step used DIFFERENT vertical paddings for the same row, on the
   same template. Daypart 9px then 11px. Frost 8px then 10px. Dual Month 10px
   then 11px. Inline 7px then 6px. A customer confirms a booking and the list
   they just read redraws at a different rhythm — five templates did that, and
   across the catalogue the same component carried 6, 7, 8, 9, 10, 11 and 12px
   for no reason anyone could state.

   One token, three density steps, read by `.so-bw-summary-row`,
   `.so-bw-confirmation-detail` and `.so-bw-itinerary-row` on all nineteen — so
   a template's two list screens can no longer disagree, and a fourth density
   is a decision someone has to make on purpose.

   THE TOKEN HAS TO BE DECLARED, which is the whole point of putting this
   block first. The conversion landed 23 `var(--so-bw-row-pad-y)` usages before
   this declaration existed, and an undeclared custom property is invalid at
   computed-value time — so `padding: var(--so-bw-row-pad-y) 0` did not fall
   back to anything, it computed to the initial value and every summary row in
   the catalogue lost its vertical padding entirely. Nothing overflowed,
   nothing clipped, and the geometric sweep stayed green through all of it.
   widget-css-cascade.test.ts now fails on any var() this file uses and does
   not define. */
#so-booking-widget .so-bw-container {
    --so-bw-row-pad-y: 10px;
}
/* Compact: a 380px column beside somebody else's article. */
/* Roomy: the soft register's 52px targets, and the serif register's measure. */
#so-booking-widget[data-template="soft"] .so-bw-container,
#so-booking-widget[data-template="editorial"] .so-bw-container {
    --so-bw-row-pad-y: 12px;
}

/* ── A · The avatar and the name it belongs to ───────────────────────────
   REPORTED: 145 findings across Centred, Dialog, Dusk, Agenda, Timeline,
   Editorial, Wide Canvas and Conversation — "neither top-aligned with the name
   nor centred on its first line", from 4px out to 59px.

   17.16 gave the avatar `margin-top: var(--so-bw-svc-avatar-inset)`, a
   free-standing 14px that had nothing to do with where the text beside it
   starts. The text starts at `.so-bw-card--service .so-bw-card-top`'s own
   padding-top, which is `calc(var(--so-bw-card-pad) - 4px)` — a TOKEN, and one
   that retunes at the phone tier. So the two agreed at exactly one card
   padding and drifted everywhere else, worst on Editorial (59px) where the
   card was additionally centring its rows.

   Reading the same expression is what makes this stay fixed: retune
   --so-bw-card-pad and the avatar follows the text automatically.

   TOP-ALIGNED, not centred, and deliberately: these avatars are 30–72px
   beside a title with up to two lines of description under it. Centring sinks
   the circle into the paragraph. The small end of the range (Conversation's
   24px chip) has no description at all, so its top and its centre are the same
   place to within a pixel. */
#so-booking-widget[data-template="centred"] .so-bw-card-media,
#so-booking-widget[data-template="agenda"] .so-bw-card-media,
#so-booking-widget[data-template="timeline"] .so-bw-card-media,
#so-booking-widget[data-template="dusk"] .so-bw-card-media,
#so-booking-widget[data-template="editorial"] .so-bw-card-media {
    align-self: start;
    margin-top: calc(var(--so-bw-card-pad) - 4px);
}
/* The three that pad their own card-top rather than taking the token's
   default. Stated next to each other so the set is visible in one place. */
#so-booking-widget[data-template="soft"] .so-bw-card-media {
    align-self: start;
    margin-top: 16px;      /* matches 17.14's .so-bw-card-top padding: 16px 18px */
}
/* Editorial pads the CARD (16px 2px !important) and its card-top then adds the
   token default on top, so the calc above already lands. What has to go is the
   row centring 17.16 gave it — that is what put the avatar 59px below the
   name it labels, the largest single misalignment in the catalogue. */
#so-booking-widget[data-template="editorial"] .so-bw-card--service:not(.so-bw-sp__panel) {
    align-items: start !important;
}

/* Mechanism B — the two cards that are already flex ROWS. `align-self: center`
   centred the avatar on the whole row, which is right for a one-line label and
   wrong the moment it wraps: at 360px Conversation's chip label takes two
   lines and Wide Canvas's service name takes three, and the circle sank 43px
   and 60px respectively. Both card-tops start at their card's padding box, so
   flex-start with no offset puts the avatar on the first line. */
/* Wide Canvas is the exception, and its own layout says so: the comparison
   row sets `align-items: center` on the card, on the card-top and on the
   card-body, because a table row aligns its cells across. An avatar pinned to
   the top of that row is the one thing on it that does not agree with the
   rest. Centred — with the media column reserved so a three-line name at
   360px cannot pull the circle out of the row. */
#so-booking-widget[data-template="wide"] .so-bw-card-media {
    align-self: center;
    margin-top: 0;
}

/* ── C · Two orderings this section had to correct in itself ─────────────

   MEDIA QUERIES ADD NO SPECIFICITY — hazard 5, and this block tripped it.
   Block A above sets `align-self: start` on the avatar at (1,2,0). Agenda's
   phone tier already centred it, at the same (1,2,0), inside a
   `@media (max-width: 480px)` — and earlier in the file. Source order decides,
   so the unconditional rule won at every width and the phone avatar sat 21px
   above the name, pinned to the duration instead. Restated here, AFTER block
   A, which is the only thing that makes the tier apply. */
/* Unwrapped onto the CONTAINER width for the same reason as the block at
   ~6336: a viewport media query never fires for a 420px floating panel on a
   desktop, so Agenda's avatar sat 21px above the name in every shell. :where()
   keeps the specificity the media query had, and the rule stays in place so it
   still lands after block A, which is what makes the tier apply at all. */
#so-booking-widget:where([data-cw~="480"])[data-template="agenda"] .so-bw-card-media {
    align-self: center;
    margin-top: 0;
}

/* WIDE CANVAS'S BADGES KEPT A JUSTIFY-SELF THEY NO LONGER WANTED.
   REPORTED: left-edge — badges 16px, 30px, 64px, 71px, 86px and 100px off the
   title, on Wide Canvas at both phone widths, the offset tracking the badge
   row's own width.

   The comparison row puts the badges in column 2 and pins them to its right
   edge with `justify-self: end`. Its phone tier collapses the body to one
   column and moves the badges to `grid-column: 1` under the name — but never
   released the `justify-self`, so the badge row stayed shrink-wrapped and
   pushed to the right of a track that is now the full width. Every service
   showed its duration and price at a different indent, decided by how long
   they happened to be.

   This is the fault the whole polish pass was looking for: nothing overflows,
   nothing clips, and the column is visibly ragged. */
/* was @media (max-width: 640px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    #so-booking-widget:where([data-cw~="640"])[data-template="wide"] .so-bw-card-badges,
    #so-booking-widget:where([data-cw~="640"])[data-template="wide"] .so-bw-card-profile{
        justify-self: start;
    }



/* ==========================================================================
   18. DATE & TIME, ONE TREATMENT PER TEMPLATE
   --------------------------------------------------------------------------
   WHAT THIS FIXES, AND HOW IT WAS FOUND

   Section 17 rebuilt the choice, form, review and confirmation steps for all
   nineteen templates. It did NOT rebuild the month grid. A fingerprint probe
   rendered the date step and the times step on all nineteen, stripped the two
   things that differ by construction (container width, accent hue) and
   clustered what was left:

     DATE   15 distinct treatments across 19.
            Centred, Card Deck, Daypart, Timeline and Dusk were BYTE-IDENTICAL
            on every measured property — shell, header, weekday row, grid, day
            chip, numeral. Five templates, one month.
            Partial sharing was worse than the clusters suggest:
              calShell    15 of 19 share one value
              calHead     15 of 19
              calMonth    13 of 19
              calDayNum   12 of 19
              calDay      11 of 19

     TIME   the panel SHELL differed, the panel INTERIOR did not.
              slotHead    IDENTICAL on all 19
              slotScroll  18 of 19 share one value
              slotTitle   17 of 19
              slotGroup   16 of 19
              slotTime    11 of 19
              slotRow     Centred, Split Canvas, Tabbed Rail, Daypart and
                          Dual Month share one row exactly

   So the step the customer spends the most time on was the step that looked
   the same everywhere. This section gives each template a date and a time
   treatment drawn from its own language.

   -------------------------------------------------------------------------
   HAZARD 6 · A PER-TEMPLATE DAY OR ROW RULE OUTRANKS THE BASE STATE RULES

   This is new, it is specific to this section, and it is the reason several
   rules below look more defensive than they need to.

     #so-booking-widget .so-bw-calendar-day--disabled          (1,1,0)
     #so-booking-widget .so-bw-calendar-day--selected          (1,1,0)
     #so-booking-widget[data-template="x"] .so-bw-calendar-day (1,2,0)  <-- wins

   A per-template rule that simply says `background: ...` on `.so-bw-calendar-day`
   therefore repaints DISABLED days too, and the "unavailable days carry no
   chip, the gaps ARE the information" behaviour — the thing that makes a quiet
   month legible at a glance — silently disappears on that template.

   Every chip-fill rule below is guarded with
   `:not(.so-bw-calendar-day--disabled):not(.so-bw-calendar-day--selected)`.
   Geometry and typography (radius, min-height, font) are safe on the bare
   selector, because no state rule sets them.

   HAZARD 7 · SELECTION IS NOT REPAINTABLE, AND THAT IS DELIBERATE

     #so-booking-widget[data-template] .so-bw-calendar-day--selected  (1,3,0) !important
     #so-booking-widget[data-template] .so-bw-slot-row--selected      (1,3,0) !important

   Both are locked. Nothing here tries to beat them: "the day you picked is
   solid accent" and "the time you picked is an accent tint with a 2px ring"
   stay true on all nineteen, which is the one thing that SHOULD be identical
   everywhere — a customer moving between two sites built on this plugin reads
   selection the same way both times.

   Selection is differentiated on the axes that are NOT locked: border-radius,
   ::after marks, and font-weight. Read that as the rule, not as a workaround.
   ========================================================================== */


/* ── Shared scaffolding ──────────────────────────────────────────────────
   Three tokens the nineteen blocks below retune instead of restating
   geometry. Declared here, on the container, so a template that sets none of
   them still computes — see the undeclared-token fault section 17.20 records,
   where 23 usages of a token nobody declared silently zeroed every summary
   row. widget-css-cascade.test.ts now fails on that class of mistake; these
   are declared first for the same reason. */
#so-booking-widget .so-bw-container {
    --so-bw-cal-gap: 4px;
    --so-bw-cal-day-h: 40px;
    --so-bw-slot-min-h: 44px;
}
#so-booking-widget .so-bw-calendar-grid,
#so-booking-widget .so-bw-calendar-weekdays {
    gap: var(--so-bw-cal-gap);
}
#so-booking-widget .so-bw-calendar-day {
    min-height: var(--so-bw-cal-day-h);
    /* THE MIN-HEIGHT ABOVE BECOMES A MIN-WIDTH ON THE CIRCLE TEMPLATES.
       REPORTED: inner-overflow-x, div.so-bw-calendar-grid, 304 > 276, on
       Centred at 360px — and the arithmetic names the cause exactly:
       7 x 40 + 6 x 4 = 304.

       Centred, Hero and Dialog give the day `aspect-ratio: 1 / 1`. A grid
       item with an aspect ratio TRANSFERS its height minimum through the
       ratio into a width minimum, and a `1fr` track's automatic minimum is
       min-content — so every column acquired a 40px floor and the month grew
       28px wider than the scroller holding it.

       `min-width: 0` is the documented opt-out from that automatic minimum.
       Stated for every template, not just the three: it is inert wherever the
       column is already wider than the floor, and it means adding an aspect
       ratio to a fourth month cannot reintroduce this. */
    min-width: 0;
}

/* The month label and the nav pair are one optical unit on every template;
   what changes is the type. Stated once so the per-template blocks only carry
   the part that is theirs. */
#so-booking-widget .so-bw-calendar-month {
    font-variant-numeric: tabular-nums;
}


/* ── 18.1 · CENTRED — the circle month ───────────────────────────────────
   C4 is one narrow measure and nothing competing with the question, so the
   month is the most generous of the nineteen: true circles, air between them,
   and the label centred over the grid rather than pushed to one side.

   The grid is bounded at 392px and centred. Unbounded, Centred's 520px measure
   produced 68px circles — a month that reads as a control panel rather than a
   date. 392 / 7 gives a 50px circle at desktop and lets the cells shrink to
   41px at 360, which clears the 30px floor with room. */
#so-booking-widget[data-template="centred"] .so-bw-container {
    --so-bw-cal-gap: 6px;
}
#so-booking-widget[data-template="centred"] .so-bw-calendar {
    max-width: 392px;
    margin-inline: auto;
}
#so-booking-widget[data-template="centred"] .so-bw-calendar-header {
    justify-content: center;
    gap: 10px;
}
#so-booking-widget[data-template="centred"] .so-bw-calendar-month {
    flex: 0 1 auto;
    font-size: 17px;
    font-weight: 600;
    letter-spacing: -0.2px;
    text-align: center;
}
#so-booking-widget[data-template="centred"] .so-bw-calendar-day {
    aspect-ratio: 1 / 1;
    border-radius: 50%;
}
#so-booking-widget[data-template="centred"] .so-bw-calendar-day-num {
    font-size: 13.5px;
}
/* Time: 4-up circles-adjacent rows in the measure. The row keeps a soft 10px
   corner so it reads as a sibling of the circle above it without being one. */
/* CENTRING A TITLE INSIDE HALF A ROW IS NOT CENTRING IT.
   Seen in the browser, not in a measurement: the panel head is a flex row with
   the titles on the left and the timezone control on the right, so
   `text-align: center` centred "Time Slot" on the LEFT HALF — 120px left of
   the panel's true centre and 155px right of the gutter that "Change date"
   and the slot rows align to. Neither centred nor aligned, which is the worst
   of the three options and the only one that looks accidental.

   Making the head a column makes the centring true: the title sits over the
   middle of the panel and the timezone control under it, both on the panel's
   axis. That is the one arrangement consistent with a template whose entire
   premise is a single centred measure. */
#so-booking-widget[data-template="centred"] .so-bw-slot-panel-head {
    border-bottom: none;
    flex-direction: column;
    align-items: center;
    gap: 10px;
    text-align: center;
}
#so-booking-widget[data-template="centred"] .so-bw-slot-panel-head > * {
    flex: 0 0 auto;
    max-width: 100%;
}
#so-booking-widget[data-template="centred"] .so-bw-slot-panel-titles {
    align-items: center;
}
#so-booking-widget[data-template="centred"] .so-bw-slot-row {
    border-radius: var(--so-bw-r-10);
    min-height: 46px;
}
#so-booking-widget[data-template="centred"] .so-bw-slot-time {
    font-size: 14px;
    letter-spacing: -0.1px;
}

/* ── 18.4 · HERO — the month at hero scale ───────────────────────────────
   The photo card sets the register: one big thing, centred, with air. The
   month follows it — a 22px light month name over 8px-separated circles.
   Bounded at 420px for the same reason Centred is bounded. */
#so-booking-widget[data-template="hero"] .so-bw-container {
    --so-bw-cal-gap: 8px;
}
#so-booking-widget[data-template="hero"] .so-bw-calendar {
    max-width: 420px;
    margin-inline: auto;
}
#so-booking-widget[data-template="hero"] .so-bw-calendar-header {
    justify-content: center;
    gap: 14px;
    margin-bottom: 6px;
}
#so-booking-widget[data-template="hero"] .so-bw-calendar-month {
    flex: 0 1 auto;
    font-size: 22px;
    font-weight: 300;
    letter-spacing: 0.02em;
    text-align: center;
}
#so-booking-widget[data-template="hero"] .so-bw-calendar-day {
    aspect-ratio: 1 / 1;
    border-radius: 50%;
}
#so-booking-widget[data-template="hero"] .so-bw-calendar-day-num {
    font-family: inherit;
    font-size: 15px;
    font-weight: 500;
}
#so-booking-widget[data-template="hero"] .so-bw-calendar-weekdays span {
    font-size: 10px;
    letter-spacing: 0.16em;
}
/* Time: a gallery of large tiles, matching the photo card's scale. */
#so-booking-widget[data-template="hero"] .so-bw-slot-panel-head {
    border-bottom: none;
}
#so-booking-widget[data-template="hero"] .so-bw-slot-time {
    font-size: 15px;
    font-weight: 500;
}


/* ── 18.5 · TABBED RAIL — the docked month ───────────────────────────────
   The rail template is the densest of the nineteen and its month should read
   as an instrument panel: 2px gutters, square corners, mono throughout, and
   a ruled weekday header that belongs to the grid rather than floating above
   it. */
#so-booking-widget[data-template="tabbed"] .so-bw-container {
    --so-bw-cal-gap: 2px;
    --so-bw-cal-day-h: 38px;
}
#so-booking-widget[data-template="tabbed"] .so-bw-calendar-header {
    padding-bottom: 8px;
}
#so-booking-widget[data-template="tabbed"] .so-bw-calendar-month {
    font-family: var(--so-bw-mono);
    font-size: 11.5px;
    font-weight: 500;
    letter-spacing: 0.14em;
    text-transform: uppercase;
}
#so-booking-widget[data-template="tabbed"] .so-bw-calendar-weekdays {
    padding-bottom: 4px;
    border-bottom: 1px solid var(--so-bw-ds-n200);
    margin-bottom: 6px;
}
#so-booking-widget[data-template="tabbed"] .so-bw-calendar-day {
    border-radius: var(--so-bw-r-4);
}
#so-booking-widget[data-template="tabbed"] .so-bw-calendar-day-num {
    font-size: 12px;
}
/* Time: the docked strip keeps the same square language. */
#so-booking-widget[data-template="tabbed"] .so-bw-slot-row {
    border-radius: var(--so-bw-r-4);
}
#so-booking-widget[data-template="tabbed"] .so-bw-slot-time {
    font-size: 12.5px;
    letter-spacing: 0.02em;
}
#so-booking-widget[data-template="tabbed"] .so-bw-slot-panel-head {
    border-bottom-width: 2px;
    border-bottom-color: var(--so-bw-primary);
}


/* ── 18.7 · AGENDA — the month as a running list ─────────────────────────
   Agenda answers the date question with a list of days, not a grid; the month
   grid is the secondary path behind it. So the grid is drawn as a LEDGER —
   no chips, hairline rules between weeks, numerals set left in the cell — and
   the two paths read as the same idea at two densities instead of as two
   unrelated controls. */
#so-booking-widget[data-template="agenda"] .so-bw-container {
    --so-bw-cal-gap: 0px;
}
#so-booking-widget[data-template="agenda"] .so-bw-calendar-header {
    padding-bottom: 8px;
    border-bottom: 2px solid var(--so-bw-ds-n900);
    margin-bottom: 0;
}
#so-booking-widget[data-template="agenda"] .so-bw-calendar-month {
    font-size: 13px;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
}
#so-booking-widget[data-template="agenda"] .so-bw-calendar-weekdays {
    margin-bottom: 0;
}
#so-booking-widget[data-template="agenda"] .so-bw-calendar-weekdays span {
    padding: 6px 0 5px;
    border-bottom: 1px solid var(--so-bw-ds-n200);
    text-align: left;
    padding-left: 6px;
}
#so-booking-widget[data-template="agenda"] .so-bw-calendar-day {
    border-radius: 0;
    justify-content: flex-start;
    padding-left: 6px;
    box-shadow: inset 0 -1px 0 var(--so-bw-ds-n200);
}
#so-booking-widget[data-template="agenda"] .so-bw-calendar-day:not(.so-bw-calendar-day--disabled):not(.so-bw-calendar-day--selected) {
    background: transparent;
}
#so-booking-widget[data-template="agenda"] .so-bw-calendar-day:not(.so-bw-calendar-day--disabled):not(.so-bw-calendar-day--selected):hover {
    background: var(--so-bw-ds-n50);
}
/* An available day in a ledger needs a POSITIVE mark, because "has a chip" is
   no longer the signal — with the fills gone, an available day and a disabled
   one differ only by text colour, and colour alone is not a state.
   A 12x2 accent underscore does it without adding a second fill.

   `.so-bw-calendar-day` is already `position: relative` (it carries the
   today dot in ::after), so this needs no positioning context of its own. */
#so-booking-widget[data-template="agenda"] .so-bw-calendar-day--available:not(.so-bw-calendar-day--selected)::before {
    content: '';
    position: absolute;
    left: 6px;
    bottom: 6px;
    width: 12px;
    height: 2px;
    background: var(--so-bw-primary);
    border-radius: var(--so-bw-r-1);
}


/* ── 18.8 · WEEK BOARD — the board's own month ───────────────────────────
   The week board is the date treatment; the month behind it is how you get to
   a different week. Drawn as seven columns of equal weight with a heavier
   column rule, so it previews the board it leads to. */
#so-booking-widget[data-template="weekboard"] .so-bw-container {
    --so-bw-cal-gap: 1px;
}
#so-booking-widget[data-template="weekboard"] .so-bw-calendar-grid {
    background: var(--so-bw-ds-n200);
    border: 1px solid var(--so-bw-ds-n200);
    border-radius: var(--so-bw-ds-radius-ctl);
    overflow: hidden;
}
#so-booking-widget[data-template="weekboard"] .so-bw-calendar-day {
    border-radius: 0;
}
#so-booking-widget[data-template="weekboard"] .so-bw-calendar-day:not(.so-bw-calendar-day--disabled):not(.so-bw-calendar-day--selected) {
    background: var(--so-bw-ds-n0);
}
#so-booking-widget[data-template="weekboard"] .so-bw-calendar-day--disabled {
    background: var(--so-bw-ds-n50);
}
#so-booking-widget[data-template="weekboard"] .so-bw-calendar-month {
    font-size: 13px;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
}


/* ── 18.10 · CARD DECK — the month IS a card ─────────────────────────────
   The deck's premise is that every question arrives as a card off the top of
   a stack. The month was the one screen still rendering bare on the step
   background. Now it is a card: white, bordered, shadowed, with the label and
   the nav on its own top edge. */
#so-booking-widget[data-template="deck"] .so-bw-container {
    --so-bw-cal-gap: 5px;
}
#so-booking-widget[data-template="deck"] .so-bw-calendar {
    background: var(--so-bw-ds-n0);
    border: 1px solid var(--so-bw-ds-n200);
    border-radius: var(--so-bw-ds-radius-card);
    padding: 14px 16px 16px;
    box-shadow: 0 1px 2px rgba(16, 24, 40, 0.06), 0 4px 12px rgba(16, 24, 40, 0.05);
}
#so-booking-widget[data-template="deck"] .so-bw-calendar-header {
    padding-bottom: 10px;
    border-bottom: 1px solid var(--so-bw-ds-n100);
    margin-bottom: 10px;
}
#so-booking-widget[data-template="deck"] .so-bw-calendar-month {
    font-size: 15px;
    font-weight: 600;
    text-align: left;
}
#so-booking-widget[data-template="deck"] .so-bw-calendar-day {
    border-radius: var(--so-bw-ds-radius-card);
}
#so-booking-widget[data-template="deck"] .so-bw-calendar-day-num {
    font-family: inherit;
    font-size: 13px;
    font-weight: 500;
}


/* ── 18.11 · WIDE CANVAS — the ledger month ──────────────────────────────
   Wide Canvas answers everything as a dense comparison table, and the month
   is the last thing on it still drawn as a set of soft chips. Tightened to
   2px gutters and 4px corners so it sits in the same visual class as the
   table beside it. */
#so-booking-widget[data-template="wide"] .so-bw-container {
    --so-bw-cal-gap: 2px;
    --so-bw-cal-day-h: 38px;
}
#so-booking-widget[data-template="wide"] .so-bw-calendar-weekdays {
    padding-bottom: 5px;
    border-bottom: 1px solid var(--so-bw-ds-n200);
    margin-bottom: 5px;
}
#so-booking-widget[data-template="wide"] .so-bw-calendar-weekdays span {
    font-size: 9.5px;
    letter-spacing: 0.1em;
}
#so-booking-widget[data-template="wide"] .so-bw-calendar-day {
    border-radius: var(--so-bw-r-4);
}
#so-booking-widget[data-template="wide"] .so-bw-calendar-day-num {
    font-size: 12px;
}
#so-booking-widget[data-template="wide"] .so-bw-calendar-month {
    font-size: 13.5px;
    font-weight: 600;
}


/* ── 18.13 · DAYPART — the month tinted by daypart ───────────────────────
   The template's idea is that a day has three parts. The month carries that
   idea forward: a tinted header band over a tight grid, so the month and the
   triptych below it read as one instrument. Row gutter deliberately wider
   than the column gutter — weeks are the unit here, not days. */
#so-booking-widget[data-template="daypart"] .so-bw-container {
    --so-bw-cal-gap: 3px;
}
#so-booking-widget[data-template="daypart"] .so-bw-calendar-grid {
    row-gap: 6px;
    column-gap: 3px;
}
#so-booking-widget[data-template="daypart"] .so-bw-calendar-weekdays {
    background: color-mix(in srgb, var(--so-bw-primary) 8%, transparent);
    border-radius: var(--so-bw-ds-radius-chip);
    padding: 6px 0;
    margin-bottom: 8px;
}
#so-booking-widget[data-template="daypart"] .so-bw-calendar-weekdays span {
    padding: 0;
    color: var(--so-bw-primary-ink, var(--so-bw-ds-n700));
    letter-spacing: 0.1em;
}
#so-booking-widget[data-template="daypart"] .so-bw-calendar-day {
    border-radius: var(--so-bw-r-6);
}
#so-booking-widget[data-template="daypart"] .so-bw-calendar-month {
    font-size: 14px;
    font-weight: 600;
    letter-spacing: 0.04em;
}
/* Time: the triptych's tiles lose their outline and gain a fill, so the three
   panels read as three surfaces rather than 42 outlined boxes. */
#so-booking-widget[data-template="daypart"] .so-bw-slot-row {
    border-radius: var(--so-bw-r-8);
    border-color: transparent;
    min-height: 46px;
}
#so-booking-widget[data-template="daypart"] .so-bw-slot-time {
    font-size: 13px;
    font-weight: 600;
}


/* ── 18.14 · TIMELINE — the month as an axis ─────────────────────────────
   Timeline's whole language is a vertical axis with events pinned to it. The
   month grid gets the same treatment rotated: each week is a band, each day
   is a station on it, numerals set left against a leading rule. No chips —
   an axis does not have chips. */
#so-booking-widget[data-template="timeline"] .so-bw-container {
    --so-bw-cal-gap: 0px;
    --so-bw-cal-day-h: 42px;
}
#so-booking-widget[data-template="timeline"] .so-bw-calendar-header {
    justify-content: flex-start;
    gap: 6px;
}
#so-booking-widget[data-template="timeline"] .so-bw-calendar-month {
    order: -1;
    flex: 1 1 auto;
    text-align: left;
    font-size: 14px;
    font-weight: 600;
    letter-spacing: 0.02em;
}
#so-booking-widget[data-template="timeline"] .so-bw-calendar-weekdays span {
    text-align: left;
    padding-left: 7px;
}
#so-booking-widget[data-template="timeline"] .so-bw-calendar-day {
    border-radius: 0;
    justify-content: flex-start;
    padding-left: 7px;
    box-shadow: inset 1px 0 0 var(--so-bw-ds-n200);
}
#so-booking-widget[data-template="timeline"] .so-bw-calendar-day:not(.so-bw-calendar-day--disabled):not(.so-bw-calendar-day--selected) {
    background: transparent;
}
#so-booking-widget[data-template="timeline"] .so-bw-calendar-day--available:not(.so-bw-calendar-day--selected) {
    box-shadow: inset 2px 0 0 var(--so-bw-primary);
}
#so-booking-widget[data-template="timeline"] .so-bw-calendar-day:not(.so-bw-calendar-day--disabled):not(.so-bw-calendar-day--selected):hover {
    background: var(--so-bw-ds-n50);
}
#so-booking-widget[data-template="timeline"] .so-bw-calendar-day-num {
    font-size: 12.5px;
}


/* ── 18.16 · FROST — the month on glass ──────────────────────────────────
   The sheet is the material. The month floats on it with no fill of its own
   and 14px corners matching the pill sheet below, so the two surfaces belong
   to one system. */
#so-booking-widget[data-template="frost"] .so-bw-container {
    --so-bw-cal-gap: 5px;
}
#so-booking-widget[data-template="frost"] .so-bw-calendar-day {
    border-radius: var(--so-bw-r-14);
}
#so-booking-widget[data-template="frost"] .so-bw-calendar-day:not(.so-bw-calendar-day--disabled):not(.so-bw-calendar-day--selected) {
    background: rgba(255, 255, 255, 0.55);
}
#so-booking-widget[data-template="frost"] .so-bw-calendar-day:not(.so-bw-calendar-day--disabled):not(.so-bw-calendar-day--selected):hover {
    background: rgba(255, 255, 255, 0.85);
}
#so-booking-widget[data-template="frost"] .so-bw-calendar-month {
    font-size: 15px;
    font-weight: 500;
    letter-spacing: 0.01em;
}
#so-booking-widget[data-template="frost"] .so-bw-calendar-day-num {
    font-family: inherit;
    font-size: 13px;
    font-weight: 500;
}


/* ── 18.17 · DUSK — the month on ink ─────────────────────────────────────
   Hairline rows on a dark ground, champagne accent. The month is a lattice of
   raised ink cells separated by a single pixel of the ground showing through,
   which is the same device the service list uses one row at a time.

   Numerals are tracked out: at 13px on ink, tight mono reads as a smear. */
#so-booking-widget[data-template="dusk"] .so-bw-container {
    --so-bw-cal-gap: 1px;
}
#so-booking-widget[data-template="dusk"] .so-bw-calendar-day {
    border-radius: 0;
}
#so-booking-widget[data-template="dusk"] .so-bw-calendar-day:not(.so-bw-calendar-day--disabled):not(.so-bw-calendar-day--selected) {
    background: rgba(255, 255, 255, 0.055);
}
#so-booking-widget[data-template="dusk"] .so-bw-calendar-day:not(.so-bw-calendar-day--disabled):not(.so-bw-calendar-day--selected):hover {
    background: rgba(255, 255, 255, 0.12);
}
#so-booking-widget[data-template="dusk"] .so-bw-calendar-day-num {
    font-size: 12.5px;
    letter-spacing: 0.06em;
}
#so-booking-widget[data-template="dusk"] .so-bw-calendar-month {
    font-size: 13px;
    font-weight: 500;
    letter-spacing: 0.16em;
    text-transform: uppercase;
}
#so-booking-widget[data-template="dusk"] .so-bw-calendar-weekdays span {
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
    letter-spacing: 0.12em;
}


/* ── 18.18 · EDITORIAL — the month as an index ───────────────────────────
   The serif register. Numerals in Newsreader at 15px, weeks separated by
   rules rather than gutters, and the month name set as a display line. The
   index is this template's one idea and the month is an index of days.

   Weekday initials keep the mono micro-label: mixing a serif numeral with a
   serif label at 10px produced two serifs at two sizes fighting, and the
   label is signage, not reading. */
#so-booking-widget[data-template="editorial"] .so-bw-container {
    --so-bw-cal-gap: 0px;
    --so-bw-cal-day-h: 44px;
}
#so-booking-widget[data-template="editorial"] .so-bw-calendar-header {
    padding-bottom: 10px;
    border-bottom: 1px solid var(--so-bw-ds-n900);
    margin-bottom: 4px;
}
#so-booking-widget[data-template="editorial"] .so-bw-calendar-month {
    font-family: 'Newsreader', Georgia, serif;
    font-size: 20px;
    font-weight: 400;
    letter-spacing: 0;
}
#so-booking-widget[data-template="editorial"] .so-bw-calendar-day {
    border-radius: 0;
    box-shadow: inset 0 -1px 0 var(--so-bw-ds-n200);
}
#so-booking-widget[data-template="editorial"] .so-bw-calendar-day:not(.so-bw-calendar-day--disabled):not(.so-bw-calendar-day--selected) {
    background: transparent;
}
#so-booking-widget[data-template="editorial"] .so-bw-calendar-day:not(.so-bw-calendar-day--disabled):not(.so-bw-calendar-day--selected):hover {
    background: var(--so-bw-ds-n50);
}
#so-booking-widget[data-template="editorial"] .so-bw-calendar-day-num {
    font-family: 'Newsreader', Georgia, serif;
    font-size: 15px;
    font-weight: 400;
}
/* THE AVAILABILITY MARK HAD BECOME A HYPERLINK.
   Found by looking at it, not by measuring it — every geometric and contrast
   check passes, because a 2px gold bar under a numeral is exactly as correct
   as it is wrong depending on what the numeral is.

   Editorial has always marked an available day with `box-shadow: 0 2px 0` on
   the NUMERAL — "a rule under its numeral, the way a printed listing marks a
   live entry", per the rule above. That read as a listing mark while the
   numeral was a 13px mono figure sitting on a grey chip. This section made it
   a 15px serif figure on a transparent ledger cell, and in that setting a
   coloured rule hugging the width of the text is the web's single most
   universal signal for a link. Every available day looked clickable-as-a-link
   rather than selectable-as-a-date.

   The idea survives; only its position moves. The mark becomes the CELL's own
   bottom rule — full width, aligned with the week rules around it, which is
   unambiguously a ledger rule and cannot be read as underlined text. It also
   keeps availability off colour alone: an available day carries a 2px rule
   where an unavailable one carries the 1px hairline, so the two differ in
   weight as well as hue. */
#so-booking-widget[data-template="editorial"] .so-bw-calendar-day--available .so-bw-calendar-day-num {
    box-shadow: none;
}
#so-booking-widget[data-template="editorial"] .so-bw-calendar-day--available:not(.so-bw-calendar-day--selected) {
    box-shadow: inset 0 -2px 0 var(--so-bw-primary);
}


/* ── 18.19 · CORAL POP — the pillow month ────────────────────────────────
   Round everything. Full pills, generous gutters, a heavier numeral so the
   month keeps weight against the soft ground. */
#so-booking-widget[data-template="soft"] .so-bw-container {
    --so-bw-cal-gap: 6px;
}
#so-booking-widget[data-template="soft"] .so-bw-calendar-day {
    border-radius: 999px;
}
#so-booking-widget[data-template="soft"] .so-bw-calendar-day-num {
    font-family: inherit;
    font-size: 13.5px;
    font-weight: 600;
}
#so-booking-widget[data-template="soft"] .so-bw-calendar-month {
    font-size: 16px;
    font-weight: 700;
}
#so-booking-widget[data-template="soft"] .so-bw-calendar-weekdays span {
    color: var(--so-bw-ds-n700);
}
#so-booking-widget[data-template="soft"] .so-bw-slot-panel-head {
    border-bottom: none;
}


/* ── 18.20 · The panel header, nineteen ways ─────────────────────────────
   `slotHead` measured IDENTICAL on all nineteen: 83px tall, space-between, a
   1px bottom rule. The blocks above already retuned it on the eight templates
   whose language demanded it; these are the remaining eleven, each taking the
   divider treatment its own step headers use.

   Grouped rather than spread through the blocks above because the point is
   the CONTRAST between them — read as a set, it is obvious that no two match;
   read one per block, it is impossible to tell. */
#so-booking-widget[data-template="agenda"] .so-bw-slot-panel-head {
    border-bottom-width: 2px;
    border-bottom-color: var(--so-bw-ds-n900);
}
#so-booking-widget[data-template="weekboard"] .so-bw-slot-panel-head {
    border-bottom: none;
    padding-bottom: 0;
}
#so-booking-widget[data-template="deck"] .so-bw-slot-panel-head {
    border-bottom-color: var(--so-bw-ds-n100);
}
#so-booking-widget[data-template="wide"] .so-bw-slot-panel-head {
    border-bottom-color: var(--so-bw-ds-n300);
}
#so-booking-widget[data-template="daypart"] .so-bw-slot-panel-head {
    border-bottom: none;
}
/* Timeline's tick is on the TOP edge, not the left. A left rule with the
   padding it needs indents the panel title relative to the slot rows beneath
   it, which is precisely the ragged-gutter fault section 17.20 spent a pass
   removing. A top rule carries the same accent with no horizontal shift. */
#so-booking-widget[data-template="timeline"] .so-bw-slot-panel-head {
    border-bottom: none;
    border-top: 2px solid var(--so-bw-primary);
    padding-top: 10px;
}
#so-booking-widget[data-template="frost"] .so-bw-slot-panel-head {
    border-bottom-color: rgba(255, 255, 255, 0.6);
}
#so-booking-widget[data-template="dusk"] .so-bw-slot-panel-head {
    border-bottom-color: rgba(255, 255, 255, 0.12);
}
#so-booking-widget[data-template="editorial"] .so-bw-slot-panel-head {
    border-bottom: 3px double var(--so-bw-ds-n900);
}

/* The panel title, likewise: 17 of 19 shared one value. Only the templates
   whose register is genuinely different are moved — a difference for its own
   sake here would just be noise on a heading nobody is reading twice. */
#so-booking-widget[data-template="editorial"] .so-bw-slot-panel-title {
    font-family: 'Newsreader', Georgia, serif;
    font-size: 19px;
    font-weight: 400;
}
#so-booking-widget[data-template="dusk"] .so-bw-slot-panel-title {
    font-family: var(--so-bw-mono);
    font-size: 12px;
    font-weight: 500;
    letter-spacing: 0.12em;
    text-transform: uppercase;
}
#so-booking-widget[data-template="hero"] .so-bw-slot-panel-title {
    font-size: 20px;
    font-weight: 300;
}
#so-booking-widget[data-template="soft"] .so-bw-slot-panel-title {
    font-size: 17px;
    font-weight: 700;
}
#so-booking-widget[data-template="tabbed"] .so-bw-slot-panel-title,
#so-booking-widget[data-template="wide"] .so-bw-slot-panel-title {
    font-size: 13px;
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
}


/* ── 18.21 · Phone tier ──────────────────────────────────────────────────
   Two treatments above are generous by design and stop being generous at
   360px, where seven columns leave 41px per cell.

   Circles keep their aspect ratio and simply get smaller, which is correct.
   What does NOT survive is the gutter: 8px of gutter out of 328px of width is
   17% of the month spent on air. Both gutters step down, and the two bounded
   calendars release their max-width so the month uses the full column. */
/* was @media (max-width: 480px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    /* The three circle months also drop their height floor here. With
       `min-width: 0` releasing the transferred minimum, a 360px screen gives
       36px columns — so a 40px floor would make the height 40 against a width
       of 36 and the circle would render as an ellipse. 34px lets the square,
       and therefore the circle, hold. Still clear of the 30px floor the
       layout audit enforces and of WCAG 2.2's 24px target minimum. */
    #so-booking-widget:where([data-cw~="480"])[data-template="centred"] .so-bw-container,
    #so-booking-widget:where([data-cw~="480"])[data-template="hero"] .so-bw-container{
        --so-bw-cal-day-h: 34px;
    }
    #so-booking-widget:where([data-cw~="480"])[data-template="centred"] .so-bw-container,
    #so-booking-widget:where([data-cw~="480"])[data-template="hero"] .so-bw-container{
        --so-bw-cal-gap: 4px;
    }
    #so-booking-widget:where([data-cw~="480"])[data-template="centred"] .so-bw-calendar,
    #so-booking-widget:where([data-cw~="480"])[data-template="hero"] .so-bw-calendar{
        max-width: none;
    }
    #so-booking-widget:where([data-cw~="480"])[data-template="soft"] .so-bw-container{
        --so-bw-cal-gap: 4px;
    }
    /* Editorial's 44px ledger rows are 7 x 44 = 308px of month on a 328px
       column; the serif numeral holds at 14px and the row at 40. */
    #so-booking-widget:where([data-cw~="480"])[data-template="editorial"] .so-bw-container{
        --so-bw-cal-day-h: 40px;
    }
    #so-booking-widget:where([data-cw~="480"])[data-template="editorial"] .so-bw-calendar-day-num{
        font-size: 14px;
    }
    /* Hero's 22px month name is the single widest label in the set and it
       wraps under the chevrons at 360. */
    #so-booking-widget:where([data-cw~="480"])[data-template="hero"] .so-bw-calendar-month{
        font-size: 18px;
    }
    #so-booking-widget:where([data-cw~="480"])[data-template="editorial"] .so-bw-calendar-month{
        font-size: 17px;
    }



/* ── 18.22 · Closing the last shared clusters ────────────────────────────
   Re-fingerprinting after 18.1–18.21 put both axes at 19 distinct treatments
   out of 19. Four properties were still shared by ten or more templates:

     calShell    11 share the bare transparent shell
     calHead     11 share the default space-between header
     slotTitle   12 share the 16px bold panel heading
     slotGTitle  11 share the 10.5px mono period caption

   These are containers and captions, not the controls, so the bar for
   changing them is different: a shell added purely to make a number go down
   is noise on the screen a customer looks at longest. Only the templates
   whose own language already answers "what is a panel" or "what is a caption"
   differently are moved. The rest keep the bare shell ON PURPOSE — a month
   floating on the step background is a treatment, and Centred, Hero, Agenda,
   Timeline, Editorial and Dusk all earn their distinction in the grid itself,
   where 18.1–18.19 put it.

   PARTLY SUPERSEDED — see section 22. The two SHELL clusters above still
   stand: a box added to move a number is still noise. The two CAPTION
   clusters do not. Leaving slotTitle and slotGTitle shared was justified
   here as "captions, not controls", and that turned out to be the wrong
   read: with the boxes deliberately left alone, the caption IS the only
   thing distinguishing one panel from the next, and twelve templates
   naming their pane in identical 16px bold is what a reader actually
   notices. Section 22 splits both — by type alone, adding no boxes, so the
   shell reasoning above is untouched. */

/* Shells, for the four whose steps are already panelled surfaces. */
#so-booking-widget[data-template="soft"] .so-bw-calendar {
    background: var(--so-bw-ds-n0);
    border-radius: var(--so-bw-r-20);
    padding: 14px 16px 16px;
}
#so-booking-widget[data-template="daypart"] .so-bw-calendar {
    background: var(--so-bw-ds-n50);
    border-radius: var(--so-bw-ds-radius-card);
    padding: 12px 14px 14px;
}
#so-booking-widget[data-template="tabbed"] .so-bw-calendar {
    border-top: 2px solid var(--so-bw-ds-n900);
    padding-top: 10px;
}

/* Header arrangement. Centred over the grid, hard left, or split to the
   edges — three answers, given to the templates that read that way elsewhere. */
#so-booking-widget[data-template="soft"] .so-bw-calendar-header,
#so-booking-widget[data-template="frost"] .so-bw-calendar-header {
    justify-content: center;
    gap: 12px;
}
#so-booking-widget[data-template="soft"] .so-bw-calendar-month,
#so-booking-widget[data-template="frost"] .so-bw-calendar-month {
    flex: 0 1 auto;
    text-align: center;
}
#so-booking-widget[data-template="wide"] .so-bw-calendar-header {
    justify-content: flex-start;
    gap: 4px;
}
#so-booking-widget[data-template="wide"] .so-bw-calendar-month {
    order: -1;
    flex: 1 1 auto;
    text-align: left;
}

/* The period caption. Morning / Afternoon / Evening is the one label on this
   step that repeats three times, so it is where a register is most legible. */
#so-booking-widget[data-template="editorial"] .so-bw-slot-group-title {
    font-family: 'Newsreader', Georgia, serif;
    font-size: 15px;
    font-weight: 400;
    letter-spacing: 0;
    text-transform: none;
}
#so-booking-widget[data-template="hero"] .so-bw-slot-group-title {
    font-family: inherit;
    font-size: 13px;
    font-weight: 400;
    letter-spacing: 0.18em;
}
#so-booking-widget[data-template="soft"] .so-bw-slot-group-title {
    font-family: inherit;
    font-size: 13px;
    font-weight: 700;
    letter-spacing: 0.02em;
    text-transform: none;
}
#so-booking-widget[data-template="agenda"] .so-bw-slot-group-title {
    font-family: inherit;
    font-size: 12px;
    font-weight: 700;
    letter-spacing: 0.06em;
}
#so-booking-widget[data-template="frost"] .so-bw-slot-group-title {
    font-family: inherit;
    font-size: 12px;
    font-weight: 500;
    letter-spacing: 0.1em;
    text-transform: none;
}
#so-booking-widget[data-template="deck"] .so-bw-slot-group-title {
    font-family: inherit;
    font-size: 12.5px;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: none;
}

/* The panel heading, for the four still on the shared value whose register
   genuinely differs from it. */
#so-booking-widget[data-template="agenda"] .so-bw-slot-panel-title {
    font-size: 14px;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
}
#so-booking-widget[data-template="deck"] .so-bw-slot-panel-title {
    font-size: 15px;
    font-weight: 600;
    letter-spacing: -0.1px;
}
#so-booking-widget[data-template="frost"] .so-bw-slot-panel-title {
    font-size: 16px;
    font-weight: 500;
    letter-spacing: 0.01em;
}


/* ==========================================================================
   19. ACCESSIBILITY
   --------------------------------------------------------------------------
   Driven by tests/visual/a11y-audit.mjs, which checks what the geometric
   sweep cannot see: whether a control can be reached, whether you can see
   where you are once you reach it, whether the layout survives a 200% zoom
   and a reader's own text spacing, and whether anything is still visible when
   the operating system throws every colour away.

   The layout sweep ran green at 1,520 checkpoints with all of it unverified.
   ========================================================================== */

/* ── 19.1 · Screen-reader-only text ──────────────────────────────────────
   Carries the step announcement — see the role="status" live region at the
   top of the container in booking-widget.js. Before it existed the a11y
   audit reported "no-live-region" on all nineteen templates and every step:
   each step change is a client-side x-show swap, so nothing navigates,
   nothing takes focus, and a screen-reader user pressing Continue heard
   silence.

   The 1x1 clipped box rather than `display: none`, because a hidden element
   is not in the accessibility tree at all, which is the one thing this must
   not be. `clip` is kept alongside `clip-path` for Safari. */
#so-booking-widget .so-bw-sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

/* ── 19.2 · The focus halo, on all nineteen ──────────────────────────────
   The halo in the PILOT FOCUS RINGS block is enumerated per template, and
   the enumeration is the five pilots. The other fourteen were not left
   without a focus state — the a11y audit's focus pass reports no
   `no-visible-focus` anywhere, because they fall through to the browser's own
   ring — but that means two different focus treatments ship in one product,
   decided by which template the site happens to use.

   Same declarations, no template attribute. Scores below the pilot list, so
   that block still wins where it applies and this is what the other fourteen
   get: identical, and now deliberate rather than inherited from Chrome.

   :focus-visible only. Pointer focus is suppressed a few hundred lines above
   for the WordPress-theme reason documented there, and nothing here reopens
   it. Forced-colors already restores a real outline for exactly these
   selectors — that block is template-agnostic, so it covers the fourteen
   without a second entry. */
#so-booking-widget .so-bw-card[role="button"]:focus-visible,
#so-booking-widget .so-bw-card-profile:focus-visible,
#so-booking-widget .so-bw-card-more:focus-visible,
#so-booking-widget .so-bw-calendar-nav:focus-visible,
#so-booking-widget .so-bw-slot-back:focus-visible,
#so-booking-widget .so-bw-slot-row:focus-visible,
#so-booking-widget .so-bw-calendar-day:focus-visible,
#so-booking-widget .so-bw-week-slot:focus-visible,
#so-booking-widget .so-bw-agenda-day:focus-visible,
#so-booking-widget .so-bw-suggest-opt:focus-visible {
    outline: none;
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--so-bw-primary) 28%, transparent);
}

/* THE HALO IS A BOX-SHADOW, AND SELECTION LOCKS BOX-SHADOW.
   REPORTED: no-visible-focus, button.so-bw-calendar-day--selected, Wide
   Canvas, focus/times — the only finding in a 399-checkpoint sweep.

   Hazard 7 in section 18: the selected day and the selected slot row set
   `background` and `box-shadow` with !important at (1,3,0). The halo above is
   a box-shadow at (1,2,0), so on an ALREADY-SELECTED control it loses and
   tabbing onto it changes nothing at all. Wide Canvas is simply where the tab
   traversal reached one inside sixty stops; the fault is in every template.

   Outline is not part of that lock, so the selection ring stays exactly as it
   is and focus gets its own ring outside it.

   Drawn in --so-bw-text, not the accent: this outline sits on the step
   background, 2px beyond a chip that is ALREADY filled with the accent, so an
   accent ring would be a low-contrast band against its own fill. The
   template's text colour is the one value guaranteed to contrast with the
   template's background — #1b1f2a on Centred's white, #e9e6df on Dusk's
   ink — which is exactly the relationship a focus ring needs.

   AND NOTE THE BARE [data-template], for the second time in this section.
   Written as `#so-booking-widget .so-bw-calendar-day--selected:focus-visible`
   this scores (1,2,0) and loses to the PILOT FOCUS RINGS block, which is
   enumerated per template — `[data-template="wide"] .so-bw-calendar-day:focus-visible`
   at (1,3,0) — and sets `outline: none`. The probe that found it is worth
   recording: `outline-offset` came back as 2px while `outline` came back
   `none 0px`. Half the rule applied. That is the signature of losing a
   shorthand to a later rule that names only one of its longhands, and it is
   invisible unless you read the offset. */
#so-booking-widget[data-template] .so-bw-calendar-day--selected:focus-visible,
#so-booking-widget[data-template] .so-bw-slot-row--selected:focus-visible,
#so-booking-widget[data-template] .so-bw-card--selected:focus-visible,
#so-booking-widget[data-template] .so-bw-week-slot--selected:focus-visible {
    outline: 2px solid var(--so-bw-text);
    outline-offset: 2px;
}

/* A horizontal scroller is keyboard-focusable in Chrome — that is correct,
   it is how you scroll it without a mouse — and it lands there with the UA's
   1px ring. REPORTED: thin-focus-ring, div.so-bw-stepbar, "1px outline and no
   other change", on the focus pass.

   1px around a 500px-wide strip is not a focus state you can find. These get
   the widget's own 2px, inset so it is not clipped by the scroller's own
   bounds — the one place an offset ring would be cut in half. */
#so-booking-widget .so-bw-stepbar:focus-visible,
#so-booking-widget .so-bw-slot-scroll:focus-visible,
#so-booking-widget .so-bw-calendar-scroll:focus-visible,
#so-booking-widget .so-bw-week-wrap:focus-visible {
    outline: 2px solid var(--so-bw-primary);
    outline-offset: -2px;
}


/* ── 19.3 · Forced colours, where the audit found holes ──────────────────
   Section 25 has a forced-colors block already. It predates the redesign and
   it shows: it marks selection on `.so-bw-time-slot--selected`, a class the
   widget has not rendered for a long time, and says nothing about
   `.so-bw-slot-row`, which is what actually carries the time choice now.

   REPORTED by the audit's forced-colors pass:
     so-bw-card      no border and no outline — Inline, Dusk
     so-bw-slot-row  no border and no outline — Dusk

   Both are templates whose whole visual language is fills and hairline
   shadows rather than borders, which is exactly the language forced-colors
   discards. On Dusk in High Contrast the slot list was a column of times
   with nothing around them and no way to tell which one was chosen.

   currentColor rather than a system keyword for the resting border: it
   inherits whatever text colour the OS has set, so the edge is guaranteed to
   contrast with the surface it is drawn on. */
@media (forced-colors: active) {
    /* NOTE THE ATTRIBUTE SELECTOR, and that it is bare.
       The per-template rules that strip these borders are written as
       `[data-template="dusk"] .so-bw-slot-row`, which scores (1,3,0). A rule
       written `#so-booking-widget .so-bw-slot-row` is (1,2,0) and loses
       silently — which is what the first version of this block did, and the
       audit went on reporting Dusk's slot row as edgeless with the fix
       apparently already in the file. `[data-template]` matches every
       template, scores the same (1,3,0), and wins on source order. */
    #so-booking-widget[data-template] .so-bw-card:not(.so-bw-sp__panel),
    #so-booking-widget[data-template] .so-bw-slot-row,
    #so-booking-widget[data-template] .so-bw-calendar-day--available,
    #so-booking-widget[data-template] .so-bw-agenda-day,
    #so-booking-widget[data-template] .so-bw-suggest-opt,
    #so-booking-widget[data-template] .so-bw-week-slot {
        border: 1px solid currentColor;
    }

    /* Selection. Everywhere else in this stylesheet it is a 15-16% accent
       wash plus a box-shadow ring — and forced-colors discards both, so the
       chosen row became identical to its neighbours. Restated in the system's
       own selection colours, which is what those keywords are for.

       forced-color-adjust: none is required: without it the background and
       colour set here are themselves overridden, which is the trap that makes
       most "high contrast support" not work. */
    #so-booking-widget[data-template] .so-bw-slot-row--selected,
    #so-booking-widget[data-template] .so-bw-calendar-day--selected,
    #so-booking-widget[data-template] .so-bw-card--selected:not(.so-bw-sp__panel),
    #so-booking-widget[data-template] .so-bw-week-slot--selected {
        forced-color-adjust: none;
        background: Highlight;
        color: HighlightText;
        border: 2px solid Highlight;
    }

    /* Unavailable. On screen this is carried by a missing chip and a grey —
       one is absence and the other is colour, and forced-colors keeps
       neither. GrayText is the system's own disabled ink, and the dashed
       edge carries the state without relying on colour at all. */
    #so-booking-widget[data-template] .so-bw-slot-row--booked,
    #so-booking-widget[data-template] .so-bw-calendar-day--disabled {
        color: GrayText;
        border-style: dashed;
    }
}


/* ==========================================================================
   20. THE LABEL COLUMN NEEDS A FLOOR
   --------------------------------------------------------------------------
   REPORTED by the new text-one-char-per-line check, after a screenshot of the
   confirmation screen showed "Service" set as S/e/r/v/i/c/e down a 9px column
   and "Date & Time" as nine stacked characters:

     Card Deck    label 8px   at 13px type
     Dialog       label 12px, 18px, 21px
     Agenda       label 14px
     Frost        label 24px
     Split Canvas label 17px

   Every geometric check passed all of it. Nothing overflowed, nothing was
   clipped, no row was ragged — the text merely wrapped, one letter at a time,
   on the last screen a customer ever sees.

   TWO SEPARATE CAUSES, both fixed at the component rather than per template:

   1 · FLEX ROWS. `.so-bw-summary-label` and `.so-bw-confirmation-detail-label`
       paired `min-width: 0` with `overflow-wrap: anywhere`. The first removes
       the flex item's automatic min-content floor; the second makes its
       min-content one character. A flex row only wraps when its items cannot
       fit at their MINIMUM widths, so with a one-character minimum the label
       always "fitted" and the row never wrapped. Fixed where those two rules
       are declared.

   2 · GRID ROWS — this section. Fourteen templates lay the summary row and
       the confirmation row out as

           grid-template-columns: minmax(0, 1fr) minmax(0, auto);

       `auto` on the value track resolves to max-content, and `minmax(0, ...)`
       on the label track sets its minimum to ZERO. So a long value — a
       full service name, a localised date with a timezone suffix — takes the
       whole row and the label gets whatever is left, which is nothing.

       Dual Month and Timeline hit the extreme form of this and were fixed
       individually by capping the VALUE at 58% and 62%. That works, but it
       is a guess per template and it only ever gets applied after someone
       notices. Flooring the LABEL is the same fix stated once and needs no
       number: `min-content` is exactly "never narrower than my longest word",
       which is the actual requirement.

       Dual Month and Timeline keep their caps and are deliberately not listed
       below: their second track is no longer `auto`, so re-declaring the
       shorthand here would silently undo two verified fixes.
   ========================================================================== */
#so-booking-widget[data-template="weekboard"] .so-bw-summary-row,
#so-booking-widget[data-template="weekboard"] .so-bw-confirmation-detail,
#so-booking-widget[data-template="dusk"] .so-bw-summary-row,
#so-booking-widget[data-template="dusk"] .so-bw-confirmation-detail,
#so-booking-widget[data-template="agenda"] .so-bw-confirmation-detail,
#so-booking-widget[data-template="centred"] .so-bw-confirmation-detail,
#so-booking-widget[data-template="tabbed"] .so-bw-confirmation-detail,
#so-booking-widget[data-template="deck"] .so-bw-summary-row,
#so-booking-widget[data-template="deck"] .so-bw-confirmation-detail,
#so-booking-widget[data-template="daypart"] .so-bw-confirmation-detail,
#so-booking-widget[data-template="frost"] .so-bw-summary-row,
#so-booking-widget[data-template="frost"] .so-bw-confirmation-detail,
#so-booking-widget[data-template="timeline"] .so-bw-summary-row,
#so-booking-widget[data-template="timeline"] .so-bw-confirmation-detail {
    /* `fit-content(45%)`, not `minmax(min-content, 1fr)`.
       min-content floors the label at its LONGEST WORD, which stops the
       one-character column but still set "Date & Time" as three stacked
       words on a 400px card. fit-content gives the label its natural width —
       one line whenever the row has room — capped at 45% so a long localised
       label cannot take the row.

       The value moves from `auto` (max-content, which is what was starving
       the label) to `1fr`, so it takes what is left and WRAPS. That is the
       same bargain the flex version of this row already strikes with
       `flex: 0 1 40%; max-width: 45%` on the label: label keeps its words,
       value gives up its line. */
    grid-template-columns: fit-content(45%) minmax(0, 1fr);
}
/* A `1fr` value track STRETCHES its cell, and most values are text so nobody
   notices — but the Status value is a pill, and a stretched pill is a
   full-width lozenge with "Confirmed" floating in the middle of it. Caught by
   looking at the fix, one screenshot after making it.

   `justify-self: end` sizes the cell to its content and keeps it on the right
   edge. Text is unaffected: a value too long for the track still fills it and
   wraps, because fit-content resolves to the track width once the content
   exceeds it. */
#so-booking-widget[data-template="weekboard"] .so-bw-summary-value,
#so-booking-widget[data-template="weekboard"] .so-bw-confirmation-detail-value,
#so-booking-widget[data-template="dusk"] .so-bw-summary-value,
#so-booking-widget[data-template="dusk"] .so-bw-confirmation-detail-value,
#so-booking-widget[data-template="agenda"] .so-bw-confirmation-detail-value,
#so-booking-widget[data-template="centred"] .so-bw-confirmation-detail-value,
#so-booking-widget[data-template="tabbed"] .so-bw-confirmation-detail-value,
#so-booking-widget[data-template="deck"] .so-bw-summary-value,
#so-booking-widget[data-template="deck"] .so-bw-confirmation-detail-value,
#so-booking-widget[data-template="daypart"] .so-bw-confirmation-detail-value,
#so-booking-widget[data-template="frost"] .so-bw-summary-value,
#so-booking-widget[data-template="frost"] .so-bw-confirmation-detail-value,
#so-booking-widget[data-template="timeline"] .so-bw-summary-value,
#so-booking-widget[data-template="timeline"] .so-bw-confirmation-detail-value {
    justify-self: end;
    max-width: 100%;
}
/* The Status row's value is not a `.so-bw-confirmation-detail-value` at all —
   it is a `.so-bw-badge--status` sitting directly in the grid, at
   `display: flex`, so it stretched to the full 298px track and printed
   "Confirmed" in the middle of a full-width lozenge. The rule above could
   never have caught it, because it names a class this row does not use.

   Stated for every template rather than the seventeen above: a badge is a
   pill sized to its own label, and there is no layout in this widget where a
   stretched one is wanted. Inert wherever the track is already max-content. */
#so-booking-widget .so-bw-confirmation-detail > .so-bw-badge,
#so-booking-widget .so-bw-summary-row > .so-bw-badge,
#so-booking-widget[data-template] .so-bw-confirmation-detail > .so-bw-badge,
#so-booking-widget[data-template] .so-bw-summary-row > .so-bw-badge {
    justify-self: end;
    align-self: center;
    width: fit-content;
    max-width: 100%;
}
/* The wide/stacked variants are one column by design — a floor there would
   fight the stack rather than help it. Restated after the block above,
   because that block names the row and this names a modifier of it at the
   same specificity. */
#so-booking-widget[data-template="deck"] .so-bw-summary-row--wide,
#so-booking-widget[data-template="frost"] .so-bw-summary-row--wide,
#so-booking-widget[data-template="dusk"] .so-bw-summary-row--wide,
#so-booking-widget[data-template="weekboard"] .so-bw-summary-row--wide {
    grid-template-columns: 1fr;
}

/* ── ...and a stacked row must release justify-self with it ──────────────
   The six above put the label on one line and the value on the next. Section
   20.1 separately gives `.so-bw-summary-value` `justify-self: end` on these
   same templates, which is right for the TWO-column rows it was written for:
   the value shrink-wraps and hugs the right edge of its own track, opposite
   its label.

   In a one-column stack that track is the whole row, so `end` puts the value
   at the far right of a full-width line directly beneath a left-aligned
   label. On the review step that rendered "First Name" hard left with "Alex"
   hard right on the line below it — 780px apart on a 838px row, reading as
   two unrelated items rather than a labelled value. Measured: label x16,
   value x795.

   Each of these templates already declares the intent — Card Deck's is
   `.so-bw-summary-row--wide .so-bw-summary-value { text-align: left }` — but
   `text-align` cannot move a box that has been shrink-wrapped and placed by
   the grid; it only aligns text INSIDE it, and the box was already at the
   right. `justify-self` is the property doing the placing, so it is the one
   that has to be released.

   (1,3,0) against 20.1's (1,2,0), so it wins on specificity and does not
   depend on staying below it. */
#so-booking-widget[data-template="deck"] .so-bw-summary-row--wide .so-bw-summary-value,
#so-booking-widget[data-template="frost"] .so-bw-summary-row--wide .so-bw-summary-value,
#so-booking-widget[data-template="dusk"] .so-bw-summary-row--wide .so-bw-summary-value,
#so-booking-widget[data-template="weekboard"] .so-bw-summary-row--wide .so-bw-summary-value {
    justify-self: start;
    text-align: left;
}

/* ── 20.1 · AND THE RULE THAT WAS UNDOING ALL OF IT ──────────────────────
   Flooring the track was not enough, and the reason is worth recording,
   because it beat three separate fixes before it was found.

   The "never truncate" block in the responsive section applies to every label
   in the widget and declares, with !important:

       white-space: normal !important;
       overflow-wrap: anywhere !important;
       word-break: break-word !important;
       min-width: 0 !important;

   Its intent is right and it should stay: text may break, boxes may not clip,
   no ellipsis may stand in for either. But those last two together are
   precisely the pair that produces a one-character column —

     · `min-width: 0` removes the flex/grid item's automatic min-content floor
     · `overflow-wrap: anywhere` makes its min-content ONE CHARACTER

   — and its own note says as much: min-width:0 is there to make the break
   "actually possible inside a row". The author chose collapsing over
   truncating. There is a third option they did not need at the time:

       `overflow-wrap: break-word` still breaks a word too long for its line,
       but the break opportunity is NOT counted when the intrinsic minimum is
       computed. Text never truncates, boxes never clip, and the item keeps a
       real floor.

   So this restates the two declarations for LABELS ONLY — short strings the
   widget writes itself. Values, titles and service names keep `anywhere`,
   which is correct for them: they carry customer and catalogue text that may
   genuinely contain an unbreakable 40-character token.

   !important is required to beat !important at equal specificity, and later
   source order carries it. This is the second documented !important in the
   per-template work and, like Frost's selected sheet, it is here because the
   rule it overrides is itself !important. */
#so-booking-widget[data-template] .so-bw-summary-label,
#so-booking-widget[data-template] .so-bw-confirmation-detail-label,
#so-booking-widget .so-bw-summary-label,
#so-booking-widget .so-bw-confirmation-detail-label {
    overflow-wrap: break-word !important;
    word-break: normal !important;
    min-width: min-content !important;
}


/* ==========================================================================
   21. ONE LEFT EDGE FOR THE WHOLE REVIEW PANEL
   --------------------------------------------------------------------------
   REPORTED from a live screenshot of the review step, and confirmed on all
   nineteen: the panel's content does not share a left edge. Measured on Split
   Canvas, but every template has the same three-way split, only with
   different numbers:

     .so-bw-summary-section-title        16px      (section content)
     .so-bw-summary-section > row        16px
     .so-bw-summary-block                 0px      (gift card, referral, pay)
     .so-bw-summary-row--total           20px      (price rows, outside a section)

   Three different starting points inside one bordered panel, stacked directly
   on top of each other. It is the most visible kind of misalignment there is
   — a reader's eye follows the left margin down a list — and no check in the
   sweep looks at it, because nothing overflows and every row is individually
   well formed.

   The cause is structural rather than anyone's mistake: the sections take
   their inset from the row component, the price rows take theirs from
   `--so-bw-summary-pad-x`, and the blocks were never given one because in the
   original single-column design the panel itself was padded. Once the panel
   stopped being padded on most templates, the three drifted apart.

   So the inset becomes ONE token, owned by the panel and read by everything
   inside it. Stated at (1,3,0) with `[data-template]` because the per-template
   summary rules that zero this padding are themselves (1,2,0)-(1,3,0), and
   later source order carries it.
   ========================================================================== */
#so-booking-widget .so-bw-summary,
#so-booking-widget[data-template] .so-bw-summary {
    --so-bw-summary-inset: 16px;
}

/* THE INSET IS OWNED BY THE ROW, SO THE ANCESTORS MUST STOP ADDING ONE.
   Frost pads its section by 16px and Coral Pop its panel by 20px, so their
   section content landed at 33px and 36px while their price rows — which sit
   outside both — landed at 16px. Same panel, two left edges, and the wider one
   moved whenever a template changed an ancestor nobody was thinking about.

   One owner is the whole point: change --so-bw-summary-inset and every row,
   title and block in the panel moves together. */
#so-booking-widget[data-template] .so-bw-summary,
#so-booking-widget[data-template] .so-bw-summary-section,
#so-booking-widget .so-bw-summary,
#so-booking-widget .so-bw-summary-section {
    padding-inline: 0;
}

/* The panel's own content rows, whichever wrapper they arrive in. */
/* NOTE THE ABSENCE OF A CHILD COMBINATOR ON THE LOOSE ROWS.
   The first version wrote `.so-bw-summary > .so-bw-summary-row` and the price
   and total rows stayed exactly where they were. They are not children of the
   panel: they sit inside `.so-bw-summary-pay`, which is `display: contents`.
   That removes the WRAPPER'S BOX from the layout, so the rows lay out as if
   they were the panel's children — but it does not change the DOM, and
   selectors read the DOM. `>` still means "child of", and their parent is
   still the wrapper.

   Hence a descendant selector that excludes the rows already handled by the
   section and block rules, which is also immune to either wrapper being
   restructured later. */
#so-booking-widget[data-template] .so-bw-summary-section-title,
#so-booking-widget[data-template] .so-bw-summary-section > .so-bw-summary-row,
#so-booking-widget[data-template] .so-bw-summary .so-bw-summary-row:not(.so-bw-summary-section .so-bw-summary-row):not(.so-bw-summary-block .so-bw-summary-row),
#so-booking-widget[data-template] .so-bw-summary-block,
#so-booking-widget[data-template] .so-bw-summary-section--member,
#so-booking-widget .so-bw-summary-section-title,
#so-booking-widget .so-bw-summary-section > .so-bw-summary-row,
#so-booking-widget .so-bw-summary .so-bw-summary-row:not(.so-bw-summary-section .so-bw-summary-row):not(.so-bw-summary-block .so-bw-summary-row),
#so-booking-widget .so-bw-summary-block,
#so-booking-widget .so-bw-summary-section--member {
    padding-inline: var(--so-bw-summary-inset);
}

/* AND THE BLEED MARGINS THAT COMPENSATED THE PADDING JUST REMOVED.
   Coral Pop pulls its total bar out with `margin: 10px -20px 0` so the tint
   reaches the panel's edges, cancelling the panel's own `padding: 4px 20px`.
   With that padding now zero, the -20px had nothing left to cancel and pushed
   the bar 20px past the panel on both sides, where `overflow: hidden` cut it:
   the row rendered as "otal ... £220.0", clipped at both ends. Measured left
   inset -4px.

   The bar still spans the full panel — that is what the tint is for — it just
   no longer needs to be dragged there, because the panel no longer pushes it
   in. Its text lands on the shared inset like every other row.

   Scoped away from rows inside a section: the two-up divider is drawn with
   `margin-inline-start: -1px` and must keep it. */
#so-booking-widget[data-template] .so-bw-summary .so-bw-summary-row:not(.so-bw-summary-section .so-bw-summary-row),
#so-booking-widget .so-bw-summary .so-bw-summary-row:not(.so-bw-summary-section .so-bw-summary-row) {
    margin-inline: 0;
}

/* DAYPART'S TRIPTYCH WAS A STAIRCASE WITH HOLES.
   Its summary is a WHAT / WHEN / WHO grid — service and duration in column 1,
   date and time in column 2, staff and location in column 3 — and that idea
   is worth keeping. What shipped was three-quarters empty: a blank top-left,
   a blank middle band, and each row on its own line.

   Cause: the rows carry `grid-column: N` but are placed in DOM order, and
   grid auto-placement is forward-only. Location (column 3) comes first in the
   markup, so it takes row 1; Service (column 1) cannot go back to row 1 and
   opens row 2; and so on down. Every row gets its own line and two thirds of
   the panel is empty.

   `dense` lets later items backfill earlier gaps, which is exactly the
   behaviour the column assignments assume. Reading order in the accessibility
   tree follows the DOM either way, and each cell is an independently labelled
   label/value pair, so nothing depends on visual sequence.

   TWO THINGS I TRIED FIRST, BOTH WORSE, BOTH RECORDED SO NOBODY REPEATS THEM:

   1 · dense + restoring the section headings. `dense` backfills across the
       WHOLE grid, and the grid spans both sections, so Name and Email from
       "Your details" filled the empty cells of "Booking details" and rendered
       ABOVE the heading they belong under. Correct-looking and wrong.

   2 · moving the grid down to each SECTION so backfill stays inside its own
       group. That made the two sections siblings in the panel's own flow, and
       they laid out SIDE BY SIDE at three columns each — nine columns across
       a 780px panel, ~80px apiece, every value set one word per line
       ("Full / Balay / age / with / Tone / r,"). Comfortably the worst state
       this screen has been in.

   So the grid stays exactly where the design put it, and the headings stay
   hidden as that design intended. `dense` alone is the whole fix: the columns
   are the grouping here — WHAT, WHEN, WHO — and every row carries its own
   label, so no heading is load-bearing. */
#so-booking-widget[data-template="daypart"] .so-bw-summary-main {
    grid-auto-flow: row dense;
}

/* EDITORIAL'S THIRD COLUMN HAD THE SAME `auto` FAULT.
   Its row is `22px minmax(0, 1fr) auto` — number, label, value — and `auto`
   on the value again resolves to max-content, so the long service name took
   the row and the label track went to zero. With 20.1 giving labels a real
   minimum, the label stopped collapsing and started printing over its own
   value: "ServFull Balayage with Toner, Gloss", "StaMaya Krishnamurthy".

   Same fix as the two-column rows in section 20, with the counter column left
   alone. The 50px label inset that follows is Editorial's numbered index and
   is deliberate — the label is meant to sit after the number, which is why
   this template reads as ragged against the others and is correct anyway. */
#so-booking-widget[data-template="editorial"] .so-bw-summary-row {
    grid-template-columns: 22px fit-content(45%) minmax(0, 1fr);
}
#so-booking-widget[data-template="editorial"] .so-bw-summary-value {
    justify-self: end;
    max-width: 100%;
}

/* A block already carries the inset, so nothing inside it may add a second.
   Scoped to the rows rather than every descendant: the gift-card input row
   and the code buttons keep their own internal spacing. */
#so-booking-widget[data-template] .so-bw-summary-block .so-bw-summary-row,
#so-booking-widget[data-template] .so-bw-summary-block .so-bw-summary-block-title,
#so-booking-widget .so-bw-summary-block .so-bw-summary-row,
#so-booking-widget .so-bw-summary-block .so-bw-summary-block-title {
    padding-inline: 0;
}

/* The member banner is a section, so it is already covered above; its inner
   rows are not, and they were sitting at the banner's own padding plus the
   row's. */
#so-booking-widget[data-template] .so-bw-summary-section--member > .so-bw-summary-row,
#so-booking-widget .so-bw-summary-section--member > .so-bw-summary-row {
    padding-inline: 0;
}
/* ==========================================================================
   22 · DATE & TIME — ONE TYPE VOICE PER TEMPLATE
   ==========================================================================

   Section 18 gave each template its own date & time GEOMETRY: circle months,
   ledgers, lattices, bubbles, pills. It did not give them their own TYPE, and
   a fingerprint of the computed typography across all nineteen showed how far
   that went:

       weekday row     9 of 16 templates identical  (Plex Mono 10.5/500/0.42)
       panel title     8 of 19 identical            (Inter 16/700/-0.16)
       group title     8 of 19 identical            (Plex Mono 10.5/500/1.47)
       end time        8 of 19 identical            (Plex Mono 13/500)

   The month title was the one surface section 18 did treat per template, and
   it was the one surface that came back nearly all-unique. That is the whole
   diagnosis: the headline was designed nineteen times and everything under it
   was left at the base value, so nineteen different grids were being read in
   the same voice.

   WHAT THIS SECTION CHANGES, AND WHAT IT DOES NOT

   Only type: family, size, weight, tracking, case. No box, no colour, no
   spacing, no state. The layouts from section 18 are untouched, and so is
   every rule that carries selection or availability.

   THE AXES, IN ORDER OF HOW MUCH A READER ACTUALLY SEES

     1. family   Inter / IBM Plex Mono / Newsreader / Space Grotesk
     2. case     uppercase micro-label vs sentence case
     3. weight   a real step (400 -> 600), not 500 -> 500
     4. size     >= 1px apart to register at all
     5. tracking last resort, and never the ONLY difference between two
                 templates — 0.2px of tracking is not a design decision, it
                 is a number that makes a diff look busy. Several pairs that
                 predate this section differed by exactly that much and read
                 as identical; they are corrected here too.

   THREE CONSTRAINTS THIS SECTION MUST NOT BREAK

     Tabular figures. A column of times is only straight because its digits
     are one width. Where a surface moves off Plex Mono it carries
     `font-variant-numeric: tabular-nums` explicitly, so Inter and Space
     Grotesk set their tabular figures rather than their proportional ones.

     Loaded weights. The @import at the top of this file fetches
     Inter 400-800, IBM Plex Mono 400/500/600, Newsreader 400/500/600 and
     Space Grotesk 400-700. Plex Mono has NO 700 — two pre-existing rules
     asked for it and were being synthesised by the browser, which is why
     Daypart's and Timeline's group titles looked slightly smeared next to
     the real 600s. Both are corrected to 600 below.

     Minimum sizes. Nothing here goes below 9.5px, which is where the
     existing micro-labels already sit, and no size drops for a surface a
     customer reads rather than scans.

   Editorial and Agenda are absent from this section on purpose. Newsreader
   and the bold indigo ledger were already a complete voice on every surface;
   there was nothing shared to break.                                        */

/* ── 22.1 · CENTRED — quiet humanist ─────────────────────────────────────
   Chrome entirely in Inter, mono reserved for numerals. Nothing shouts, which
   is the point of the circle month. */
#so-booking-widget[data-template="centred"] .so-bw-calendar-weekdays > * {
    font-family: var(--so-bw-font);
    font-size: 10.5px;
    font-weight: 600;
    letter-spacing: 0.9px;
    text-transform: uppercase;
}
#so-booking-widget[data-template="centred"] .so-bw-slot-panel-title {
    font-weight: 600;
}
#so-booking-widget[data-template="centred"] .so-bw-slot-group-title {
    font-family: var(--so-bw-font);
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 1.1px;
}
#so-booking-widget[data-template="centred"] .so-bw-slot-till {
    font-size: 12.5px;
    font-weight: 400;
}

/* ── 22.4 · HERO — airy, light ───────────────────────────────────────────
   Its 22px/300 month already sets the voice; the end time was the one surface
   still at someone else's weight. */
#so-booking-widget[data-template="hero"] .so-bw-slot-till {
    font-size: 14.5px;
    font-weight: 400;
}

/* ── 22.5 · TABBED RAIL — technical rail ─────────────────────────────────
   Mono everywhere, tracked wide, the numerals tracked too. */
#so-booking-widget[data-template="tabbed"] .so-bw-calendar-weekdays > * {
    font-size: 10px;
    font-weight: 600;
    letter-spacing: 1.8px;
    text-transform: uppercase;
}
#so-booking-widget[data-template="tabbed"] .so-bw-calendar-day-num {
    font-size: 12px;
    font-weight: 500;
    letter-spacing: 0.6px;
}
#so-booking-widget[data-template="tabbed"] .so-bw-slot-group-title {
    font-size: 10px;
    font-weight: 600;
    letter-spacing: 2px;
}
#so-booking-widget[data-template="tabbed"] .so-bw-slot-till {
    font-size: 11.5px;
    font-weight: 400;
    letter-spacing: 0.3px;
}

/* ── 22.7 · WEEK BOARD — board headings ──────────────────────────────────
   The heaviest sans in the set. A board is read at a glance, from a distance,
   and its own dow/num/slot type was already unique to it. */
#so-booking-widget[data-template="weekboard"] .so-bw-slot-panel-title {
    font-size: 13.5px;
    font-weight: 800;
    letter-spacing: 0.5px;
    text-transform: uppercase;
}
#so-booking-widget[data-template="weekboard"] .so-bw-slot-group-title {
    font-family: var(--so-bw-font);
    font-size: 10.5px;
    font-weight: 800;
    letter-spacing: 1px;
}
#so-booking-widget[data-template="weekboard"] .so-bw-slot-time {
    font-size: 12.5px;
    font-weight: 400;
    letter-spacing: 0;
}

/* ── 22.9 · CARD DECK — a pocket card ────────────────────────────────────
   Its month was byte-identical to Dialog's, and its first replacement landed
   on Frost's. Now 15/600 with open tracking — a card caption. */
#so-booking-widget[data-template="deck"] .so-bw-calendar-month {
    font-size: 15px;
    font-weight: 600;
    letter-spacing: 0.3px;
}
#so-booking-widget[data-template="deck"] .so-bw-calendar-weekdays > * {
    font-family: var(--so-bw-font);
    font-size: 10px;
    font-weight: 500;
    letter-spacing: 1.2px;
    text-transform: uppercase;
}
#so-booking-widget[data-template="deck"] .so-bw-calendar-day-num {
    font-size: 13px;
    font-weight: 600;
}
#so-booking-widget[data-template="deck"] .so-bw-slot-till {
    font-size: 13.5px;
    font-weight: 500;
}

/* ── 22.10 · WIDE CANVAS — the dashboard ─────────────────────────────────
   Its panel title was sharing Tabbed Rail's. A dashboard names its panes in
   the same small mono it uses for every other chrome label. */
#so-booking-widget[data-template="wide"] .so-bw-slot-panel-title {
    font-family: var(--so-bw-mono);
    font-size: 11.5px;
    font-weight: 600;
    letter-spacing: 1px;
    text-transform: uppercase;
}

/* ── 22.12 · DAYPART — the tinted band ───────────────────────────────────
   Its weekday row was 0.21px of tracking from Dusk's; weight and case carry
   it now. Its group title asked for Plex Mono 700, which does not exist in
   the loaded set and was being synthesised. */
#so-booking-widget[data-template="daypart"] .so-bw-calendar-weekdays > * {
    font-weight: 600;
    text-transform: uppercase;
}
#so-booking-widget[data-template="daypart"] .so-bw-slot-group-title {
    font-weight: 600;
}
/* Daypart was the last template still wearing the widget's DEFAULT panel
   title. Once the other seven that shared it had moved, the fingerprint
   scored it "unique" — which was true and meaningless: unique by being the
   only one left on the fallback, not by being designed. A template whose
   whole idea is a banded morning/afternoon/evening heading should name its
   pane in the same voice as the bands. */
#so-booking-widget[data-template="daypart"] .so-bw-slot-panel-title {
    font-size: 15px;
    font-weight: 700;
    letter-spacing: 1px;
    text-transform: uppercase;
}
#so-booking-widget[data-template="daypart"] .so-bw-slot-till {
    font-size: 12px;
    font-weight: 400;
}

/* ── 22.13 · TIMELINE — the axis ─────────────────────────────────────────
   Mono chrome to match the ruled axis, sentence-case weekdays so the row
   reads as an axis label rather than a heading. Group title corrected off
   the synthesised Plex Mono 700. */
#so-booking-widget[data-template="timeline"] .so-bw-calendar-month {
    font-family: var(--so-bw-mono);
    font-size: 13.5px;
    font-weight: 500;
    letter-spacing: 0.8px;
}
#so-booking-widget[data-template="timeline"] .so-bw-calendar-weekdays > * {
    font-size: 10px;
    font-weight: 600;
    letter-spacing: 0.5px;
    text-transform: none;
}
#so-booking-widget[data-template="timeline"] .so-bw-calendar-day-num {
    font-size: 12.5px;
    font-weight: 500;
}
#so-booking-widget[data-template="timeline"] .so-bw-slot-panel-title {
    font-family: var(--so-bw-mono);
    font-size: 13px;
    font-weight: 600;
    letter-spacing: 0.9px;
    text-transform: uppercase;
}
#so-booking-widget[data-template="timeline"] .so-bw-slot-group-title {
    font-weight: 600;
}

/* ── 22.15 · FROST — translucent and open ────────────────────────────────
   Light weights, open tracking, sans micro-labels: type with as little weight
   on the glass as the panel behind it. */
#so-booking-widget[data-template="frost"] .so-bw-calendar-weekdays > * {
    font-family: var(--so-bw-font);
    font-size: 10.5px;
    font-weight: 500;
    letter-spacing: 1.4px;
    text-transform: uppercase;
}
#so-booking-widget[data-template="frost"] .so-bw-slot-till {
    font-size: 12px;
    font-weight: 500;
}

/* ── 22.16 · DUSK — luxe, dark, wide ─────────────────────────────────────
   The lightest weights in the set at the widest tracking. On a dark canvas a
   600 micro-label blooms; 400 at 2.4px of tracking reads as intent. */
#so-booking-widget[data-template="dusk"] .so-bw-slot-group-title {
    font-size: 10.5px;
    font-weight: 400;
    letter-spacing: 2.4px;
}
#so-booking-widget[data-template="dusk"] .so-bw-slot-time {
    font-size: 13px;
    font-weight: 400;
    letter-spacing: 0.5px;
}
#so-booking-widget[data-template="dusk"] .so-bw-slot-till {
    font-size: 13px;
    font-weight: 400;
    letter-spacing: 0.6px;
}

/* ── 22.17 · CORAL POP — bold and friendly ───────────────────────────────
   The heaviest weights in the set, and the second template to set its figures
   in Inter: round pills want round friendly numerals, not a typewriter. */
#so-booking-widget[data-template="soft"] .so-bw-calendar-weekdays > * {
    font-family: var(--so-bw-font);
    font-size: 11px;
    font-weight: 800;
    letter-spacing: 0.4px;
    text-transform: uppercase;
}
#so-booking-widget[data-template="soft"] .so-bw-slot-time {
    font-family: var(--so-bw-font);
    font-size: 14px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
}
#so-booking-widget[data-template="soft"] .so-bw-slot-till {
    font-family: var(--so-bw-font);
    font-size: 13.5px;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
}

/* ==========================================================================
   24 · THE THREE INTENT-LED ARCHITECTURES
   ==========================================================================

   From the "Booking Templates 20-29" design file. The nineteen above differ
   in HOW the booking is asked — where progress lives, how much context stays
   on screen, whether the steps are sequential. These three differ in WHAT the
   customer had already decided before they arrived:

     Roster Slate   "who do I want to book with?"   → the team never leaves
                                                       the screen.
     Bundle Moss    "what is the best value?"       → the choice is a priced
                                                       shop front.
     Counter Sand   "can I book right here?"        → a tablet on a counter,
                                                       thumbs only.

   ── WHY THIS SECTION SITS BEFORE SECTION 23 ─────────────────────────────

   Section 23 is the two Studio appearance controls (Widget Size and Corner
   Radius) and it works by SCALING each template's own value at (1,3,0). A
   section appended after it would out-order those rules on equal specificity
   and the size radio would silently stop responding on exactly the three
   templates added last — the same trap section 17 fell into when it was first
   appended past the popup block. So the architectures go here, above 23, and
   both of section 23's blocks — the width tokens and the card corners — carry
   one line each for the three, and both are noted at their rules there.

   (Written out rather than cited by number on purpose: the cascade test
   locates the corner block by searching this file for its heading string, and
   a second mention of that string ANYWHERE ABOVE it silently re-anchors the
   search to here. This section then reads as part of that block and every
   architecture rule in it is reported as a defect. Do not name the sub-section
   numerically above its own heading.)

   ── THE THREE RULES THIS SECTION KEEPS ──────────────────────────────────

   1. ONLY CLASSES THE WIDGET CAN EMIT. tests/unit/widget-architecture-markup
      fails the build on a rule that styles a class booking-widget.js never
      renders, which is how the earlier "signatures" for Daypart and Timeline
      turned out to be dead code. Everything below targets the shared markup:
      the rail, the stepbar, the card grid, the slot panel, the summary rows.
      Nothing here needs new DOM.

   2. NO LITERAL HORIZONTAL INSET on .so-bw-header / .so-bw-nav / .so-bw-step.
      Vertical padding is an architecture's business; the x-inset belongs to
      --so-bw-gutter, or the header, the content column and the commit bar
      stair-step against each other at every breakpoint. Counter Sand wants a
      roomier band than stock and takes it as vertical padding only.

   3. THE POPUP GUARD ON EVERY .so-bw-card RULE. `.so-bw-sp__panel` (the staff
      profile popup) carries `.so-bw-card` to inherit its tokens, so an
      unguarded card rule repaints a component that is not part of the step at
      all. `:not(.so-bw-sp__panel)` also takes the selector to (1,3,0), which
      is what makes these bind over the shared card rules.
   ========================================================================== */


/* ==========================================================================
   24.2 · BUNDLE MOSS  (bundle-moss, layout key "bundle")
   --------------------------------------------------------------------------
   A PRICE IS A DESIGN ELEMENT.

   Every other template in the catalogue treats the price as metadata — a
   badge on a card, a line in a summary. This one treats it the way a shop
   treats it: set large, set against what it used to be, with the saving named
   underneath. That is the whole proposition, and it is why the service tile
   here is the only one in the file that leads with a photograph and gives the
   money its own line rather than a chip.

   THE STANDING STRIP. The header becomes a tinted panel holding the position
   in the plan and a filled bar. On a package booking the honest reading of
   "how far through am I" is how much of the plan is spent, and the widget's
   own progress figure is exactly that quantity — so the bar is the progress
   bar, promoted from a 6px hairline nobody reads to the thing the panel is
   built around, rather than a second invented indicator that could disagree
   with it.
   ========================================================================== */

#so-booking-widget[data-template="bundle"] .so-bw-container {
    max-width: 860px;
    margin-inline: auto;
}

/* ── The balance strip ───────────────────────────────────────────────────
   Vertical padding only. The x-inset stays on --so-bw-gutter so the strip,
   the tiles under it and the commit bar below share one left edge — see rule
   2 in this section's header. */
#so-booking-widget[data-template="bundle"] .so-bw-header {
    padding-top: 16px;
    padding-bottom: 14px;
    background: color-mix(in srgb, var(--so-bw-primary) 7%, var(--so-bw-ds-n0));
    border-bottom: 1px solid color-mix(in srgb, var(--so-bw-primary) 16%, transparent);
}
#so-booking-widget[data-template="bundle"] .so-bw-step-indicator {
    margin-bottom: 10px;
}
#so-booking-widget[data-template="bundle"] .so-bw-step-label {
    font-size: 13px;
    font-weight: 800;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}
#so-booking-widget[data-template="bundle"] .so-bw-step-count {
    font-family: var(--so-bw-mono);
    font-size: 12px;
    font-weight: 700;
    color: var(--so-bw-text);
}

/* Steps as tags, not as a connected walk. A package shop states where you are
   as a row of labels — the dots and their connecting rules are the wizard
   idiom this template is deliberately not. */
#so-booking-widget[data-template="bundle"] .so-bw-stepbar {
    flex-wrap: wrap;
    gap: 6px;
    margin: 0 0 12px;
    overflow-x: visible;
}
#so-booking-widget[data-template="bundle"] .so-bw-stepbar-item {
    flex: 0 0 auto;
    flex-direction: row;
    min-width: 0;
    gap: 0;
    padding: 5px 10px;
    border-radius: var(--so-bw-r-6);
    background: var(--so-bw-ds-n50);
}
#so-booking-widget[data-template="bundle"] .so-bw-stepbar-item::before,
#so-booking-widget[data-template="bundle"] .so-bw-stepbar-item::after {
    display: none;
}
#so-booking-widget[data-template="bundle"] .so-bw-stepbar-dot { display: none; }
#so-booking-widget[data-template="bundle"] .so-bw-stepbar-label {
    font-size: 11px;
    font-weight: 700;
    color: var(--so-bw-text-secondary);
    /* The label is the whole chip now, so it must not be clipped to a track
       width that no longer exists. */
    max-width: none;
    white-space: nowrap;
}
#so-booking-widget[data-template="bundle"] .so-bw-stepbar-item--done {
    background: color-mix(in srgb, var(--so-bw-primary) 12%, transparent);
}
#so-booking-widget[data-template="bundle"] .so-bw-stepbar-item--done .so-bw-stepbar-label {
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}
#so-booking-widget[data-template="bundle"] .so-bw-stepbar-item--active {
    background: var(--so-bw-primary);
}
#so-booking-widget[data-template="bundle"] .so-bw-stepbar-item--active .so-bw-stepbar-label {
    color: var(--so-bw-on-primary, #fff);
}

/* The bar the strip is built around. */
#so-booking-widget[data-template="bundle"] .so-bw-progress {
    height: 8px;
    border-radius: 999px;
    background: color-mix(in srgb, var(--so-bw-primary) 20%, var(--so-bw-ds-n0));
}
#so-booking-widget[data-template="bundle"] .so-bw-progress-bar {
    border-radius: 999px;
}

/* ── The shop front ──────────────────────────────────────────────────────
   Three tiles across at full width, each a photograph over a name, a
   what-is-included line and a price. 190px is the design file's own track
   floor and the width at which a two-line package name still sets cleanly. */
/* `.so-bw-step` for specificity — see the note at Roster's grid. Without it
   the 960 tier's 220px auto-FILL wins on Location and Staff, which is a
   different track size AND a different algorithm (auto-fill leaves empty
   tracks; auto-fit collapses them), so the shop and its branch list would
   break at different widths for no reason a reader could see. */
#so-booking-widget[data-template="bundle"] .so-bw-step .so-bw-card-grid,
#so-booking-widget[data-template="bundle"] .so-bw-card-grid {
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 190px), 1fr));
    gap: 12px !important;
}
#so-booking-widget[data-template="bundle"] .so-bw-card:not(.so-bw-sp__panel) {
    border-radius: var(--so-bw-ds-radius-card);
}
/* The picture is the tile's headline, and it is INSET rather than capping the
   card. A flush cover band is the stock treatment (16:10, top corners only,
   edge to edge) and it reads as a card with a picture on it. The design file
   draws a photograph sitting INSIDE the tile with the copy beneath — the
   shop-listing idiom — so it takes the card's own inset on three sides and a
   full corner of its own.

   Three properties are undoing three base declarations, not decorating:
     width: auto      base pins 100%, which with a margin would overflow the
                      card by exactly the margin. `.so-bw-card--service` is
                      `align-items: stretch !important`, so auto fills the
                      column minus the margins — which is the whole trick.
     aspect-ratio     base sets 16/10. A definite height plus a definite width
                      already wins, but stating `auto` says so rather than
                      leaving a reader to work out which declaration lost.
     border-radius    base rounds the TOP two corners only, because a cover
                      band caps the card. This one floats inside it. */
#so-booking-widget[data-template="bundle"] .so-bw-card-media {
    /* flex + centring, NOT `display: block`. A service with no picture renders
       `.so-bw-card-media--placeholder` holding a coloured initials tile, and on
       a block band that tile sits hard against the left edge of a 96px strip
       of nothing — measured, and it reads as a broken image rather than as the
       designed no-image state §17.16 describes. Centred, it is a monogram on a
       cover. */
    display: flex;
    align-items: center;
    justify-content: center;
    width: auto;
    height: 96px;
    aspect-ratio: auto;
    margin: 12px 12px 0;
    border-radius: var(--so-bw-ds-radius-ctl);
    overflow: hidden;
}
/* And the tile is sized for the band it sits in. The initials tile pins all
   six of width/height/min-/max- to --so-bw-icon-size, so this is the only
   lever — a bare width would be clamped straight back to the 48px default and
   leave a small chip adrift in a 96px cover. */
#so-booking-widget[data-template="bundle"] .so-bw-card-media-avatar {
    --so-bw-icon-size: 64px;
    --so-bw-icon-svg-size: 28px;
    --so-bw-icon-text-size: 23px;
    border-radius: var(--so-bw-ds-radius-ctl);
    box-shadow: none;
}
#so-booking-widget[data-template="bundle"] .so-bw-card-title {
    font-size: 14px;
    font-weight: 800;
    line-height: 1.3;
}
#so-booking-widget[data-template="bundle"] .so-bw-card-meta {
    font-size: 12px;
    line-height: 1.45;
}
#so-booking-widget[data-template="bundle"] .so-bw-card-meta--clamp {
    -webkit-line-clamp: 2 !important;
    line-clamp: 2 !important;
}
/* Money on its own line, at shop-front scale. The badges row is a flex strip
   of chips everywhere else; here the price badge is set as type and the
   duration keeps its chip, so the two are read in the right order. */
#so-booking-widget[data-template="bundle"] .so-bw-card-badges {
    align-items: baseline;
    gap: 8px;
    margin-top: 4px;
}
#so-booking-widget[data-template="bundle"] .so-bw-badge--price {
    padding: 0;
    border: 0;
    background: transparent;
    font-size: 19px;
    font-weight: 800;
    letter-spacing: -0.01em;
    color: var(--so-bw-text);
}
#so-booking-widget[data-template="bundle"] .so-bw-badge--duration {
    font-size: 11.5px;
    color: var(--so-bw-text-secondary);
}
/* "Included free" reads as the saving line the design file puts under the
   price, so it takes the accent rather than the stock success green. */
#so-booking-widget[data-template="bundle"] .so-bw-badge--free {
    padding: 0;
    border: 0;
    background: transparent;
    font-size: 11.5px;
    font-weight: 700;
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}

/* Locations are not merchandise — a branch list is a list. Rows, one per
   line, with the distance where the price would be on a tile. */
#so-booking-widget[data-template="bundle"] .so-bw-card-grid:not(:has(.so-bw-card--service)) {
    grid-template-columns: minmax(0, 1fr);
    gap: 8px !important;
}

/* ── Date & time ─────────────────────────────────────────────────────────
   The one step where a package booking behaves like any other booking, so it
   takes the shared treatment with the design file's 10px slot corner rather
   than a shape of its own. */
#so-booking-widget[data-template="bundle"] .so-bw-calendar {
    max-width: 460px;
}
#so-booking-widget[data-template="bundle"] .so-bw-slot-list {
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 108px), 1fr));
    gap: 8px;
}
#so-booking-widget[data-template="bundle"] .so-bw-slot-row {
    border-radius: var(--so-bw-ds-radius-ctl);
    border-width: 1.5px;
    font-weight: 700;
}

/* ── Details · two up ────────────────────────────────────────────────────
   860px holds two fields comfortably, and a package customer is usually a
   returning one filling in a form they have filled before. */
#so-booking-widget[data-template="bundle"] .so-bw-form {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 14px 18px;
    align-items: start;
}
#so-booking-widget[data-template="bundle"] .so-bw-form-group { grid-column: auto; }
#so-booking-widget[data-template="bundle"] .so-bw-form-row { display: contents; }
#so-booking-widget[data-template="bundle"] .so-bw-form-group:has(.so-bw-textarea) { grid-column: 1 / -1; }

/* ── Review · what the package covers, then what is left to pay ──────────
   The section heads take the same moss tint as the balance strip at the top,
   which is what ties the ending back to the offer that opened it.

   On the HEADS rather than on `.so-bw-summary-main`: the shared review is a
   bordered table of white cells and paints straight over a panel background.
   Measured on the first pass — the tint was declared, and the review rendered
   identically to stock. */
#so-booking-widget[data-template="bundle"] .so-bw-summary-section-title {
    background: color-mix(in srgb, var(--so-bw-primary) 10%, var(--so-bw-ds-n0));
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
    border-bottom-color: color-mix(in srgb, var(--so-bw-primary) 22%, transparent);
}
#so-booking-widget[data-template="bundle"] .so-bw-summary-label {
    font-size: 12px;
    color: var(--so-bw-text-secondary);
}
#so-booking-widget[data-template="bundle"] .so-bw-summary-value {
    font-size: 12.5px;
    font-weight: 700;
}
#so-booking-widget[data-template="bundle"] .so-bw-summary-pay {
    margin-top: 14px;
    padding-top: 6px;
    border-top: 2px solid var(--so-bw-text);
}
#so-booking-widget[data-template="bundle"] .so-bw-summary-value--price {
    font-size: 18px;
    font-weight: 800;
}

/* ── Confirmation ────────────────────────────────────────────────────────
   Sits on the same tinted panel, so the ending is visibly part of the shop
   rather than the stock white receipt. */
#so-booking-widget[data-template="bundle"] .so-bw-confirmation-details {
    max-width: 480px;
    margin-inline: auto;
    padding: 0 14px;
    border-radius: var(--so-bw-ds-radius-card);
    background: color-mix(in srgb, var(--so-bw-primary) 7%, var(--so-bw-ds-n0));
}
#so-booking-widget[data-template="bundle"] .so-bw-confirmation-detail {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, auto);
    gap: 12px;
    padding: var(--so-bw-row-pad-y) 0;
    border-bottom: 1px solid color-mix(in srgb, var(--so-bw-primary) 14%, transparent);
}
#so-booking-widget[data-template="bundle"] .so-bw-confirmation-detail:last-child { border-bottom: 0; }
#so-booking-widget[data-template="bundle"] .so-bw-confirmation-detail-value { text-align: right; }

/* ── Bundle Moss · narrower ──────────────────────────────────────────────
   Two fields side by side stop being two fields at 700px; the same value the
   other two-column forms in this file collapse at, for the same reason. */

    #so-booking-widget:where([data-cw~="700"])[data-template="bundle"] .so-bw-form{
        grid-template-columns: 1fr;
    }

    /* Phone. One tile per row: a 190px track at 380px of container is a
       photograph with a price wrapped under it in three lines. The picture
       shortens rather than being dropped — it is the tile's headline. */
    #so-booking-widget:where([data-cw~="480"])[data-template="bundle"] .so-bw-step .so-bw-card-grid,
    #so-booking-widget:where([data-cw~="480"])[data-template="bundle"] .so-bw-card-grid{
        grid-template-columns: minmax(0, 1fr);
    }
    #so-booking-widget:where([data-cw~="480"])[data-template="bundle"] .so-bw-card-media{
        height: 132px;
    }
    #so-booking-widget:where([data-cw~="480"])[data-template="bundle"] .so-bw-badge--price{
        font-size: 17px;
    }
    /* The tags KEEP WRAPPING on a phone — no `nowrap; overflow-x: auto` here,
       and that is a decision rather than an omission.

       A scrolling strip was the first instinct (it saves a row of header) and
       it is wrong for this template twice over. Measured with the audit's
       long-translation fixture, it hid a step label off the right edge in 510
       of the narrow-container cases: reachable by scrolling, but a customer
       does not know a row of tags scrolls, and the whole job of this strip is
       to say how much of the plan is left. Half a plan visible is worse than
       two rows of chips. The design file wraps them too.

       Counter Sand DOES scroll its chips, for the opposite reason: they sit on
       a fixed ink band on a kiosk screen, where a second row costs height the
       question needs and a walk-in is standing in front of the tablet anyway. */
    #so-booking-widget:where([data-cw~="480"])[data-template="bundle"] .so-bw-stepbar-label{
        font-size: 10.5px;
    }


/* ==========================================================================
   24.3 · COUNTER SAND  (counter-sand, layout key "counter")
   --------------------------------------------------------------------------
   A TABLET ON A COUNTER, AND ONE THUMB.

   The design system's 44px minimum is a floor for a phone held in the hand.
   It is not enough for a device standing at arm's length on a reception desk
   being tapped by someone who did not choose to be there — so this template
   raises every target rather than adding a decoration:

     choice tiles   96px      (44px minimum → more than double)
     time buttons   72px
     the one input  64px
     the commit bar 64px

   THE INK BAND. A kiosk screen is on all day, and a white one at desk height
   is a lamp. So the canvas is warm sand rather than white and the header is a
   dark band — which is also what makes the screen legible across a room, the
   distance a walk-in actually reads it from.

   THE BAND IS DERIVED, NOT LITERAL. `--so-bw-counter-ink` is mixed from
   --so-bw-primary, so an admin who changes the accent in the Studio gets a
   band that follows it. A hardcoded #2b241a would have made this the one
   template whose header ignores the colour picker.

   ONLY ONE THING IS TYPED. The details step is a phone number at 22px and
   nothing else is asked at kiosk scale, because a full intake form on a
   counter tablet is where a walk-in gives up and asks the receptionist.
   ========================================================================== */

#so-booking-widget[data-template="counter"] .so-bw-container {
    max-width: 900px;
    margin-inline: auto;
    --so-bw-counter-ink: color-mix(in srgb, var(--so-bw-primary) 26%, #171208);
    --so-bw-counter-sand: color-mix(in srgb, var(--so-bw-primary) 6%, #fdfbf6);
    --so-bw-counter-line: color-mix(in srgb, var(--so-bw-primary) 24%, #e7dfcd);
    background: var(--so-bw-counter-sand);
}
#so-booking-widget[data-template="counter"] .so-bw-body {
    background: var(--so-bw-counter-sand);
}

/* ── A taller pane, because everything in it is at kiosk scale ───────────
   The shared bound is `min(760px, 100dvh - 112px)`, set once for all
   twenty-three. It is sized for a template whose targets are the 44px
   minimum — and this one raised every target instead of decorating, so it
   pays for that height twice: once in chrome, once in each row.

   MEASURED at 760px. The ink band is 20 + 18 of padding around a 20px step
   label and a chip row 12px under it — ~104px. The commit bar is a 64px
   button inside 16 + 16 — ~96px. That is 200px of chrome before a tile, the
   same arithmetic the popup/floating note at the end of this section already
   states ("64px commit bar plus its 20px band is 150px of chrome"). Of the
   560px left, the instruction line, the category head and the body's own
   padding take ~100 more, so the choice step gets ~460px — four 96px tiles
   plus their 12px gutters and a quarter of a fifth. On the portrait grid the
   staff step draws ~170px tiles and gets two and a half.

   900px, not "as tall as it will go". The point of a bounded card is that the
   widget is a pane on someone's page rather than the page itself. 140px is
   one more tile row and the top of the row after it — five full choices with
   a visible sliver of the sixth, which is the "there is more below this" cue
   a list cut exactly at a row boundary loses. Between Dusk's 840 (26.4c) and
   Frost's 980 (28.1), which is where this template sits: more chrome than
   Dusk, smaller cards than Frost.

   THE SECOND TERM IS UNCHANGED AT 112px AND IS WHAT KEEPS THIS SAFE. Frost
   cuts its own to 64 because its sheets stand 12px in from the container
   edge; Counter Sand's canvas is edge-to-edge sand with the band hard against
   the top, so none of the shell's clearance is already inside the template.
   `100dvh - 112px` therefore still wins on any window shorter than ~1012px —
   a 1024x768 landscape tablet and every laptop render exactly what they
   rendered before. What DOES clear 1012px is the tablet stood on its end at
   the counter (820x1180, 768x1024), which is the device this template is
   named for and the one case where the extra height is there to take. `dvh`
   rather than `vh` so a phone measures the collapsed-URL-bar viewport.

   NOT in the two shells. 26.10 caps this same container at the popup's 90vh
   and the floating panel's 80vh at the same (1,3,0), later in the file, so it
   wins there on source order — which is correct and deliberate: inside a
   shell the card must not outgrow the box around it, or the shell grows a
   second scrollbar and the docked strip scrolls out of sight.

   (1,3,0) plus !important, for the reason Dusk's carries them: the BOUNDED
   SHELL block that sets the shared cap sits far below this section, so this
   has to outrank it on specificity rather than on source order, and that rule
   carries !important of its own. */
#so-booking-widget[data-template="counter"][data-template-id] .so-bw-container {
    max-height: min(900px, calc(100dvh - 112px)) !important;
}

/* ── The ink band ────────────────────────────────────────────────────────
   Vertical padding only; the inset is the shared gutter (rule 2). */
#so-booking-widget[data-template="counter"] .so-bw-header {
    padding-top: 20px;
    padding-bottom: 18px;
    background: var(--so-bw-counter-ink);
    border-bottom: 0;
}
#so-booking-widget[data-template="counter"] .so-bw-brand__title { color: #fff; }
#so-booking-widget[data-template="counter"] .so-bw-brand__subtitle { color: rgba(255, 255, 255, 0.72); }
#so-booking-widget[data-template="counter"] .so-bw-step-indicator {
    align-items: center;
    gap: 8px 16px;
    margin-bottom: 0;
}
#so-booking-widget[data-template="counter"] .so-bw-step-label {
    font-size: 20px;
    font-weight: 800;
    letter-spacing: -0.01em;
    color: #fff;
}
#so-booking-widget[data-template="counter"] .so-bw-step-count {
    font-family: var(--so-bw-mono);
    font-size: 13px;
    font-weight: 700;
    color: rgba(255, 255, 255, 0.72);
}

/* Steps as chips ON the band, pushed to its right edge — the design file's
   own arrangement, and the one that leaves the question itself alone on the
   left where it is read first. */
#so-booking-widget[data-template="counter"] .so-bw-stepbar {
    flex-wrap: wrap;
    justify-content: flex-end;
    gap: 6px;
    margin: 12px 0 0;
    overflow-x: visible;
}
#so-booking-widget[data-template="counter"] .so-bw-stepbar-item {
    flex: 0 0 auto;
    flex-direction: row;
    min-width: 0;
    gap: 0;
    padding: 7px 12px;
    border-radius: var(--so-bw-ds-radius-chip);
    background: rgba(255, 255, 255, 0.1);
}
#so-booking-widget[data-template="counter"] .so-bw-stepbar-item::before,
#so-booking-widget[data-template="counter"] .so-bw-stepbar-item::after {
    display: none;
}
#so-booking-widget[data-template="counter"] .so-bw-stepbar-dot { display: none; }
#so-booking-widget[data-template="counter"] .so-bw-stepbar-label {
    font-size: 12px;
    font-weight: 700;
    max-width: none;
    white-space: nowrap;
    color: rgba(255, 255, 255, 0.66);
}
#so-booking-widget[data-template="counter"] .so-bw-stepbar-item--done .so-bw-stepbar-label { color: rgba(255, 255, 255, 0.86); }
#so-booking-widget[data-template="counter"] .so-bw-stepbar-item--active {
    background: #fff;
}
#so-booking-widget[data-template="counter"] .so-bw-stepbar-item--active .so-bw-stepbar-label {
    color: var(--so-bw-counter-ink);
}
/* The hairline progress bar is unreadable from across a room and says nothing
   the chips do not. */
#so-booking-widget[data-template="counter"] .so-bw-progress { display: none; }

/* ── One question, at kiosk scale ────────────────────────────────────────
   The instruction line and the category heads both step up, but NOT to the
   24px the design file gives its question. The question here is the band's own
   step label, and `.so-bw-group-title` is not that — it is the service
   CATEGORY ("Colour", "Cutting"). Set at 24px it out-shouted the title in the
   band above it and the screen had two headlines competing. 17px is a category
   at kiosk scale; the band keeps the question. */
#so-booking-widget[data-template="counter"] .so-bw-step-desc {
    font-size: 15px;
    line-height: 1.5;
    color: color-mix(in srgb, var(--so-bw-text) 68%, transparent);
}
#so-booking-widget[data-template="counter"] .so-bw-group-title {
    font-size: 17px;
    font-weight: 800;
    letter-spacing: -0.01em;
}

/* ── The tiles ───────────────────────────────────────────────────────────
   96px tall, 2px edged, 14px cornered. The 2px border is not decoration: at
   arm's length a 1px hairline on sand is not a visible edge, and a tile with
   no visible edge is not a visible target. */
/* `.so-bw-step` for specificity — see the note at Roster's grid. Here the
   track size the 960 tier would impose happens to match, but the ALGORITHM
   does not: auto-fill leaves empty tracks standing, so a two-service kiosk
   would render two 220px tiles beside two 220px gaps instead of two tiles
   sharing the row. On a screen built around large targets that is the
   difference between a button and a postage stamp. */
#so-booking-widget[data-template="counter"] .so-bw-step .so-bw-card-grid,
#so-booking-widget[data-template="counter"] .so-bw-card-grid {
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 220px), 1fr));
    gap: 12px !important;
}
#so-booking-widget[data-template="counter"] .so-bw-card:not(.so-bw-sp__panel) {
    min-height: 96px;
    border-width: 2px;
    border-radius: var(--so-bw-r-14);
    border-color: var(--so-bw-counter-line);
    background: #fff;
}
#so-booking-widget[data-template="counter"] .so-bw-card--selected:not(.so-bw-sp__panel) {
    border-color: var(--so-bw-primary);
}
#so-booking-widget[data-template="counter"] .so-bw-card-icon,
#so-booking-widget[data-template="counter"] .so-bw-card-icon--avatar {
    width: 68px;
    height: 68px;
    flex-shrink: 0;
}

/* ── Staff and locations are PORTRAITS, not rows ─────────────────────────
   `.so-bw-card-grid--scroll` is the widget's own marker for the two pickers
   that choose a PERSON or a PLACE (locations at step 1, staff at step 3) —
   never services, which keep the row above.

   As rows in a 220px track, a real name broke mid-word ("Maya Krishnamurth /
   y-Whitfield") beside a 68px circle with nothing under it. The design file
   draws this step as a portrait: the face centred and large, the name beneath
   it, the whole tile one target. That is also the right shape for a kiosk —
   a walk-in points at a face, and a centred column is legible from further
   away than a left-aligned row of the same height.

   170px tracks rather than 220: with the name under the circle instead of
   beside it, the tile needs height, not width, and the narrower track fits
   four across a landscape tablet.

   `.so-bw-step` in the selector for specificity — see the note at the card
   grid above; the 960 tier claims this exact grid at (1,3,0). */
#so-booking-widget[data-template="counter"] .so-bw-step .so-bw-card-grid--scroll {
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 170px), 1fr));
}
#so-booking-widget[data-template="counter"] .so-bw-card-grid--scroll .so-bw-card:not(.so-bw-sp__panel) {
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 10px;
    padding: 16px 12px;
    text-align: center;
}
#so-booking-widget[data-template="counter"] .so-bw-card-grid--scroll .so-bw-card-icon--avatar {
    width: 84px;
    height: 84px;
    --so-bw-icon-size: 84px;
    --so-bw-icon-svg-size: 36px;
    --so-bw-icon-text-size: 30px;
}
/* The bio is a row's second line. Under a portrait it is a paragraph, and a
   kiosk tile is not where a fifteen-year career gets read — the name is the
   choice. Two lines at most, centred with the name. */
#so-booking-widget[data-template="counter"] .so-bw-card-grid--scroll .so-bw-card-meta {
    font-size: 13px;
}
#so-booking-widget[data-template="counter"] .so-bw-card-title {
    font-size: 18px;
    font-weight: 800;
    line-height: 1.25;
}
#so-booking-widget[data-template="counter"] .so-bw-card-meta {
    font-size: 14px;
    line-height: 1.4;
}
/* One line of description. A walk-in reads the name and the price; three
   lines of prose at 14px is most of a 96px tile and none of the decision. */
#so-booking-widget[data-template="counter"] .so-bw-card-meta--clamp {
    -webkit-line-clamp: 1 !important;
    line-clamp: 1 !important;
}
#so-booking-widget[data-template="counter"] .so-bw-badge {
    font-size: 13px;
    font-weight: 700;
}
/* ── The service picture, BESIDE the name ────────────────────────────────
   §17.16 picks a treatment by the shape of the choice step: a cover band for
   grid templates, a circular avatar for list templates. Counter Sand is
   neither, and taking the default (cover) would be wrong for the reason that
   section gives — the problem is never the picture, it is the SHAPE of it. A
   16:10 band across a 280px tile is 175px of photograph above a name, which
   turns a 96px target into a 300px poster and puts three choices below the
   fold of a counter tablet.

   So it takes §17.16's MECHANISM A — the card re-laid as a grid with named
   areas, which expresses "media beside body, extras full width beneath"
   without arguing with `flex-direction: column !important` — at kiosk size,
   and squared off rather than circular. That is the design file's own
   arrangement: a rounded photograph on the left, the name and the price
   stacked to its right, the whole tile one target.

   Not added to §17.16's own selector lists on purpose. This section is later
   in source order, so equal-specificity rules here win anyway, and threading
   a fourth size through eleven-selector lists to change one value is how
   those lists become impossible to read. */
#so-booking-widget[data-template="counter"] .so-bw-card--service:not(.so-bw-sp__panel) {
    display: grid !important;
    grid-template-columns: auto minmax(0, 1fr);
    grid-template-areas:
        "media body"
        "extras extras"
        "panel panel";
    align-items: center !important;
    gap: 0 !important;
}
#so-booking-widget[data-template="counter"] .so-bw-card-media {
    grid-area: media;
    display: flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
    width: 88px;
    height: 88px;
    /* The base band is 16:10 and would fight a square. */
    aspect-ratio: 1;
    /* The service card's padding is forced to 0 at (1,2,0), so the inset is
       the media's own to carry. The gap to the text comes from
       .so-bw-card-top's left padding, which is already there. */
    margin: 14px 0 14px 14px;
    border-radius: var(--so-bw-r-12);
    overflow: hidden;
}
/* The initials tile inside it has to grow with the frame: it pins all six of
   width/height/min-/max- to --so-bw-icon-size, so a bare height would be
   clamped straight back to 48px and leave a small chip floating in an 88px
   box. Same trap §17.16 documents for the avatar treatment. */
#so-booking-widget[data-template="counter"] .so-bw-card-media-avatar {
    --so-bw-icon-size: 88px;
    --so-bw-icon-svg-size: 40px;
    --so-bw-icon-text-size: 32px;
    border-radius: var(--so-bw-r-12);
    box-shadow: none;
}
#so-booking-widget[data-template="counter"] .so-bw-card-top { grid-area: body; }
#so-booking-widget[data-template="counter"] .so-bw-card-extras-toggle { grid-area: extras; }
#so-booking-widget[data-template="counter"] .so-bw-card-extras { grid-area: panel; }
/* Carousel arrows and dots are cover-band controls. Inside an 88px square
   they cover the picture they page through, and they are a precision target
   on a screen whose whole premise is that nothing needs precision. The
   primary image shows here; the rest stay reachable from the description. */
#so-booking-widget[data-template="counter"] .so-bw-card-media-nav,
#so-booking-widget[data-template="counter"] .so-bw-card-media-dots {
    display: none;
}

/* ── "View profile" · REINSTATED, at this template's scale ───────────────
   This was `display: none`, on the reasoning that nobody reads a profile on a
   counter tablet and that the popup it opens is the one surface in the widget
   not built at this scale. The second half of that was true and is answered
   below; the first half was a guess, and it cost the walk-in the only thing on
   the staff step that says WHO these people are. The portrait tile carries a
   name, a face and at most two lines of bio — for "which of these four should
   cut my hair", that is the whole basis for the choice, and the template that
   removed the answer is the one whose entire premise is that the person in
   front of it should not have to ask a receptionist.

   NOT the 12px text link the other templates use. That size is calibrated for
   a card read at phone distance; on a tile stood at arm's length it is not
   legible and — sitting INSIDE another target — not safely hittable either.
   So it takes the shape the rest of this template uses for a secondary
   control: a bordered chip, 48px tall, on the tile's own white. Below the 64px
   commit bar and the 96px tile, which is the ranking it should have, and above
   the base's 32px, which is the floor the design system allows a control that
   sits inside another one.

   Centred, because on the staff and location steps the tile is a portrait
   (`--scroll`, above) and `text-align: center` there would otherwise leave
   this chip ragged under a centred name. The `@click.stop.prevent` in the
   markup is what keeps a tap here from also selecting the person. */
#so-booking-widget[data-template="counter"] .so-bw-card-profile {
    min-height: 48px;
    margin-top: 10px;
    padding: 10px 16px;
    border: 2px solid var(--so-bw-counter-line);
    border-radius: var(--so-bw-r-12);
    background: transparent;
    font-size: 15px;
    font-weight: 700;
    gap: 8px;
}
#so-booking-widget[data-template="counter"] .so-bw-card-profile svg {
    width: 18px;
    height: 18px;
}

/* ── …and the popup it opens, built at that scale too ────────────────────
   The panel is drawn once for all twenty-three templates at reading-on-a-
   phone sizes: a 420px sheet, an 18px name, 13.5px body copy, 10.5px section
   labels and a 32px close button. Opened from a tablet on a counter that is
   the surface the old comment was right about — so the sizes step up here
   the same way every other primitive in this section does, and nothing about
   the panel's STRUCTURE changes: same hero, same one scroll region, same
   pinned footer, same tokens.

   The footer buttons are already correct and are not repeated: they are the
   template's own `.so-bw-btn` primitive, which this section has at 64px.

   Every selector is (1,2,0) or better against the popup's own (1,1,0)/(1,2,0)
   base, which lives far below this section — so these win on specificity
   rather than on source order, and stay scoped to this one template. */
#so-booking-widget[data-template="counter"] .so-bw-card.so-bw-sp__panel {
    max-width: 520px;
}
#so-booking-widget[data-template="counter"] .so-bw-sp__head {
    gap: 12px;
    padding: 26px 22px 20px;
}
/* Width and height LITERALLY, not just the token. The base rule enlarges the
   hero by re-declaring `--so-bw-icon-size` at (1,3,0), which works everywhere
   except here: this template pins `.so-bw-card-icon--avatar` to 68px with real
   width/height at (1,2,0), and those beat the base primitive's
   `width: var(--so-bw-icon-size)` at (0,1,0) — so the hero has been rendering
   at tile size on this template, smaller than the 84px it gets on the other
   twenty-two. The token still has to be set as well, because the four
   min-/max- bounds are pinned to it and would otherwise clamp this straight
   back. (1,4,0), which clears both. */
#so-booking-widget[data-template="counter"] .so-bw-sp__head .so-bw-card-icon.so-bw-sp__avatar {
    --so-bw-icon-size: 104px;
    width: 104px;
    height: 104px;
    font-size: 34px;
}
#so-booking-widget[data-template="counter"] .so-bw-sp__head .so-bw-card-icon.so-bw-sp__avatar svg {
    width: 42px;
    height: 42px;
}
/* The close button is the only way out of a modal on a device with no keyboard
   and no back gesture, so it is the one control here that gets a full 64px —
   the commit bar's size, not a chip's. The ident's side padding tracks it, or
   a centred name is centred against the space the button leaves rather than
   against the panel. */
#so-booking-widget[data-template="counter"] .so-bw-sp__x {
    top: 12px;
    right: 12px;
    width: 64px;
    height: 64px;
    border-radius: var(--so-bw-r-14);
}
#so-booking-widget[data-template="counter"] .so-bw-sp__x svg {
    width: 24px;
    height: 24px;
}
#so-booking-widget[data-template="counter"] .so-bw-sp__ident {
    padding: 0 68px;
}
#so-booking-widget[data-template="counter"] .so-bw-sp__name {
    font-size: 24px;
    font-weight: 800;
    letter-spacing: -0.01em;
}
#so-booking-widget[data-template="counter"] .so-bw-sp__role {
    margin-top: 5px;
    font-size: 16px;
}
#so-booking-widget[data-template="counter"] .so-bw-sp__rating {
    margin-top: 8px;
    font-size: 16px;
}
#so-booking-widget[data-template="counter"] .so-bw-sp__stars svg {
    width: 17px;
    height: 17px;
}
#so-booking-widget[data-template="counter"] .so-bw-sp__body {
    padding: 18px 22px 20px;
}
#so-booking-widget[data-template="counter"] .so-bw-sp__sec + .so-bw-sp__sec {
    margin-top: 18px;
}
#so-booking-widget[data-template="counter"] .so-bw-sp__label {
    margin-bottom: 7px;
    font-size: 12px;
}
#so-booking-widget[data-template="counter"] .so-bw-sp__text {
    font-size: 16px;
    line-height: 1.55;
}
#so-booking-widget[data-template="counter"] .so-bw-sp__hours {
    gap: 6px 16px;
    font-size: 16px;
}
#so-booking-widget[data-template="counter"] .so-bw-sp__time {
    font-size: 15px;
}
#so-booking-widget[data-template="counter"] .so-bw-sp__contact {
    gap: 8px;
    font-size: 16px;
}
#so-booking-widget[data-template="counter"] .so-bw-sp__chips {
    gap: 8px;
}
#so-booking-widget[data-template="counter"] .so-bw-sp__chip {
    padding: 6px 12px;
    font-size: 14px;
}
/* "View full schedule" reveals detail in place, so it stays a text affordance
   rather than becoming a second button competing with Select Staff — but it is
   still a thumb target, so it takes the same 48px box as the card's trigger. */
#so-booking-widget[data-template="counter"] .so-bw-sp__toggle {
    min-height: 48px;
    margin-top: 10px;
    padding: 8px 0;
    font-size: 16px;
    gap: 8px;
}
#so-booking-widget[data-template="counter"] .so-bw-sp__toggle svg {
    width: 18px;
    height: 18px;
}
#so-booking-widget[data-template="counter"] .so-bw-sp__foot {
    gap: 12px;
    padding: 14px 22px;
}
#so-booking-widget[data-template="counter"] .so-bw-sp__state {
    padding: 30px 22px;
    font-size: 16px;
}

/* The search field belongs to a long catalogue browsed on a phone. A kiosk
   list is short and typing is what this template is avoiding. */
#so-booking-widget[data-template="counter"] .so-bw-search { display: none; }

/* ── Times · six big buttons ─────────────────────────────────────────────
   The stock slot pill is 44px and sized for a column beside a month. Here the
   openings ARE the screen, so they take the design file's 72px block and 19px
   figure — a size that can be hit without looking at the finger. */
#so-booking-widget[data-template="counter"] .so-bw-datetime {
    grid-template-columns: 1fr;
    gap: 16px;
}
#so-booking-widget[data-template="counter"] .so-bw-calendar {
    max-width: 520px;
}
#so-booking-widget[data-template="counter"] .so-bw-calendar-day {
    min-height: 52px;
    font-size: 16px;
    font-weight: 700;
}
/* 150px, not 130. The label is a RANGE at 19px ("1:30 PM to 2:30 PM"), which
   at 130 wraps to two lines and makes a 72px block a 96px one — the row of
   buttons stops being a row of the same object. */
#so-booking-widget[data-template="counter"] .so-bw-slot-list {
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 150px), 1fr));
    gap: 12px;
}
#so-booking-widget[data-template="counter"] .so-bw-slot-row {
    min-height: 72px;
    border-width: 2px;
    border-color: var(--so-bw-counter-line);
    border-radius: var(--so-bw-r-14);
    background: #fff;
    font-size: 19px;
    font-weight: 800;
}
#so-booking-widget[data-template="counter"] .so-bw-slot-time { font-size: 19px; }
#so-booking-widget[data-template="counter"] .so-bw-slot-till { font-size: 15px; }
#so-booking-widget[data-template="counter"] .so-bw-slot-group-title {
    font-size: 12.5px;
    letter-spacing: 0.12em;
}

/* ── The one thing that is typed ─────────────────────────────────────────
   64px tall, 22px figures. A single column even at 900px: two fields side by
   side halves the target width for no gain when the form is this short. */
#so-booking-widget[data-template="counter"] .so-bw-form {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: 16px;
}
#so-booking-widget[data-template="counter"] .so-bw-label {
    font-size: 14px;
    font-weight: 700;
}
#so-booking-widget[data-template="counter"] .so-bw-input,
#so-booking-widget[data-template="counter"] .so-bw-select,
#so-booking-widget[data-template="counter"] .so-bw-phone-number,
#so-booking-widget[data-template="counter"] .so-bw-phone-dial {
    min-height: 64px;
    border-width: 2px;
    border-color: var(--so-bw-counter-line);
    border-radius: var(--so-bw-r-14);
    font-size: 20px;
    font-weight: 700;
}
#so-booking-widget[data-template="counter"] .so-bw-textarea {
    border-width: 2px;
    border-color: var(--so-bw-counter-line);
    border-radius: var(--so-bw-r-14);
    font-size: 17px;
}

/* ── Review · a bill read at arm's length ────────────────────────────────
   Everything a step larger, with the total at 26px — the figure a customer
   checks before they walk to the till. The section heads take the sand so the
   panel's structure is legible from the same distance as its numbers. */
#so-booking-widget[data-template="counter"] .so-bw-summary-section-title {
    background: color-mix(in srgb, var(--so-bw-primary) 10%, #f6f1e6);
    color: var(--so-bw-text);
    border-bottom-color: var(--so-bw-counter-line);
    font-size: 12.5px;
}
#so-booking-widget[data-template="counter"] .so-bw-summary-row {
    padding: 12px 0;
    border-bottom: 1px solid var(--so-bw-counter-line);
}
#so-booking-widget[data-template="counter"] .so-bw-summary-label { font-size: 15px; }
#so-booking-widget[data-template="counter"] .so-bw-summary-value {
    font-size: 16px;
    font-weight: 800;
}
#so-booking-widget[data-template="counter"] .so-bw-summary-pay {
    margin-top: 14px;
    padding-top: 8px;
    border-top: 2px solid var(--so-bw-text);
}
#so-booking-widget[data-template="counter"] .so-bw-summary-row--total .so-bw-summary-value,
#so-booking-widget[data-template="counter"] .so-bw-summary-value--price {
    font-size: 26px;
    font-weight: 800;
    letter-spacing: -0.02em;
}

/* ── The commit bar ──────────────────────────────────────────────────────
   64px buttons on a sand-tinted band, so the bar is visibly a separate
   surface from the question above it. Vertical padding only (rule 2). */
#so-booking-widget[data-template="counter"] .so-bw-nav {
    padding-top: 16px;
    padding-bottom: 16px;
    gap: 14px;
    background: color-mix(in srgb, var(--so-bw-primary) 12%, #f6f1e6);
    border-top: 1px solid var(--so-bw-counter-line);
}
#so-booking-widget[data-template="counter"] .so-bw-btn {
    min-height: 64px;
    border-radius: var(--so-bw-r-14);
    font-size: 18px;
    font-weight: 800;
}
#so-booking-widget[data-template="counter"] .so-bw-btn--primary,
#so-booking-widget[data-template="counter"] .so-bw-btn--confirm {
    flex: 1 1 auto;
    font-size: 20px;
}
#so-booking-widget[data-template="counter"] .so-bw-btn--secondary {
    border-width: 2px;
    border-color: color-mix(in srgb, var(--so-bw-primary) 34%, #c9bb9e);
}

/* ── Confirmation · read from a step back ────────────────────────────────
   The kiosk's ending is a message for someone already standing up, so it is
   set larger than any other template's and stated in one line. */
#so-booking-widget[data-template="counter"] .so-bw-confirmation-title {
    font-size: 26px;
    font-weight: 800;
    letter-spacing: -0.02em;
}
#so-booking-widget[data-template="counter"] .so-bw-confirmation-text {
    font-size: 17px;
    line-height: 1.5;
}
#so-booking-widget[data-template="counter"] .so-bw-confirmation-detail {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, auto);
    gap: 14px;
    padding: 12px 0;
    border-bottom: 1px solid var(--so-bw-counter-line);
}
#so-booking-widget[data-template="counter"] .so-bw-confirmation-detail:last-child { border-bottom: 0; }
#so-booking-widget[data-template="counter"] .so-bw-confirmation-detail-label { font-size: 15px; }
#so-booking-widget[data-template="counter"] .so-bw-confirmation-detail-value {
    font-size: 16px;
    font-weight: 800;
    text-align: right;
}

/* ── Counter Sand · the small tablet and the phone ───────────────────────
   A kiosk is a landscape tablet, but the same widget is served to whoever
   opens the page — so the scale steps down twice rather than snapping to the
   phone treatment. Targets never drop below the tiles' own floor; only the
   type and the tracks move.

   768 is the portrait-tablet boundary the rest of this file uses. */

    #so-booking-widget:where([data-cw~="768"])[data-template="counter"] .so-bw-step-label{
        font-size: 18px;
    }
    #so-booking-widget:where([data-cw~="768"])[data-template="counter"] .so-bw-group-title{
        font-size: 21px;
    }
    #so-booking-widget:where([data-cw~="768"])[data-template="counter"] .so-bw-step .so-bw-card-grid,
    #so-booking-widget:where([data-cw~="768"])[data-template="counter"] .so-bw-card-grid{
        grid-template-columns: repeat(auto-fit, minmax(min(100%, 190px), 1fr));
    }
    #so-booking-widget:where([data-cw~="768"])[data-template="counter"] .so-bw-slot-list{
        grid-template-columns: repeat(auto-fit, minmax(min(100%, 110px), 1fr));
    }

    /* Phone. The band's chips would take three rows of a 380px screen, so
       they scroll on one; the tiles go one per row and keep their 96px. */
    #so-booking-widget:where([data-cw~="480"])[data-template="counter"] .so-bw-stepbar{
        flex-wrap: nowrap;
        justify-content: flex-start;
        overflow-x: auto;
    }
    #so-booking-widget:where([data-cw~="480"])[data-template="counter"] .so-bw-step .so-bw-card-grid,
    #so-booking-widget:where([data-cw~="480"])[data-template="counter"] .so-bw-card-grid{
        grid-template-columns: minmax(0, 1fr);
    }
    #so-booking-widget:where([data-cw~="480"])[data-template="counter"] .so-bw-card-icon,
    #so-booking-widget:where([data-cw~="480"])[data-template="counter"] .so-bw-card-icon--avatar{
        width: 56px;
        height: 56px;
    }
    #so-booking-widget:where([data-cw~="480"])[data-template="counter"] .so-bw-card-title{
        font-size: 16px;
    }
    #so-booking-widget:where([data-cw~="480"])[data-template="counter"] .so-bw-slot-row{
        min-height: 64px;
        font-size: 17px;
    }
    #so-booking-widget:where([data-cw~="480"])[data-template="counter"] .so-bw-btn{
        min-height: 56px;
        font-size: 16px;
    }
    #so-booking-widget:where([data-cw~="480"])[data-template="counter"] .so-bw-btn--primary,
    #so-booking-widget:where([data-cw~="480"])[data-template="counter"] .so-bw-btn--confirm{
        font-size: 17px;
    }
    #so-booking-widget:where([data-cw~="480"])[data-template="counter"] .so-bw-input,
    #so-booking-widget:where([data-cw~="480"])[data-template="counter"] .so-bw-select,
    #so-booking-widget:where([data-cw~="480"])[data-template="counter"] .so-bw-phone-number,
    #so-booking-widget:where([data-cw~="480"])[data-template="counter"] .so-bw-phone-dial{
        min-height: 56px;
        font-size: 18px;
    }
    #so-booking-widget:where([data-cw~="480"])[data-template="counter"] .so-bw-confirmation-title{
        font-size: 22px;
    }


/* ==========================================================================
   24.4 · THE THREE IN THE THREE DISPLAY MODES
   --------------------------------------------------------------------------
   Inline / Popup / Floating Button are `data-position` on the root and are
   architecture-agnostic by construction: the popup and floating shells wrap
   .so-bw-container rather than replacing it, and the container-width system
   (data-cw) measures the box the widget was actually given, so a 400px
   floating panel already gets the phone tier without a viewport ever being
   consulted.

   Three things do NOT come free, and they are the three below. Each is the
   same fix the existing architectures carry for the same reason — the notes
   are at the rules rather than restated here.
   ========================================================================== */

/* 1 · An edge-to-edge shell inside a panel. Roster's card is a grid with a
   coloured column running to its own border, and the popup / floating shells
   clip their contents to a rounded corner. Without `overflow: hidden` on the
   container the roster column's square top-left corner paints over the
   panel's rounded one. Stated for all three positions, because the inline
   card has the same corner. */
#so-booking-widget[data-template="counter"] .so-bw-container {
    overflow: hidden;
}

/* 2 · Roster in a narrow shell. A popup dialog is capped well under the
   940px the two columns were designed for, so the collapse above is what
   actually renders there — which is correct and needs nothing. What it does
   need is for the roster strip to keep the panel's inset: `padding: 12px
   var(--so-bw-gutter)` already tracks the tier, so this is a no-op by
   construction and is recorded here only so the next person does not add a
   literal. */

/* 3 · Counter Sand's ink band against a white panel. The popup and floating
   shells paint `background: #fff` on their own wrapper (see the popup
   section, which must stay last in this file). The band is opaque, so it
   covers that — but the SAND body underneath is a tint, and on the floating
   panel's white it read a half-shade lighter than it does inline. Painting
   the sand on the container rather than only the body settles it in one
   place, which is why the token sits on .so-bw-container above and the body
   merely repeats it. */
/* …except once collapsed, where there is no reserved column to size. (1,3,0)
   against the pair above at (1,2,0) — a media query would add nothing, so the
   specificity has to come from the selector. */

/* 5 · Full Width kiosk. A 1920px landscape tablet is the literal use case, so
   the tiles are allowed a wider track rather than stretching four of them
   across a metre of screen. */
#so-booking-widget[data-size="full"][data-template="counter"] .so-bw-card-grid {
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr));
}

/* 6 · Popup and floating both give the widget a shell with its own padding
   and its own scroll region. Counter Sand's 64px commit bar plus its 20px
   band is 150px of chrome before a single tile — enough that on a short
   floating panel the first tile can start below the fold. The bar is sticky
   already (base .so-bw-nav), so nothing is unreachable; the band gives back
   the difference instead of the content doing it. */
#so-booking-widget[data-position="popup"][data-template="counter"] .so-bw-header,
#so-booking-widget[data-position="floating"][data-template="counter"] .so-bw-header {
    padding-top: 16px;
    padding-bottom: 14px;
}
#so-booking-widget[data-position="popup"][data-template="counter"] .so-bw-nav,
#so-booking-widget[data-position="floating"][data-template="counter"] .so-bw-nav {
    padding-top: 12px;
    padding-bottom: 12px;
}


/* ==========================================================================
   25 · WORKSPACE BRASS — THE DESKTOP WORKSPACE
   ==========================================================================

   The twenty-two above are drawn for a card. This one is drawn for a
   WORKSPACE: a laptop-width surface with three permanent regions — a
   charcoal column down the left, a numbered brass step bar across the top,
   and a body that lays itself out per step rather than repeating one card
   grid eight times.

   ── WHAT MAKES IT A DIFFERENT ARCHITECTURE ──────────────────────────────

   Split Canvas also stands a panel beside the steps, and Wide Canvas also
   puts two things in one row, so the reading to avoid was "Split in gold" or
   "Wide with a rail". The difference is that here BOTH are permanent AND the
   body changes shape by step:

     Split      one 50/50 grid, the same on every step.
     Wide       one wide column; only the date step is two panes.
     This       a fixed 236px nav column, a brass stepper, and a body that
                is a list beside a month, a form beside its own summary, or
                details beside the money — a different two-up per step.

   The column is NOT Split's reversed-out brand pane and NOT Roster's light
   directory. It is site navigation in the host brand's charcoal, carrying
   the running order and the total, and it never scrolls away — which is the
   whole proposition: on a laptop the customer can always see what they have
   chosen and what it costs without leaving the question they are answering.

   ── THE FOLD ORDER, WHICH IS THE RESPONSIVE DESIGN ──────────────────────

   Three permanent regions is a lot to promise inside a 420px floating panel,
   so each is given up in a stated order rather than all at once:

     900   the two-up bodies stack (month over times, details over money).
     820   the nav column UNPINS and becomes a horizontal band above the
           header, and drops every row the customer has not answered yet.
     700   the lists go one-up and the form goes one column.
     560   the step bar loses its labels and keeps its numbers.
     480   the band keeps only the brand line and the total.

   Every one of those gates is `data-cw`, i.e. the CONTAINER's width, so the
   fold happens in a small inline embed and a popup dialog on a 27" monitor
   exactly as it does on a phone. There is no viewport media query in this
   section on purpose.

   ── THE THREE RULES THIS SECTION KEEPS (same three as section 24) ───────

   1. ONLY CLASSES THE WIDGET CAN EMIT — nothing here needs new DOM. The
      column is `.so-bw-rail`, the stepper is `.so-bw-stepbar`, the two-up
      date step is `.so-bw-datetime`, the two-pane review is
      `.so-bw-summary-main` / `.so-bw-summary-pay`.
   2. NO LITERAL HORIZONTAL INSET on .so-bw-header / .so-bw-nav /
      .so-bw-step. The x-inset is --so-bw-gutter; only vertical padding is
      this architecture's business.
   3. THE POPUP GUARD ON EVERY .so-bw-card RULE. `.so-bw-sp__panel` carries
      `.so-bw-card`, so an unguarded card rule repaints the staff-profile
      dialog — and the guard is also what takes these to (1,3,0).

   ── ONE LINE OF JS, AND WHY ─────────────────────────────────────────────

   `usesSplitDatetime` in booking-widget.js gained this layout key. It is a
   whitelist of layouts that keep the month MOUNTED after a date is picked;
   without it the left pane of the date step unmounts on the first click and
   the times slide across a half-empty row. Additive — every other template
   reads false and is untouched.
   ========================================================================== */


/* ── 25.1 · The shell and the standing column ────────────────────────────
   A RESERVED COLUMN, not a grid — the same mechanism Roster Slate uses and
   for the same reason. `.so-bw-container` has thirteen direct children and
   only three of them (header, body, nav) would ever carry a placement; on a
   grid the other ten auto-flow into whatever cell is free, which puts the
   hold-timer warning under the commit bar and "Powered by" under the nav.
   Taking the column out of flow and reserving its width with padding leaves
   every other child flowing in DOM order down the step column, at full
   width, exactly as it does on the fifteen non-grid architectures.

   `position: relative` is what the column's left/top/bottom resolve
   against; without it the column escapes to the page.

   `overflow: hidden` because the column runs to the container's own border
   and the popup / floating shells clip to a rounded corner — without it the
   column's square top-left paints over the panel's round one. */
#so-booking-widget[data-template="workspace"] .so-bw-container {
    position: relative;
    max-width: 1180px;
    min-height: 560px;
    padding-left: 236px;
    overflow: hidden;
    background: var(--so-bw-ds-n0);

    /* DERIVED, NOT LITERAL — the same discipline Counter Sand's ink band
       keeps. Every surface below is mixed from --so-bw-primary, so an admin
       who changes the accent in the Studio gets a charcoal that follows it
       instead of one template whose column ignores the colour picker.

       NOTHING HERE IS AN INK TOKEN. Small brass TEXT on the white canvas —
       the step count, the active step label, "View profile", the total —
       reads --so-bw-primary-ink, the shared token that darkens the same hue
       in OKLCH until it clears AA. A hand-mixed shade would have been a
       twenty-fourth answer to a question the stylesheet already answers
       once, and it would drift the moment an admin changed the accent. */
    --so-bw-ws-ink:  color-mix(in srgb, var(--so-bw-primary) 10%, #131110);
    --so-bw-ws-ink2: color-mix(in srgb, var(--so-bw-primary) 17%, #232019);
    --so-bw-ws-wash: color-mix(in srgb, var(--so-bw-primary) 7%, #ffffff);
    --so-bw-ws-line: color-mix(in srgb, var(--so-bw-primary) 26%, #e7e3db);

    /* WHAT SITS ON A BRASS FILL. The default is white, which is the right
       answer for the deep blues and greens most of the catalogue is built on
       and the wrong one for brass: measured 3.2:1 for the "Apply" buttons
       beside the gift-card and referral fields, which is a real control with
       real text on it. Ink instead — 8:1 on the same fill, and it is also
       what the design shows on its one gold button.

       Safe for the commit bar, which paints charcoal and states `color: #fff`
       on itself rather than reading this token. */
    --so-bw-on-primary: var(--so-bw-ws-ink);
}

/* The column itself. top/bottom rather than a height is what makes it a
   COLUMN: it runs the full height of the card beside the header, the step
   and the commit bar alike, whatever the step's own height — which is the
   point of standing navigation. Its own scroll so a long service name plus
   the running order cannot overflow the card on a short viewport. */
#so-booking-widget[data-template="workspace"] .so-bw-rail {
    display: flex;
    flex-direction: column;
    position: absolute;
    left: 0;
    top: 0;
    bottom: 0;
    width: 236px;
    padding: 26px 20px;
    gap: 18px;
    color: #fff;
    background: var(--so-bw-ws-ink);
    background: linear-gradient(180deg, var(--so-bw-ws-ink2), var(--so-bw-ws-ink) 46%);
    overflow-y: auto;
    overscroll-behavior: contain;
    z-index: 2;

    /* The column is the one DARK surface this template has, so the brand hue
       has to be corrected the other way here: --so-bw-primary-ink darkens for
       a white canvas and would put a 3:1 brass on near-black. Same mechanism
       as the stylesheet's dark-canvas pair, lifting instead of darkening, and
       with the plain value on the line above so an engine without relative
       colour syntax simply keeps today's rendering. */
    --so-bw-ws-brass: var(--so-bw-primary);
    --so-bw-ws-brass: oklch(from var(--so-bw-primary) max(l, 0.74) c h);
}
#so-booking-widget[data-template="workspace"] .so-bw-rail-head {
    margin-bottom: 0;
    padding-bottom: 16px;
    border-bottom: 1px solid rgba(255, 255, 255, 0.10);
}
#so-booking-widget[data-template="workspace"] .so-bw-rail-title {
    font-size: 18px;
    font-weight: 800;
    line-height: 1.18;
    letter-spacing: -0.01em;
    color: #fff;
}
/* The brass rule under the brand line. Two pixels of accent is the whole
   signature of the column — enough to say which business this is without
   the panel becoming a colour field. */
#so-booking-widget[data-template="workspace"] .so-bw-rail-title::after {
    content: '';
    display: block;
    width: 26px;
    height: 2px;
    margin-top: 10px;
    background: var(--so-bw-primary);
}
#so-booking-widget[data-template="workspace"] .so-bw-rail-sub {
    margin-top: 10px;
    font-size: 12px;
    line-height: 1.45;
    color: rgba(255, 255, 255, 0.55);
}
#so-booking-widget[data-template="workspace"] .so-bw-rail-list {
    display: flex;
    flex-direction: column;
    gap: 0;
    flex: 1 1 auto;
}
/* Stacked key over value, not a two-column row: 196px of usable column
   cannot hold "Specialist" and a full name side by side without one of them
   ellipsing, and the value is the half that matters. */
#so-booking-widget[data-template="workspace"] .so-bw-rail-row {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 3px;
    padding: 11px 0;
    border-bottom: 1px solid rgba(255, 255, 255, 0.07);
}
/* …but NOT the tally row. The base hides it at (1,1,0) and the rule above
   scores (1,2,0), so revealing every row also revealed the one row the
   eighteen tall-column templates deliberately do not show — "Selected ·
   0 services" sitting above a running order that already names the service.
   It comes back at 820, where the column is a horizontal band and the tally
   is exactly the one line that band has room for. */
#so-booking-widget[data-template="workspace"] .so-bw-rail-row--tally {
    display: none;
}
#so-booking-widget[data-template="workspace"] .so-bw-rail-k {
    color: rgba(255, 255, 255, 0.42);
}
#so-booking-widget[data-template="workspace"] .so-bw-rail-v {
    font-size: 13px;
    font-weight: 600;
    line-height: 1.35;
    color: rgba(255, 255, 255, 0.92);
    overflow-wrap: anywhere;
}
/* Answered rows are brass; unanswered ones stay a dash in the quietest
   grey the column has. The customer can read their progress off the colour
   alone, which is what a standing panel is for. */
#so-booking-widget[data-template="workspace"] .so-bw-rail-row:not(.so-bw-rail-row--unset) .so-bw-rail-v {
    color: var(--so-bw-ws-brass, var(--so-bw-primary));
}
#so-booking-widget[data-template="workspace"] .so-bw-rail-row--unset .so-bw-rail-v {
    color: rgba(255, 255, 255, 0.30);
}
#so-booking-widget[data-template="workspace"] .so-bw-rail-total {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    margin-top: auto;
    padding-top: 16px;
    border-top: 1px solid rgba(255, 255, 255, 0.14);
}
#so-booking-widget[data-template="workspace"] .so-bw-rail-total span:first-child {
    color: rgba(255, 255, 255, 0.42);
}
#so-booking-widget[data-template="workspace"] .so-bw-rail-total span:last-child {
    font-size: 17px;
    font-weight: 700;
    color: var(--so-bw-ws-brass, var(--so-bw-primary));
}


/* ── 25.2 · The workspace header and the brass step bar ──────────────────
   DOM order in the header is brand → step indicator → step bar → progress.
   The design puts the numbered bar ABOVE the question it is numbering, so
   the header becomes a flex column and the three blocks are re-ordered.
   `order` rather than new markup: the reading order for a screen reader is
   unchanged (the live region announces the step regardless), and nothing
   about the DOM has to move for a purely visual sequence. */
#so-booking-widget[data-template="workspace"] .so-bw-header {
    display: flex;
    flex-direction: column;
    gap: 16px;
    padding-top: 22px;
    padding-bottom: 18px;
    background: var(--so-bw-ds-n0);
    border-bottom: 1px solid var(--so-bw-ds-n100);
}
#so-booking-widget[data-template="workspace"] .so-bw-brand { order: 1; }
#so-booking-widget[data-template="workspace"] .so-bw-stepbar { order: 2; }
#so-booking-widget[data-template="workspace"] .so-bw-step-indicator { order: 3; }
#so-booking-widget[data-template="workspace"] .so-bw-progress { order: 4; }

/* The step count leads and the title follows — "STEP 1 OF 8" set as the
   design system's quietest device, then the question at 21px. It is the one
   place this template spends type size, because on a 1180px workspace a
   17px heading beside a 236px column reads as a form label. */
#so-booking-widget[data-template="workspace"] .so-bw-step-indicator {
    flex-direction: column;
    align-items: flex-start;
    gap: 5px;
    margin-bottom: 0;
}
#so-booking-widget[data-template="workspace"] .so-bw-step-count {
    order: -1;
    font-size: 10.5px;
    font-weight: 500;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}
#so-booking-widget[data-template="workspace"] .so-bw-step-label {
    font-size: 21px;
    font-weight: 800;
    line-height: 1.2;
    letter-spacing: -0.022em;
    color: var(--so-bw-text);
}

/* THE BRASS NUMBERS. The base bar is already numbered dots on a connector
   line, so this is a restatement of weight and colour rather than a new
   component: bigger dots, a hairline connector that runs behind them, a
   filled brass dot for the step you are on and a ringed one for the steps
   you have finished — done and active have to be told apart at a glance or
   the bar stops being progress and becomes decoration. */
#so-booking-widget[data-template="workspace"] .so-bw-stepbar {
    margin: 0;
    gap: 0;
    padding-bottom: 2px;
}
#so-booking-widget[data-template="workspace"] .so-bw-stepbar-item {
    min-width: 62px;
    gap: 8px;
}
#so-booking-widget[data-template="workspace"] .so-bw-stepbar-item:not(:first-child)::before {
    top: 14px;
    height: 1px;
    background: var(--so-bw-ds-n200);
}
#so-booking-widget[data-template="workspace"] .so-bw-stepbar-item--active:not(:first-child)::before,
#so-booking-widget[data-template="workspace"] .so-bw-stepbar-item--done:not(:first-child)::before {
    background: var(--so-bw-primary);
}
/* :first-of-type, NOT :first-child — and the difference is visible.
   The items are rendered by an Alpine `x-for`, which leaves its own
   `<template>` element in the DOM as the loop's anchor, so the FIRST
   rendered item is the parent's second child and the base rule's
   `:not(:first-child)` guard never excludes it. Measured: step 1's
   connector drew at (176,129,58) — a brass stub running off the left edge
   of the bar into nothing, because step 1 is also always `--active`.
   `first-of-type` counts only sibling DIVs, so it lands on the real first
   item. Centred hit the same thing and fixes it the same way. */
#so-booking-widget[data-template="workspace"] .so-bw-stepbar-item:first-of-type::before {
    display: none;
}
#so-booking-widget[data-template="workspace"] .so-bw-stepbar-dot {
    width: 28px;
    height: 28px;
    font-family: var(--so-bw-mono);
    font-size: 11.5px;
    font-weight: 600;
    background: var(--so-bw-ds-n0);
    border: 1px solid var(--so-bw-ds-n200);
    color: var(--so-bw-text-tertiary);
}
#so-booking-widget[data-template="workspace"] .so-bw-stepbar-item--active .so-bw-stepbar-dot {
    background: var(--so-bw-primary);
    border-color: var(--so-bw-primary);
    color: #fff;
    box-shadow: 0 0 0 4px var(--so-bw-ws-wash);
}
/* Done is the accent as an OUTLINE, not as a second fill. Two filled brass
   dots on the same bar leave the customer looking for which one is current. */
#so-booking-widget[data-template="workspace"] .so-bw-stepbar-item--done .so-bw-stepbar-dot {
    background: var(--so-bw-ws-wash);
    border-color: var(--so-bw-primary);
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
    box-shadow: none;
}
#so-booking-widget[data-template="workspace"] .so-bw-stepbar-label {
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.01em;
    color: var(--so-bw-text-tertiary);
}
/* --deep, not --so-bw-primary: an 11px label is small text and the stock
   brass does not clear AA on white at that size. Same value, two shades
   down; the dot beside it carries the actual hue. */
#so-booking-widget[data-template="workspace"] .so-bw-stepbar-item--active .so-bw-stepbar-label {
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
    font-weight: 700;
}
#so-booking-widget[data-template="workspace"] .so-bw-stepbar-item--done .so-bw-stepbar-label {
    color: var(--so-bw-text-secondary);
}


/* ── 25.3 · The body and the lists ───────────────────────────────────────
   The step gets its vertical rhythm here; the horizontal inset stays on the
   shared gutter (rule 2 at the top of this section). */
#so-booking-widget[data-template="workspace"] .so-bw-body {
    background: var(--so-bw-ds-n0);
}
#so-booking-widget[data-template="workspace"] .so-bw-step {
    padding-top: 22px;
    padding-bottom: 22px;
    background: transparent;
}
#so-booking-widget[data-template="workspace"] .so-bw-step-desc {
    margin-top: -4px;
    margin-bottom: 18px;
    font-size: 13.5px;
    color: var(--so-bw-text-secondary);
}

/* The search field sits on the workspace rather than in it — a full-width
   control above the list, which is where the catalogue is actually filtered
   from on a wide card. */
#so-booking-widget[data-template="workspace"] .so-bw-search {
    margin-bottom: 16px;
}
#so-booking-widget[data-template="workspace"] .so-bw-search-input {
    padding-top: 11px;
    padding-bottom: 11px;
    border-radius: var(--so-bw-ds-radius-ctl);
    background: var(--so-bw-ds-n25);
}

/* TWO UP on the workspace. 320px is what a title, a description line and a
   duration/price pair need before the description starts breaking a word
   per line; below that the grid drops to one column on its own. */
#so-booking-widget[data-template="workspace"] .so-bw-card-grid {
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 320px), 1fr));
    gap: 12px;
}
/* …except the LOCATION step, which is one-up. A branch row carries an address
   and a phone — free text that has nowhere to go in a half-width tile.
   `.so-bw-card--location` is emitted only on that step, so `:has()` identifies
   it without the markup having to label the grid. (It used to key on
   `.so-bw-card-directions`, which went with the coordinates; the card's own
   modifier is the same signal and does not depend on a feature.) */
#so-booking-widget[data-template="workspace"] .so-bw-card-grid:has(.so-bw-card--location) {
    grid-template-columns: 1fr;
}
/* The scrolling variant (staff, and long service catalogues) keeps its cap
   but gets more of the workspace: on a laptop 60vh of an 1180px card is a
   lot of list, and the commit bar is sticky anyway. */
#so-booking-widget[data-template="workspace"] .so-bw-card-grid--scroll {
    max-height: 52vh;
}

/* THE ROW. Every choice in this template is a horizontal row: a square
   thumbnail, the name and its description, then the duration and price held
   to the right edge. Service cards need all four properties restated with
   !important because the guard in section 26 pins them to protect the
   stacked card — the same opt-out Wide Canvas documents in full. */
#so-booking-widget[data-template="workspace"] .so-bw-card--service:not(.so-bw-sp__panel) {
    flex-direction: row !important;
    align-items: center !important;
    padding: 12px 14px !important;
    gap: 14px !important;
}
#so-booking-widget[data-template="workspace"] .so-bw-card:not(.so-bw-sp__panel) {
    display: flex;
    flex-direction: row;
    align-items: center;
    gap: 14px;
    padding: 12px 14px;
    min-height: 76px;
    border: 1px solid var(--so-bw-ds-n200);
    background: var(--so-bw-ds-n0);
    box-shadow: none;
    transition: border-color var(--so-bw-ds-motion), background var(--so-bw-ds-motion);
}
#so-booking-widget[data-template="workspace"] .so-bw-card:not(.so-bw-sp__panel):hover {
    border-color: var(--so-bw-ws-line);
    background: var(--so-bw-ds-n25);
}
/* Selected is a brass edge and the faintest wash — the design's whole
   selected state, and the reason the check below can stay small. */
#so-booking-widget[data-template="workspace"] .so-bw-card--selected:not(.so-bw-sp__panel) {
    border-color: var(--so-bw-primary);
    background: var(--so-bw-ws-wash);
    box-shadow: inset 0 0 0 1px var(--so-bw-primary);
}
/* Both, because the two card shapes differ: a SERVICE card wraps its body
   in .so-bw-card-top, a STAFF card puts .so-bw-card-body straight under the
   card. Whichever is the flex child has to size to its contents — the base
   gives .so-bw-card-body `flex: 1`, i.e. a flex-basis of ZERO, which pushes
   a long staff name outside its own card in a narrow column. */
#so-booking-widget[data-template="workspace"] .so-bw-card-top,
#so-booking-widget[data-template="workspace"] .so-bw-card:not(.so-bw-sp__panel) > .so-bw-card-body {
    flex: 1 1 auto;
    min-width: 0;
    padding: 0;
}
#so-booking-widget[data-template="workspace"] .so-bw-card-body {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: center;
    column-gap: 14px;
    row-gap: 3px;
}
#so-booking-widget[data-template="workspace"] .so-bw-card-title,
#so-booking-widget[data-template="workspace"] .so-bw-card-meta,
#so-booking-widget[data-template="workspace"] .so-bw-card-more {
    grid-column: 1;
}
#so-booking-widget[data-template="workspace"] .so-bw-card-title {
    font-size: 14.5px;
    font-weight: 700;
    letter-spacing: -0.01em;
}
#so-booking-widget[data-template="workspace"] .so-bw-card-meta {
    font-size: 12.5px;
    line-height: 1.45;
    color: var(--so-bw-text-secondary);
}
/* The badges hold the right-hand column across however many lines the text
   beside them needs — the price/duration pair is what the customer scans a
   catalogue for, so it gets a fixed edge to be scanned down. */
#so-booking-widget[data-template="workspace"] .so-bw-card-badges {
    grid-column: 2;
    grid-row: 1 / span 2;
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 4px;
}
#so-booking-widget[data-template="workspace"] .so-bw-badge--price {
    font-family: var(--so-bw-mono);
    font-size: 13.5px;
    font-weight: 700;
    color: var(--so-bw-text);
    background: transparent;
    padding: 0;
}
#so-booking-widget[data-template="workspace"] .so-bw-badge--duration {
    background: var(--so-bw-ds-n50);
    color: var(--so-bw-text-secondary);
}
/* "View profile" is the way into the staff dialog, which is this template's
   answer to the design's specialist panel — so it reads as a real link
   rather than as a fifth line of grey meta. */
#so-booking-widget[data-template="workspace"] .so-bw-card-profile {
    grid-column: 1;
    justify-self: start;
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
    font-weight: 600;
}

/* The thumbnail. A fixed square tile at the head of the row, not the
   full-bleed header band the stacked card uses — the band is what makes a
   grid of tall cards, and this template's list is rows. */
#so-booking-widget[data-template="workspace"] .so-bw-card-media {
    flex: 0 0 auto;
    width: 64px;
    height: 64px;
    aspect-ratio: auto;
    margin: 0;
    border-radius: var(--so-bw-ds-radius-chip);
    overflow: hidden;
}
#so-booking-widget[data-template="workspace"] .so-bw-card-media-img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}
/* SIZED THROUGH THE TOKEN, not through width/height alone. The avatar
   primitive pins all six of width / height / min- / max- to
   `--so-bw-icon-size` (section 16, beside `.so-bw-card-media-avatar`), and a
   min-/max- pair beats a plain `width` — so declaring 64px here left the tile
   clamped to the primitive's 68px inside this template's 64px band, and the
   round mark was shaved 2px on each side on EVERY card. MEASURED across all
   twelve size/position cells at 1440 and 390: tile 253..321 in a band
   255..319, identically at every one of them. The literals are kept beside
   the token so the declared size and the enforced size cannot drift apart. */
#so-booking-widget[data-template="workspace"] .so-bw-card-media-avatar,
#so-booking-widget[data-template="workspace"] .so-bw-card-media .so-bw-card-icon,
#so-booking-widget[data-template="workspace"] .so-bw-card:not(.so-bw-sp__panel) > .so-bw-card-icon {
    --so-bw-icon-size: 64px;
    width: 64px;
    height: 64px;
    flex: 0 0 auto;
    border-radius: var(--so-bw-ds-radius-chip);
    font-size: 17px;
}
/* A carousel inside a 64px tile is four controls nobody can hit. The images
   are still all there — the card opens the same service either way — and
   the arrows come back at the sizes the band is actually rendered at. */
#so-booking-widget[data-template="workspace"] .so-bw-card-media-nav,
#so-booking-widget[data-template="workspace"] .so-bw-card-media-dots {
    display: none;
}

/* The check is a small brass disc at the right edge of the row. It is a
   confirmation, not the selected state — the edge and the wash above are
   what actually read at a glance. */
#so-booking-widget[data-template="workspace"] .so-bw-card-check {
    width: 22px;
    height: 22px;
    flex: 0 0 auto;
    border-radius: 50%;
    background: var(--so-bw-primary);
    color: var(--so-bw-on-primary, #fff);
}
#so-booking-widget[data-template="workspace"] .so-bw-checkbox--checked {
    background: var(--so-bw-primary);
    border-color: var(--so-bw-primary);
}


/* ── 25.4 · Date & time — two framed panels ──────────────────────────────
   The month and the times are ONE decision, so they share the row and the
   frame. `usesSplitDatetime` keeps the month mounted after a date is picked
   (see the JS note at the top of this section), which is what stops the
   left pane vanishing the moment the customer commits to a day. */
#so-booking-widget[data-template="workspace"] .so-bw-datetime {
    /* Explicit tracks rather than the base `auto-fit, minmax(300px, 1fr)`:
       auto-fit gives both tracks equal width, which spends half the
       workspace on a month that needs ~330px and starves the times, which
       can use everything they are given. */
    grid-template-columns: minmax(0, 348px) minmax(0, 1fr);
    gap: 18px;
    align-items: stretch;
}
#so-booking-widget[data-template="workspace"] .so-bw-calendar {
    padding: 16px;
    border: 1px solid var(--so-bw-ds-n200);
    border-radius: var(--so-bw-ds-radius-card);
    background: var(--so-bw-ds-n0);
    box-sizing: border-box;
    height: 100%;
}
#so-booking-widget[data-template="workspace"] .so-bw-slot-panel {
    padding: 16px;
    border: 1px solid var(--so-bw-ds-n200);
    border-radius: var(--so-bw-ds-radius-card);
    background: var(--so-bw-ds-n25);
    min-height: 340px;
}
/* Both panel headers on one line, so the two grids beneath them start
   level. 62px is the timezone control's natural height — its label sits
   ABOVE the combobox by design, so that header cannot be shorter, and
   giving the month the same floor is what puts the two rules on one line. */
#so-booking-widget[data-template="workspace"] .so-bw-calendar-header,
#so-booking-widget[data-template="workspace"] .so-bw-slot-panel-head {
    min-height: 62px;
    margin: 0 0 14px;
    padding: 0 0 14px;
    border-bottom: 1px solid var(--so-bw-ds-n100);
    align-items: center;
}
#so-booking-widget[data-template="workspace"] .so-bw-calendar-month,
#so-booking-widget[data-template="workspace"] .so-bw-slot-panel-title {
    font-size: 15.5px;
    font-weight: 700;
    letter-spacing: -0.01em;
}
#so-booking-widget[data-template="workspace"] .so-bw-calendar-nav {
    border-radius: var(--so-bw-ds-radius-ctl);
    border: 1px solid var(--so-bw-ds-n200);
}
#so-booking-widget[data-template="workspace"] .so-bw-calendar-weekdays > * {
    font-family: var(--so-bw-mono);
    padding: 8px 0;
    font-size: 10px;
    font-weight: 500;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--so-bw-text-tertiary);
}
/* The month is permanent furniture here rather than a page the customer
   passes through, so its cells get the room to be read at a glance. */
#so-booking-widget[data-template="workspace"] .so-bw-calendar-day {
    aspect-ratio: auto;
    min-height: 42px;
    font-size: 13.5px;
    border-radius: var(--so-bw-ds-radius-chip);
}
/* SELECTION IS NOT REPAINTED HERE, DELIBERATELY. "The day you picked is
   solid accent" and "the time you picked is an accent tint with a 2px ring"
   are locked across all templates at (1,3,0) !important — a customer moving
   between two sites built on this plugin has to read selection the same way
   both times. A charcoal selected day was written here first, measured as
   brass anyway, and is gone rather than left as a rule that looks live.
   Differentiation goes on the axes that are NOT locked: the chip radius, the
   today ring, and the pill geometry below. */
#so-booking-widget[data-template="workspace"] .so-bw-calendar-day--today:not(.so-bw-calendar-day--selected) {
    box-shadow: inset 0 0 0 1px var(--so-bw-primary);
}
/* The times as a tabular grid of pills, which is what a whole day beside a
   month needs to be: 128px holds a full start time and its "till" line on
   two lines at 12.5px without the row height changing per label. */
#so-booking-widget[data-template="workspace"] .so-bw-slot-list {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 128px), 1fr));
    gap: 8px;
}
#so-booking-widget[data-template="workspace"] .so-bw-slot-row {
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 2px;
    padding: 10px 8px;
    text-align: center;
    border: 1px solid var(--so-bw-ds-n200);
    border-radius: var(--so-bw-ds-radius-chip);
    background: var(--so-bw-ds-n0);
}
#so-booking-widget[data-template="workspace"] .so-bw-slot-row:hover:not(.so-bw-slot-row--booked) {
    border-color: var(--so-bw-primary);
    background: var(--so-bw-ws-wash);
}
/* No `--selected` rule here either — same lock as the calendar day above. */
#so-booking-widget[data-template="workspace"] .so-bw-slot-time {
    font-size: 13.5px;
    font-weight: 600;
}
#so-booking-widget[data-template="workspace"] .so-bw-slot-till {
    font-size: 11px;
    opacity: 0.72;
}
#so-booking-widget[data-template="workspace"] .so-bw-slot-group-title {
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}
/* With the month permanently on screen this is no longer dead markup: it is
   what the right pane shows before a day is chosen, which on a split layout
   is the first thing the customer sees. Centre it so it reads as a waiting
   panel rather than as an error at the top. */
#so-booking-widget[data-template="workspace"] .so-bw-slot-panel-empty {
    margin: auto;
}


/* ── 25.5 · Details — the form as two columns ────────────────────────────
   A workspace should not ask for eight scrolls of single-column form beside
   600px of white, so the form becomes a REAL two-column grid rather than
   half-spans inside the shared twelve.

   That distinction is the whole rule here, and the first draft got it wrong:
   `.so-bw-form-group { grid-column: span 6 }` scores (1,2,0) and so also beat
   `.so-bw-form-row .so-bw-form-group { grid-column: auto }` at (0,2,0) — and
   a `span 6` inside the name row's OWN 2-column grid makes the browser
   auto-create four implicit zero-width tracks. Measured: First Name and Last
   Name each took a full row, i.e. the one pair the base markup already pairs
   was the one pair that stopped pairing.

   The idiom Week Board and Wide Canvas use avoids it entirely — two real
   columns, `auto` spans, and the nested row flattened with `display:
   contents` so it becomes two ordinary cells of the outer grid instead of a
   grid inside a grid. It also makes the one-column fallback need no reset. */
#so-booking-widget[data-template="workspace"] .so-bw-form {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 16px 18px;
    align-items: start;
}
#so-booking-widget[data-template="workspace"] .so-bw-form-group {
    grid-column: auto;
}
#so-booking-widget[data-template="workspace"] .so-bw-form-row {
    display: contents;
}
/* Anything free-form keeps the full width — a notes box or an address in a
   half-column is a field the customer cannot read back. */
#so-booking-widget[data-template="workspace"] .so-bw-form-group:has(.so-bw-textarea) {
    grid-column: 1 / -1;
}
#so-booking-widget[data-template="workspace"] .so-bw-label {
    font-size: 12px;
    font-weight: 600;
    letter-spacing: 0.01em;
    color: var(--so-bw-text-secondary);
}
#so-booking-widget[data-template="workspace"] .so-bw-input,
#so-booking-widget[data-template="workspace"] .so-bw-select,
#so-booking-widget[data-template="workspace"] .so-bw-textarea {
    border-radius: var(--so-bw-ds-radius-ctl);
    background: var(--so-bw-ds-n25);
}
#so-booking-widget[data-template="workspace"] .so-bw-input:focus,
#so-booking-widget[data-template="workspace"] .so-bw-select:focus,
#so-booking-widget[data-template="workspace"] .so-bw-textarea:focus {
    background: var(--so-bw-ds-n0);
}


/* ── 25.6 · Review — details on one side, the money on the other ─────────
   TWO GRID ITEMS, ONE PER PANE, for the reason Wide Canvas records at
   length: the summary has fourteen children and ten of them are unclassed
   inline-styled divs, so placing blocks individually cannot work — grid
   auto-placement never moves backwards and the money column ends up
   starting three rows down. Each pane is a single item owning its own
   vertical stack, so both start at the top and neither can interleave. */
#so-booking-widget[data-template="workspace"] .so-bw-summary {
    display: grid;
    grid-template-columns: minmax(0, 1.55fr) minmax(0, 1fr);
    gap: 0;
    align-items: stretch;
    padding: 0;
    border: 1px solid var(--so-bw-ds-n200);
    border-radius: var(--so-bw-ds-radius-card);
    overflow: hidden;
}
#so-booking-widget[data-template="workspace"] .so-bw-summary-main,
#so-booking-widget[data-template="workspace"] .so-bw-summary-pay {
    display: flex;
    flex-direction: column;
    min-width: 0;
}
#so-booking-widget[data-template="workspace"] .so-bw-summary-main {
    grid-column: 1;
    grid-row: 1;
    padding: 4px 0 16px;
}
/* The money pane is the tinted one and the rule between the panes is its
   left border, so it runs the full height of the taller pane rather than
   stopping under the last price row — which is why `align-items: stretch`
   is on the grid above. */
#so-booking-widget[data-template="workspace"] .so-bw-summary-pay {
    grid-column: 2;
    grid-row: 1;
    gap: 2px;
    padding: 16px;
    background: var(--so-bw-ds-n25);
    border-left: 1px solid var(--so-bw-ds-n200);
}
#so-booking-widget[data-template="workspace"] .so-bw-summary-section {
    border: 0;
    padding: 16px 18px 0;
}
#so-booking-widget[data-template="workspace"] .so-bw-summary-section-title {
    font-family: var(--so-bw-mono);
    font-size: 10.5px;
    font-weight: 500;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--so-bw-text-secondary);
}
#so-booking-widget[data-template="workspace"] .so-bw-summary-row {
    display: flex;
    justify-content: space-between;
    gap: 14px;
    padding: 9px 18px;
}
/* The pay pane supplies its own 16px inset, so rows inside it must not add
   a second one — that is what puts the price column 34px off the edge. */
#so-booking-widget[data-template="workspace"] .so-bw-summary-pay .so-bw-summary-row {
    padding-left: 0;
    padding-right: 0;
}
#so-booking-widget[data-template="workspace"] .so-bw-summary-label {
    flex: 1 1 auto;
    color: var(--so-bw-text-secondary);
}
#so-booking-widget[data-template="workspace"] .so-bw-summary-value {
    flex: 0 1 auto;
    text-align: right;
    font-variant-numeric: tabular-nums;
}
#so-booking-widget[data-template="workspace"] .so-bw-summary-row--total,
#so-booking-widget[data-template="workspace"] .so-bw-summary-row--deposit {
    margin: 0;
    padding: 12px 0 0;
    border-top: 1px solid var(--so-bw-ds-n200);
}
/* The total is the one number on this screen the customer came to see. */
#so-booking-widget[data-template="workspace"] .so-bw-summary-row--total .so-bw-summary-value {
    font-size: 19px;
    font-weight: 800;
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}


/* ── 25.7 · The commit bar ───────────────────────────────────────────────
   Docked below the workspace, quiet, with Back as a ghost on the left and
   the commit filled in INK rather than brass. That is deliberate and it is
   the design's own arrangement: brass is the accent that marks what has
   been chosen, so making the commit brass too would put the same colour on
   "this is selected" and on "do the thing". The charcoal button is also the
   only fill that holds white at any accent an admin picks. */
#so-booking-widget[data-template="workspace"] .so-bw-nav {
    padding-top: 16px;
    padding-bottom: 16px;
    background: var(--so-bw-ds-n25);
    border-top: 1px solid var(--so-bw-ds-n100);
}
#so-booking-widget[data-template="workspace"] .so-bw-btn {
    border-radius: var(--so-bw-ds-radius-ctl);
    font-weight: 700;
    letter-spacing: 0.01em;
}
#so-booking-widget[data-template="workspace"] .so-bw-btn--primary {
    padding-left: 22px;
    padding-right: 22px;
    background: var(--so-bw-ws-ink);
    border-color: var(--so-bw-ws-ink);
    color: #fff;
}
#so-booking-widget[data-template="workspace"] .so-bw-btn--primary:hover:not(:disabled) {
    background: var(--so-bw-ws-ink2);
    border-color: var(--so-bw-ws-ink2);
}
#so-booking-widget[data-template="workspace"] .so-bw-btn--secondary {
    background: transparent;
    border: 1px solid var(--so-bw-ds-n200);
    color: var(--so-bw-text-secondary);
}
#so-booking-widget[data-template="workspace"] .so-bw-btn--secondary:hover:not(:disabled) {
    border-color: var(--so-bw-ws-line);
    color: var(--so-bw-text);
}


/* ── 25.8 · Confirmation — the one dark screen ───────────────────────────
   The last screen the customer sees is the one place this template turns
   the workspace over to the charcoal, with the brass check and a brass
   button on it: the booking is done, so nothing on it is a control that
   competes with a commit, and the accent is finally free to be the CTA.

   Scoped to `.so-bw-confirmation` rather than to `--done` on the container,
   because the column and the commit bar keep their own surfaces — only the
   step content inverts. */
#so-booking-widget[data-template="workspace"] .so-bw-confirmation {
    padding: 40px 28px;
    border-radius: var(--so-bw-ds-radius-card);
    background: var(--so-bw-ws-ink);
    background: linear-gradient(165deg, var(--so-bw-ws-ink2), var(--so-bw-ws-ink) 58%);
    color: #fff;
}
#so-booking-widget[data-template="workspace"] .so-bw-confirmation-icon {
    background: var(--so-bw-primary);
    color: var(--so-bw-on-primary, #fff);
    box-shadow: 0 0 0 8px rgba(255, 255, 255, 0.06);
}
#so-booking-widget[data-template="workspace"] .so-bw-confirmation-title {
    font-size: 25px;
    font-weight: 800;
    letter-spacing: -0.025em;
    color: #fff;
}
#so-booking-widget[data-template="workspace"] .so-bw-confirmation-text {
    color: rgba(255, 255, 255, 0.66);
}
#so-booking-widget[data-template="workspace"] .so-bw-confirmation-details {
    background: rgba(255, 255, 255, 0.05);
    border-color: rgba(255, 255, 255, 0.12);
}
#so-booking-widget[data-template="workspace"] .so-bw-confirmation-detail {
    border-color: rgba(255, 255, 255, 0.09);
}
#so-booking-widget[data-template="workspace"] .so-bw-confirmation-detail-label {
    color: rgba(255, 255, 255, 0.45);
}
#so-booking-widget[data-template="workspace"] .so-bw-confirmation-detail-value {
    color: #fff;
}
/* On the dark panel the arrangement inverts: brass is the commit and the
   charcoal ghost would be invisible, so the secondary becomes a hairline
   button in white. */
#so-booking-widget[data-template="workspace"] .so-bw-confirmation-actions .so-bw-btn--primary {
    background: var(--so-bw-primary);
    border-color: var(--so-bw-primary);
    color: var(--so-bw-on-primary, #fff);
}
#so-booking-widget[data-template="workspace"] .so-bw-confirmation-actions .so-bw-btn--secondary {
    background: transparent;
    border-color: rgba(255, 255, 255, 0.24);
    color: rgba(255, 255, 255, 0.88);
}
#so-booking-widget[data-template="workspace"] .so-bw-confirmation-actions .so-bw-btn--secondary:hover:not(:disabled) {
    border-color: rgba(255, 255, 255, 0.42);
    color: #fff;
}


/* ── 25.9 · The fold, in five steps ──────────────────────────────────────
   Gated on the CONTAINER width (`data-cw`), never the viewport — see
   booking-widget.js _wireContainerWidth. The ID stays OUTSIDE :where() so
   each selector still scores (1,0,0) plus its attributes, the specificity
   the media query it replaces had. */

/* 900 · the two-up bodies stack. Both are still fully on screen, so the
   collapsed form keeps the promise that nothing is hidden behind a step. */
    #so-booking-widget:where([data-cw~="900"])[data-template="workspace"] .so-bw-datetime {
        grid-template-columns: 1fr;
        gap: 14px;
    }
    #so-booking-widget:where([data-cw~="900"])[data-template="workspace"] .so-bw-calendar {
        height: auto;
    }
    #so-booking-widget:where([data-cw~="900"])[data-template="workspace"] .so-bw-slot-panel {
        min-height: 0;
    }
    /* One column: the panes stack, so the rule between them becomes a rule
       ABOVE the money pane and the tint spans the full width. */
    #so-booking-widget:where([data-cw~="900"])[data-template="workspace"] .so-bw-summary {
        grid-template-columns: 1fr;
    }
    #so-booking-widget:where([data-cw~="900"])[data-template="workspace"] .so-bw-summary-main,
    #so-booking-widget:where([data-cw~="900"])[data-template="workspace"] .so-bw-summary-pay {
        grid-column: 1;
    }
    #so-booking-widget:where([data-cw~="900"])[data-template="workspace"] .so-bw-summary-main {
        grid-row: 1;
    }
    #so-booking-widget:where([data-cw~="900"])[data-template="workspace"] .so-bw-summary-pay {
        grid-row: 2;
        border-left: 0;
        border-top: 1px solid var(--so-bw-ds-n200);
    }

/* 820 · THE COLUMN UNPINS. Below roughly 820px there is no width to spend on
   236px of standing navigation — the step column would be under 560px and
   the two-up lists would already have collapsed. So the column becomes a
   horizontal BAND above the header: same content, same tokens, laid on one
   line, and the container gives its reserved padding back.

   And it drops the rows that still say "—". A tall column can afford to
   show the whole running order as a promise of what is coming; a band
   cannot, and four em dashes across the top of a phone is the widget's
   scarcest resource — height — spent on nothing. The `--unset` marker is
   already bound on every row for exactly this, and the `--tally` row (one
   line: "2 services · 1 h 15 m") is revealed to take their place. */
    #so-booking-widget:where([data-cw~="820"])[data-template="workspace"] .so-bw-container {
        padding-left: 0;
        min-height: 0;
    }
    #so-booking-widget:where([data-cw~="820"])[data-template="workspace"] .so-bw-rail {
        position: static;
        width: auto;
        flex-direction: row;
        flex-wrap: wrap;
        align-items: center;
        gap: 10px 18px;
        padding: 14px var(--so-bw-gutter);
        overflow: visible;
    }
    #so-booking-widget:where([data-cw~="820"])[data-template="workspace"] .so-bw-rail-head {
        padding-bottom: 0;
        border-bottom: 0;
        margin-right: auto;
    }
    #so-booking-widget:where([data-cw~="820"])[data-template="workspace"] .so-bw-rail-title {
        font-size: 15px;
    }
    #so-booking-widget:where([data-cw~="820"])[data-template="workspace"] .so-bw-rail-title::after {
        display: none;
    }
    #so-booking-widget:where([data-cw~="820"])[data-template="workspace"] .so-bw-rail-sub {
        display: none;
    }
    #so-booking-widget:where([data-cw~="820"])[data-template="workspace"] .so-bw-rail-list {
        flex: 0 1 auto;
        flex-direction: row;
        flex-wrap: wrap;
        align-items: center;
        gap: 8px 16px;
    }
    #so-booking-widget:where([data-cw~="820"])[data-template="workspace"] .so-bw-rail-row {
        padding: 0;
        border-bottom: 0;
    }
    #so-booking-widget:where([data-cw~="820"])[data-template="workspace"] .so-bw-rail-row--unset {
        display: none;
    }
    #so-booking-widget:where([data-cw~="820"])[data-template="workspace"] .so-bw-rail-row--tally {
        display: flex;
    }
    #so-booking-widget:where([data-cw~="820"])[data-template="workspace"] .so-bw-rail-total {
        margin-top: 0;
        padding-top: 0;
        border-top: 0;
        flex: 0 0 auto;
        gap: 8px;
    }

/* 700 · one-up lists and one-column form. Below this a 320px minimum track
   and a 12px gap no longer both fit, and a half-width text input is a field
   nobody can read back. */
    #so-booking-widget:where([data-cw~="700"])[data-template="workspace"] .so-bw-card-grid {
        grid-template-columns: 1fr;
    }
    /* One declaration, because the form above is a REAL two-column grid with
       `auto` spans and a flattened row: collapsing the tracks is all it takes,
       and neither the groups nor the row need a reset of their own. */
    #so-booking-widget:where([data-cw~="700"])[data-template="workspace"] .so-bw-form {
        grid-template-columns: 1fr;
    }
    #so-booking-widget:where([data-cw~="700"])[data-template="workspace"] .so-bw-step-label {
        font-size: 19px;
    }

/* 560 · the step bar keeps its NUMBERS and gives up its labels. Eight
   labelled steps in a 540px band is either a horizontal scroll nobody
   notices or eight two-line labels 62px tall. The numbers still say where
   the customer is, and the step title directly beneath says what the
   current one is called — which is the half that was doing the work. */
    #so-booking-widget:where([data-cw~="560"])[data-template="workspace"] .so-bw-stepbar-label {
        display: none;
    }
    #so-booking-widget:where([data-cw~="560"])[data-template="workspace"] .so-bw-stepbar-item {
        min-width: 34px;
    }
    #so-booking-widget:where([data-cw~="560"])[data-template="workspace"] .so-bw-stepbar-dot {
        width: 24px;
        height: 24px;
        font-size: 11px;
    }
    #so-booking-widget:where([data-cw~="560"])[data-template="workspace"] .so-bw-stepbar-item:not(:first-child)::before {
        top: 12px;
    }
    #so-booking-widget:where([data-cw~="560"])[data-template="workspace"] .so-bw-header {
        gap: 14px;
        padding-top: 18px;
        padding-bottom: 14px;
    }
    /* The two panel headers no longer have to line up with each other once
       they are stacked, and 62px of empty header above a slot list is a
       third of a phone screen. */
    #so-booking-widget:where([data-cw~="560"])[data-template="workspace"] .so-bw-calendar-header,
    #so-booking-widget:where([data-cw~="560"])[data-template="workspace"] .so-bw-slot-panel-head {
        min-height: 0;
    }
    #so-booking-widget:where([data-cw~="560"])[data-template="workspace"] .so-bw-slot-list {
        grid-template-columns: repeat(auto-fill, minmax(min(100%, 108px), 1fr));
    }

/* 480 · the band keeps the brand line and the total, and the commit bar
   goes two equal buttons. The thumbnail comes down with the row so a
   64px tile plus a 22px check plus the text still leaves the title more
   than half the card. */
    #so-booking-widget:where([data-cw~="480"])[data-template="workspace"] .so-bw-rail-list {
        display: none;
    }
    #so-booking-widget:where([data-cw~="480"])[data-template="workspace"] .so-bw-rail {
        padding-top: 12px;
        padding-bottom: 12px;
    }
    /* Through the token here too, and for the same reason as the 64px block —
       the phone tile was the worse of the two: a 60px mark in a 52px band,
       4px shaved off each side. */
    #so-booking-widget:where([data-cw~="480"])[data-template="workspace"] .so-bw-card-media,
    #so-booking-widget:where([data-cw~="480"])[data-template="workspace"] .so-bw-card-media-avatar,
    #so-booking-widget:where([data-cw~="480"])[data-template="workspace"] .so-bw-card-media .so-bw-card-icon,
    #so-booking-widget:where([data-cw~="480"])[data-template="workspace"] .so-bw-card:not(.so-bw-sp__panel) > .so-bw-card-icon {
        --so-bw-icon-size: 52px;
        width: 52px;
        height: 52px;
    }
    #so-booking-widget:where([data-cw~="480"])[data-template="workspace"] .so-bw-card:not(.so-bw-sp__panel) {
        gap: 11px;
        padding: 11px 12px;
        min-height: 68px;
    }
    #so-booking-widget:where([data-cw~="480"])[data-template="workspace"] .so-bw-step-label {
        font-size: 17.5px;
    }
    #so-booking-widget:where([data-cw~="480"])[data-template="workspace"] .so-bw-confirmation {
        padding: 30px 18px;
    }
    #so-booking-widget:where([data-cw~="480"])[data-template="workspace"] .so-bw-btn {
        flex: 1 1 auto;
    }

/* 380 · the badges stop holding a right-hand column. Below this the price
   pair and a two-line title cannot share 340px without the title breaking a
   word per line, so the badges drop under the text and run inline. */
    #so-booking-widget:where([data-cw~="380"])[data-template="workspace"] .so-bw-card-body {
        grid-template-columns: minmax(0, 1fr);
    }
    #so-booking-widget:where([data-cw~="380"])[data-template="workspace"] .so-bw-card-badges {
        grid-column: 1;
        grid-row: auto;
        flex-direction: row;
        align-items: center;
        justify-content: flex-start;
        margin-top: 2px;
    }


/* ── 25.10 · The three display modes, and Full Width ─────────────────────
   Inline / Popup / Floating Button are `data-position` on the root, and the
   container-width system means the fold above already runs inside a 560px
   dialog or a 420px panel without a viewport ever being consulted. Three
   things do not come free.

   1 · A popup dialog and a floating panel bring their own shell padding and
   their own scroll region, and this template's chrome — a header with a
   step bar plus a docked commit bar — is ~150px before a single card. The
   bar is sticky already (base .so-bw-nav), so nothing is unreachable; the
   header gives back the difference rather than the content doing it. */
#so-booking-widget[data-position="popup"][data-template="workspace"] .so-bw-header,
#so-booking-widget[data-position="floating"][data-template="workspace"] .so-bw-header {
    padding-top: 16px;
    padding-bottom: 14px;
}
#so-booking-widget[data-position="popup"][data-template="workspace"] .so-bw-nav,
#so-booking-widget[data-position="floating"][data-template="workspace"] .so-bw-nav {
    padding-top: 12px;
    padding-bottom: 12px;
}
/* 2 · Neither shell is ever wide enough for the standing column, and both
   cap the widget well under 820px — so the band above is what actually
   renders there, which is correct and needs nothing except saying so. The
   min-height comes off because a 560px floor inside a short floating panel
   is a scroll the content did not ask for. */
#so-booking-widget[data-position="popup"][data-template="workspace"] .so-bw-container,
#so-booking-widget[data-position="floating"][data-template="workspace"] .so-bw-container {
    min-height: 0;
}
/* 3 · Full Width. With no cap on the card, a fixed 236px column becomes a
   smaller and smaller fraction of a very wide surface — at 1900px it is an
   eighth of the screen holding five short lines. It grows with the card
   instead, bounded so it can never become a second pane: this is standing
   navigation, not a Split Canvas.

   BOTH declarations, always together. The column is out of flow, so its
   width and the padding that reserves room for it are two independent
   numbers that have to agree; change one alone and the step column either
   overlaps the column or floats a gap beside it. */
#so-booking-widget[data-size="full"][data-template="workspace"] .so-bw-container {
    padding-left: clamp(236px, 17%, 320px);
}
#so-booking-widget[data-size="full"][data-template="workspace"] .so-bw-rail {
    width: clamp(236px, 17%, 320px);
}
/* …except once the column has unpinned, where there is no reserved track to
   size. (1,3,0) against the pair above at (1,2,0) — a media query would add
   nothing, so the specificity has to come from the selector. */
#so-booking-widget:where([data-cw~="820"])[data-size="full"][data-template="workspace"] .so-bw-container {
    padding-left: 0;
}
#so-booking-widget:where([data-cw~="820"])[data-size="full"][data-template="workspace"] .so-bw-rail {
    width: auto;
}
/* A 1900px workspace can hold a wider list track rather than stretching two
   cards across a metre of screen. */
#so-booking-widget[data-size="full"][data-template="workspace"] .so-bw-card-grid {
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 380px), 1fr));
}


/* ==========================================================================
   23 · THE TWO STUDIO APPEARANCE CONTROLS — SIZE AND CORNER RADIUS
   ==========================================================================

   Both controls were wired end to end and both were partly lying to the
   admin. Measured across all nineteen:

     WIDGET SIZE was NOT MONOTONIC on six templates. The stylesheet gave
     small / large / full an absolute width (660 / 1000 / 100%) that replaced
     the template's own, and gave medium NO RULE AT ALL — so medium meant
     "the template's natural width", which ranges from 380px to 1120px. The
     result was a control that ran backwards:

         Inline        small 660  <- medium 380   +280px WIDER on "Small"
         Card Deck     small 660  <- medium 600    +60px wider
         Conversation  small 660  <- medium 620    +40px wider
         Coral Pop     small 660  <- medium 620    +40px wider
         Wide Canvas   large 1000 <- medium 1120  -120px NARROWER on "Large"
         Tabbed Rail   large 1000 <- medium 1040   -40px narrower

     It also flattened the catalogue: choosing anything but medium collapsed
     all nineteen to the same width, erasing Inline's 380px column and Wide
     Canvas's 1120px canvas — both of which the catalogue describes as the
     point of those templates.

     CORNER RADIUS reached the outer shell and the buttons but not the
     SERVICE CARD, which is the largest rounded surface on the screen and the
     one an admin means by "round the corners". Eleven templates set a card
     radius as a LITERAL, and a literal outranks the token the shortcode
     emits no matter how carefully that token is derived.

   THE FIX, IN ONE IDEA: a size or radius setting SCALES each template's own
   value rather than REPLACING it. Monotonic by construction — it is
   arithmetically impossible for small to exceed medium — and each template
   keeps its proportions, so Inline stays the narrowest and Coral Pop stays
   the roundest at every setting.

   WHY (1,3,0). The per-template widths are `[data-template="x"]
   .so-bw-container` = (1,2,0). Matching on BOTH attributes the widget tag
   always carries gives (1,3,0) and wins without !important — the same trick
   BookingShortcode::render() already uses for the radius tokens, where it is
   documented as load-bearing rather than decoration. If `data-template-id`
   is ever absent the rule simply does not match and the old per-template
   width applies, which is today's behaviour: a graceful fallback, not a
   collapse.                                                                 */

/* ── 23.1 · Each template's natural width, as a token ────────────────────
   Values are the MEASURED computed max-width of each container at the
   default size, so medium is byte-for-byte what shipped before this
   section. The literal `max-width` rules in section 17 are left in place
   deliberately — they are the fallback described above. */
#so-booking-widget[data-template="deck"] .so-bw-container      { --so-bw-tpl-maxw: 600px; }
#so-booking-widget[data-template="soft"] .so-bw-container      { --so-bw-tpl-maxw: 620px; }
#so-booking-widget[data-template="editorial"] .so-bw-container { --so-bw-tpl-maxw: 680px; }
/* Timeline left Editorial's shared 680px when it grew a sidebar: 244px of that
   is now chrome, which would leave a 430px pane for a two-column service row.
   1040px is the reference comp's shell, and it must stay in step with
   BookingShortcode::SHELL_TPL_MAXW — a custom property cannot be read by the
   popup ancestor, so the number genuinely lives in two places. */
#so-booking-widget[data-template="timeline"] .so-bw-container { --so-bw-tpl-maxw: 1040px; }
/* TIMELINE DOES NOT CAP `data-size="full"`, and that is a decision rather than
   an omission. Week Board narrows its own Full Width because seven day columns
   drift apart at 1900px; Timeline was given the same cap and it was wrong —
   Full Width has to mean full width, or the Studio control is a lie on this
   template. The composition holds at any width instead: the sidebar is a fixed
   244px, and the pane's own content carries a reading measure (see "The pane's
   measure" in §17.10) so a 1900px shell does not stretch a service name a
   screen away from its Select button.

   1040px in §23.1 above is the NATURAL width — what small/medium/large scale
   from. Full is deliberately outside that ladder. */
#so-booking-widget[data-template="centred"] .so-bw-container,
#so-booking-widget[data-template="frost"] .so-bw-container     { --so-bw-tpl-maxw: 720px; }
#so-booking-widget[data-template="dusk"] .so-bw-container      { --so-bw-tpl-maxw: 820px; }
#so-booking-widget[data-template="hero"] .so-bw-container,
#so-booking-widget[data-template="daypart"] .so-bw-container   { --so-bw-tpl-maxw: 860px; }
#so-booking-widget[data-template="agenda"] .so-bw-container    { --so-bw-tpl-maxw: 960px; }
#so-booking-widget[data-template="weekboard"] .so-bw-container { --so-bw-tpl-maxw: 1000px; }
/* Tabbed Rail is a desktop workspace and is sized like one. 1040px gave it a
   661px body beside its 320px rail, and MEASURED that is three cards across —
   at the size an admin never changes. The template exists for a customer
   revising a choice, so the catalogue wants to be visible at a glance rather
   than three-at-a-time down a scroller.

   1360px puts the body at ~980px, which the shared card track fits FIVE across
   at the default size (small 3, large 6). The number is a measure, not a
   fraction of the screen — and `min(100%, …)` in the consumer still applies, so
   inside a narrower themed column it can never exceed its parent: an 800px
   column still resolves to an 800px shell and two cards, exactly as before.
   Kept in step with the literal fallback in section 14E, per the note above. */
#so-booking-widget[data-template="tabbed"] .so-bw-container    { --so-bw-tpl-maxw: 1360px; }
/* Wide Canvas carries two columns, so it sits at the wide end of this ramp —
   but it is still a MEASURE, like every other entry, not a fraction of the
   screen. It was briefly 80vw and that read as a full-bleed page section
   rather than as something embedded in a page.
   `min(100%, …)` in the consumer still applies, so inside a narrower themed
   column the card can never exceed its parent. Kept in step with the fallback
   rule in section K, and with the --so-bw-wide-w token in C9-bis §S1. */
#so-booking-widget[data-template="wide"] .so-bw-container      { --so-bw-tpl-maxw: 920px; }
/* The three intent-led architectures (section 24, which sits ABOVE this
   banner precisely so these three lines get the last word). Each value is the
   `max-width` its own block declares, restated here so the size radio scales
   it instead of replacing it — the whole point of 23.1. Adding an
   architecture without a line here silently drops it to the 800px default. */
#so-booking-widget[data-template="bundle"] .so-bw-container    { --so-bw-tpl-maxw: 860px; }
#so-booking-widget[data-template="counter"] .so-bw-container   { --so-bw-tpl-maxw: 900px; }
/* The desktop workspace. The widest natural width in the catalogue, and it
   has to be: the value is the `max-width` its own block declares, so that at
   Small it scales to 944px — still enough for a 236px standing column beside
   a two-up body — rather than being replaced by a 660px cap that would fold
   the architecture away at the setting an admin thinks only makes it tidier. */
#so-booking-widget[data-template="workspace"] .so-bw-container    { --so-bw-tpl-maxw: 1180px; }
/* The message thread (section 36). The NARROWEST in the catalogue, and that is
   the architecture rather than restraint: a two-sided thread run across a
   desktop card puts the question and the answer at opposite ends of the screen
   and stops reading as a conversation.

   Stated here rather than as a plain `max-width` inside section 36, because a
   bare `[data-template="chatbook"] .so-bw-container` rule scores (1,2,0) and
   the 23.1 sizing rule below is (1,3,0) — so the bare form loses silently and
   the template renders at the 800px default, which is precisely the failure
   the banner comment above warns about. Going through the token also means the
   Studio's size radio SCALES the thread (448 / 560 / 700) instead of replacing
   its width with a generic cap. */
#so-booking-widget[data-template="chatbook"] .so-bw-container     { --so-bw-tpl-maxw: 560px; }

/* ── 23.2 · The size radio picks a SCALE, not a width ────────────────────
   0.8 / 1 / 1.25 keeps the same ratios the old absolute ramp had at the
   middle of the catalogue (660 / 820 / 1000), so a template near 820px
   renders within a few pixels of what it did before. */
#so-booking-widget[data-size="small"]  { --so-bw-size-scale: 0.8; }
#so-booking-widget[data-size="medium"] { --so-bw-size-scale: 1; }
#so-booking-widget[data-size="large"]  { --so-bw-size-scale: 1.25; }

#so-booking-widget[data-template][data-template-id] .so-bw-container {
    /* min(100%) is what makes this safe on a phone: the scaled width is a
       ceiling, never a floor, so nothing can push the card wider than its
       column. It also means the mobile breakpoints need no companion rule. */
    max-width: min(100%, calc(var(--so-bw-tpl-maxw, 800px) * var(--so-bw-size-scale, 1)));
}

/* "Full" is a different intent from "large" — edge to edge, not a bigger
   card — so it is stated rather than approximated with a huge scale. */
#so-booking-widget[data-size="full"][data-template][data-template-id] .so-bw-container {
    max-width: 100%;
}

/* ── 23.2b · The size picks a width; Alignment picks where it sits ───────
   `.so-bw-container` is `margin: 0 auto` (section 2), which centres the card
   in whatever spare width the size above left behind. Alignment replaces one
   of those two autos: `margin-right: auto` alone pins the card left, and the
   mirror pins it right.

   Written as `margin-left`/`margin-right` rather than `margin-inline-start`/
   `-end` ON PURPOSE. The logical properties would flip with the host page's
   writing mode, and this control is not a text-direction setting — an admin
   who picks "Left" on an RTL site means the left of the page, which is what
   the Studio preview and the label both show them.

   No `!important` and no extra specificity games: these are (1,1,0) like the
   container's own rule and come later in the file, which is enough. The only
   selector in this stylesheet that fights auto margins is the `[data-size=
   "full"]` break-out, and PHP already resolves alignment to `center` at that
   size so the two can never both apply.

   `center` is emitted rather than left implicit so that switching BACK to it
   is a real rule and not a reliance on the absence of one — the widget root
   is a long-lived element and Alpine re-renders inside it constantly. */
#so-booking-widget[data-align="left"] .so-bw-container {
    margin-left: 0;
    margin-right: auto;
}
#so-booking-widget[data-align="right"] .so-bw-container {
    margin-left: auto;
    margin-right: 0;
}
#so-booking-widget[data-align="center"] .so-bw-container {
    margin-left: auto;
    margin-right: auto;
}

/* Below the narrowest card width the container already fills its column, so
   the autos resolve to zero and alignment is a no-op by arithmetic. Stating
   it as a rule anyway is cheap insurance against a future template that sets
   a fixed `width` (rather than `max-width`) on the container and would
   otherwise strand a left/right-aligned card against one edge of a phone. */
@media (max-width: 640px) {
    #so-booking-widget[data-align] .so-bw-container {
        margin-left: auto;
        margin-right: auto;
    }
}

/* ── ...except where the card IS the architecture ────────────────────────
   Card Deck is a stack: one card with two more peeking above it, drawn as
   ::before/::after inset 12px and 24px from its edges. "Edge to edge" has no
   reading for that — a deck stretched to the content column stops being a
   deck and becomes one slab with two thin slivers over it.

   It compounds downwards, which is what made this a report rather than a
   quibble. The cover band on each service card is `aspect-ratio: 2/1`, so its
   height is a function of the card's width, while the initials mark inside it
   is a fixed 68px. Bounded, that is the proportion section 17.12 describes.
   Unbounded it is not. Measured on this install, inline:

       size     container   service card   cover band   mark
       medium     600px       261 x 225     259 x 130    68px
       large      750px       336 x 263     334 x 167    68px
       full      1132px      ~520 x 265    ~518 x 260    68px

   At full the band is twice the height it was designed for with the same
   68px mark adrift in the middle of it, and the name and badges are pushed
   into the last quarter of a card that is mostly empty. That is the "too big,
   not showing full details" shape of the complaint: nothing is missing, it is
   just been pushed out of proportion by a width the template never accepts.

   So Full stays the widest this template goes, rather than the widest the
   column goes: the scale ladder (0.8 / 1 / 1.25) simply continues to 1.5.
   It is still visibly larger than Large, so the control does something —
   which is the whole reason "full" was stated separately in the first place.

   Same specificity as the rule above, so this must stay AFTER it. */
#so-booking-widget[data-size="full"][data-template="deck"][data-template-id] .so-bw-container {
    max-width: min(100%, calc(var(--so-bw-tpl-maxw, 600px) * 1.5));
}

/* ── 23.3 · Card corners follow the slider, proportionally ───────────────
   Seven templates already read `--so-bw-card-radius` and needed nothing:
   Centred, Split Canvas, Hero, Tabbed Rail, Week Board, Daypart, Dual Month.

   Conversation is deliberately absent. Its 999px service pill is a SHAPE,
   not a radius on a scale — the same reason badges and avatars ignore this
   control. A square chat bubble is not a sharper chat bubble, it is a
   different template.

   Everything below derives from `--so-bw-radius` (the base rung, 8px stock)
   rather than `--so-bw-card-radius`, because Coral Pop re-declares the card
   token on its own container: deriving from the derived token would read 22
   as the base and render a 40px corner at the default setting. */

/* EVERY selector below carries `:not(.so-bw-sp__panel)`, for two reasons and
   both matter. Semantically it keeps the staff-profile popup — which shares
   `.so-bw-card` — on its own corner, which is what the section 17 rules use
   the guard for. Structurally it is what makes these rules bind at all: the
   existing per-template card rules already carry it, and a `:not()` adds the
   specificity of its argument, so they score (1,3,0). The first draft of this
   section omitted it, scored (1,2,0), and was silently outranked on five of
   the eleven templates — Card Deck, Frost, Inline, Editorial and Wide Canvas
   all still measured frozen while Coral Pop (whose rule has no guard) moved.
   Half the fix working is how a specificity mistake disguises itself. */

/* The rounded three. Multipliers are today's literal over the 8px base, so
   the default setting reproduces the current corner exactly. */
#so-booking-widget[data-template="deck"] .so-bw-card:not(.so-bw-sp__panel) {
    border-radius: calc(var(--so-bw-radius, 8px) * 1.75);   /* was 14px */
}
#so-booking-widget[data-template="frost"] .so-bw-card:not(.so-bw-sp__panel) {
    border-radius: calc(var(--so-bw-radius, 8px) * 2);      /* was 16px */
}
#so-booking-widget[data-template="soft"] .so-bw-card:not(.so-bw-sp__panel) {
    border-radius: calc(var(--so-bw-radius, 8px) * 2.75);   /* was 22px */
}
/* The three from section 24. Multipliers over the 8px base rung reproduce
   each block's own literal exactly at the default setting, so adding these
   changes nothing until the slider is touched — and then it moves, which is
   what an admin dragging it is asking for.

   Counter Sand's 14px is not a decoration to be traded away: at kiosk scale a
   sharp 96px tile reads as a panel rather than as a button. It still tracks
   the slider, from a corner that is already generous. */
#so-booking-widget[data-template="bundle"] .so-bw-card:not(.so-bw-sp__panel) {
    border-radius: calc(var(--so-bw-radius, 8px) * 1.5);    /* was 12px */
}
#so-booking-widget[data-template="counter"] .so-bw-card:not(.so-bw-sp__panel) {
    border-radius: calc(var(--so-bw-radius, 8px) * 1.75);   /* was 14px */
}
/* And the desktop workspace. Its list rows inherit --so-bw-card-radius from
   the design system (12px at the base rung), so the multiplier restates that
   exactly: nothing moves until the slider does. */
#so-booking-widget[data-template="workspace"] .so-bw-card:not(.so-bw-sp__panel) {
    border-radius: calc(var(--so-bw-radius, 8px) * 1.5);    /* was 12px */
}
/* AGENDA MOVED HERE FROM THE SHARP SEVEN BELOW, and the move is a consequence
   rather than a preference. It was square because its rows were not boxes:
   `border: 0; background: transparent`, separated by hairlines, where a radius
   would have bent each separator into hooks at both ends and rounded nothing.
   Section 17.6 C11 now draws the row as the same white bordered card its own
   day list uses (--so-bw-ds-radius-card, 12px at the base rung), so 1.5 is
   that literal over the 8px base — the two lists agree at the default setting
   and track the slider together from there.
   Stated ABOVE the sharp-seven rule and outside its selector list: same
   specificity, so whichever comes last would win, and there must be exactly
   one answer for this template. */
#so-booking-widget[data-template="agenda"] .so-bw-card:not(.so-bw-sp__panel) {
    border-radius: calc(var(--so-bw-radius, 8px) * 1.5);    /* = --so-bw-ds-radius-card */
}

/* The sharp four — seven, before Agenda's card grew a border of its own and
   Dialog and Inline were withdrawn. A square card is these templates'
   signature, so they must
   stay square at the default AND at every setting below it — but an admin who
   drags the slider up is asking for round, and getting nothing back reads as
   a broken control. Subtracting the base rung gives exactly that: 0 at 8 and
   below, rising only above it. max() clamps the negative rather than letting
   an invalid length drop the whole declaration. */
#so-booking-widget[data-template="wide"] .so-bw-card:not(.so-bw-sp__panel),
#so-booking-widget[data-template="timeline"] .so-bw-card:not(.so-bw-sp__panel),
#so-booking-widget[data-template="dusk"] .so-bw-card:not(.so-bw-sp__panel),
#so-booking-widget[data-template="editorial"] .so-bw-card:not(.so-bw-sp__panel) {
    border-radius: max(0px, calc(var(--so-bw-radius, 8px) - 8px));
}

/* ==========================================================================
   17.10b · TIMELINE — THE CHOICE STEP, RECONCILED
   --------------------------------------------------------------------------
   WHY THIS EXISTS SEPARATELY FROM 17.10.

   17.10 states Timeline's choice step: flat diary rows, no card chrome, no
   media band, borderless mono badges, and the duration bar that is the whole
   reason the template has a service step of its own. It was written before
   the catalogue-wide service-avatar section (search "MECHANISM A · the avatar
   row"), which opted Timeline into the avatar treatment and re-laid the card
   as a grid. That section is later in source at equal or higher specificity,
   so it won — and it won MORE than it meant to. It intended to add a 44px
   circle to a list; what landed was the generic bordered card, with the
   duration bar orphaned on its own row under a three-line description where
   it reads as a stray rule rather than as a measurement.

   The result was a Timeline choice step indistinguishable from Agenda's, on a
   template whose entire argument is that a duration is a LENGTH, not a number.

   This block does not fight either predecessor with !important. It sits after
   both, at the same specificity, and states the reconciled outcome once:
   KEEP the avatar (identification in a one-column list is a good idea and the
   catalogue should be consistent about it), DROP the card chrome, and make the
   bar and its minutes one object on one line.

   The row reads: avatar, then name over description, then the bar with the
   duration set immediately at its end, with price beneath and the commit
   control pulled to the right edge and centred against the whole row.

        (DC)  Dental Cleaning                          [ Select ]
              Professional teeth cleaning, polishing…
              ████████████░░░░░░░░░░  45 min · ₹1,200.00

   THE BAR IS DRAWN AGAINST THE VISIBLE SET, not the catalogue — see
   _longestServiceDuration in booking-widget.js. The two halves have to agree:
   a bar scaled to a service the customer cannot see is a bar that lies.
   ========================================================================== */

/* ── The row · a card, not a hairline ────────────────────────────────────
   This is the ONLY place the service row's surface is stated. It scores
   (1,3,0), so it beats any `.so-bw-card` rule in 17.10 regardless of order —
   which is why 17.10 now points here instead of restating it.

   It used to be a flat row separated by hairlines, and that was right for a
   680px shell: a border round every row read as noise when the rows were the
   only thing on screen. At 1040px with a sidebar beside them, an unbordered row
   has nothing to sit in — the name sits at the far left and the commit control
   at the far right with 700px of unclaimed white between them, and the eye
   stops pairing the two. The comp draws them as cards; so does this. */
#so-booking-widget[data-template="timeline"] .so-bw-card--service:not(.so-bw-sp__panel) {
    border: 1px solid var(--so-bw-ds-n200);
    border-radius: var(--so-bw-ds-radius-card);
    background: var(--so-bw-ds-n0);
    box-shadow: none;
    padding: 14px 16px !important;
    /* Beats the catalogue-wide `.so-bw-card--service { padding: 0 !important }`
       (hazard 2). That rule exists to stop a centred stack keeping the generic
       card's padding; here the row IS the card and needs its own inset, so the
       override is stated with the reason rather than copied. */
    /* The avatar section's grid, restated with a third column so the commit
       control can span every row instead of being pushed below the bar. */
    grid-template-columns: auto minmax(0, 1fr);
}
/* HOVER MUST NOT LOOK LIKE SELECTED. The catalogue-wide card hover adds an
   accent TINT as an inset shadow (section "CARD HOVER"), and selected is a 15%
   accent wash — on a bordered card the two landed close enough that a hovered
   row read as chosen: reported from a screenshot where the first card appeared
   selected while the footer still said "No services selected".

   So hover keeps the accent BORDER and gives the tint back: a neutral n25
   surface plus a coloured edge. Selected keeps the wash and fills its button.
   The inset shadow is zeroed rather than overridden with a colour, because the
   platform rule paints it as a shadow and only a shadow can cancel it. */
#so-booking-widget[data-template="timeline"] .so-bw-card--service:not(.so-bw-sp__panel):hover {
    border-color: var(--so-bw-primary);
    background: var(--so-bw-ds-n25);
    box-shadow: none;
}
/* Selected adds a mark on the left edge — the same node-and-line vocabulary the
   day axis uses for a chosen opening.

   IT HAS TO BE A PSEUDO-ELEMENT, and that is not a stylistic choice. The
   platform owns the selected state with two catalogue-wide `!important`
   declarations that no per-template rule can outrank at any specificity:

     `.so-bw-card.so-bw-card--selected { box-shadow: none !important }`
        — so an inset box-shadow edge mark silently does nothing, and
     `background: color-mix(… var(--so-bw-primary) 15% …) !important`
        — the accent tint, which carries the selected INK with it and was put
          there to fix a reported white-text-on-tint fault.

   Both are deliberate and both are load-bearing, so this works with the tint
   rather than against it: the row keeps the platform's selected background and
   ink, and gains an edge mark the platform has no opinion about. ::after also
   costs no layout — a real border-left would shift every line 3px on click. */
/* (The 3px left edge-mark that used to sit here is gone. It existed because a
   flat row had no border to colour; now that the row is a card, the accent
   border below says the same thing once instead of twice.) */
/* Selected: the accent moves to the BORDER, which the platform has no opinion
   about. It cannot be a box-shadow — `.so-bw-card--selected { box-shadow: none
   !important }` is catalogue-wide and unbeatable at any specificity — and it
   must not be a background, because the 15% accent tint is `!important` too and
   carries the selected ink with it (see the note above). A border-colour change
   costs no layout, since the border is already 1px in the resting state. */
#so-booking-widget[data-template="timeline"] .so-bw-card--service:not(.so-bw-sp__panel).so-bw-card--selected {
    border-color: var(--so-bw-primary);
}

/* ── The body · title over description, bar and figures on one line ──────
   Three columns: the text and the bar take the first, the figures sit against
   the bar's end in the second, and the commit control holds the third for the
   full height of the row. Everything above the bar spans the first two so a
   long service name is not squeezed into a column sized by "45 min". */
#so-booking-widget[data-template="timeline"] .so-bw-card--service:not(.so-bw-sp__panel) .so-bw-card-body {
    display: grid;
    /* Text · figures · commit. Three columns, read left to right, with the
       figures and the button each holding their own track for the full height
       of the row so they land in the same place on a card with a description
       and one without. */
    grid-template-columns: minmax(0, 1fr) auto auto;
    /* ROW 2 TAKES THE SLACK, and this one line is what puts the description
       under its own title instead of 45px below it.
       The figures and the Select button both span every row. When all rows are
       intrinsically sized, a spanning item's height is shared out among them,
       and Chrome handed most of the 44px button to row 1 — so row 1 became 44px
       tall for a 20px service name and the description started below all of it.
       `1fr` on row 2 gives the spanning items somewhere to put their surplus:
       row 1 collapses to the title's own min-content and row 2 absorbs the
       rest. Measured: title→description gap 45px → 4px, card 143px → ~97px. */
    grid-template-rows: min-content 1fr;
    column-gap: 24px;
    row-gap: 2px;
    /* start, not center: the comp sets the duration on the SAME line as the
       service name, with the price under it beside the description. Centring
       floated the whole figures column and dragged the description down with
       it, because a row-spanning item's height is shared out over every row it
       crosses. */
    align-items: start;
}
#so-booking-widget[data-template="timeline"] .so-bw-card--service:not(.so-bw-sp__panel) .so-bw-card-title,
#so-booking-widget[data-template="timeline"] .so-bw-card--service:not(.so-bw-sp__panel) .so-bw-card-meta,
#so-booking-widget[data-template="timeline"] .so-bw-card--service:not(.so-bw-sp__panel) .so-bw-card-more {
    grid-column: 1;
}
#so-booking-widget[data-template="timeline"] .so-bw-card--service:not(.so-bw-sp__panel) .so-bw-card-more {
    justify-self: start;
}
/* THE DURATION BAR IS NOT DRAWN HERE. C15 is Timeline's own invention and it
   is still built — the getter, the scale and the fill all remain — but the
   approved comp states the duration in words only, and a bar per row is not in
   it. Hidden rather than deleted: the mechanism is one rule away if the design
   wants it back, and `usesDurationBars` still gates it everywhere else.

   Consequence, recorded because it is a real cost: the bar was the strongest
   greyscale identifier this template had — the only place in the catalogue a
   duration is drawn as a LENGTH. Identity now rests on the sidebar's numbered
   rail and the day axis at step 5. */
#so-booking-widget[data-template="timeline"] .so-bw-card-bar {
    display: none;
}
/* Duration over price, in their own column — the comp stacks them rather than
   running them together on one line. */
#so-booking-widget[data-template="timeline"] .so-bw-card--service:not(.so-bw-sp__panel) .so-bw-card-badges {
    grid-column: 2;
    /* SPAN EVERY ROW, and align to the top. A row-spanning item only inflates
       tracks when it is TALLER than what they already hold: the text column
       (title + two-line description ≈ 55px) out-measures this stack (≈ 36px),
       so spanning costs nothing and the two columns flow independently — which
       is the comp's alignment, duration level with the service name and price
       level with the description.
       Pinning it to row 1 instead made row 1 as tall as the whole stack and
       pushed the description ~45px clear of its own title; centring it did the
       same and dragged both columns apart. */
    grid-row: 1 / -1;
    align-self: start;
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 4px;
    margin: 0;
    padding-top: 0;
    white-space: nowrap;
}
#so-booking-widget[data-template="timeline"] .so-bw-card--service:not(.so-bw-sp__panel) .so-bw-badge--duration {
    /* Sans, not mono. The comp sets both figures in the body face; mono was
       C15's pairing with the bar, and the bar is gone. Tabular figures stay so
       the column still aligns down the list. */
    font-family: inherit;
    font-variant-numeric: tabular-nums;
    font-size: 13px;
    font-weight: 500;
    line-height: 1.2;
    color: var(--so-bw-ds-n500, var(--so-bw-text-secondary));
}
/* The price is the louder of the two figures in the comp — dark and bold under
   a quiet grey duration. */
#so-booking-widget[data-template="timeline"] .so-bw-card--service:not(.so-bw-sp__panel) .so-bw-badge--price,
#so-booking-widget[data-template="timeline"] .so-bw-card--service:not(.so-bw-sp__panel) .so-bw-badge--free {
    font-family: inherit;
    font-variant-numeric: tabular-nums;
    font-size: 15px;
    font-weight: 700;
    line-height: 1.2;
    color: var(--so-bw-text);
}
/* Tighter rows. The diary reads as a run of entries, which it cannot do when
   each one is 165px tall — that is a card's proportions, not a list's. */
#so-booking-widget[data-template="timeline"] .so-bw-card--service:not(.so-bw-sp__panel) .so-bw-card-top {
    padding-top: 12px;
    padding-bottom: 12px;
}
#so-booking-widget[data-template="timeline"] .so-bw-card--service:not(.so-bw-sp__panel) .so-bw-card-media {
    margin-top: 12px;
}
/* The two-line reserve on the description, and the reserved Read more row, buy
   CARD-HEIGHT PARITY — a one-line description must not leave its card shorter
   than the one beside it. That is a grid problem. Timeline's choice step is a
   single column of separator-delimited rows, where there is no "card beside
   it" and nothing to be level with, so the reservation is pure height: ~19px
   of empty box plus a blank Read more row on every service that does not need
   one. Released here and only here; every grid template keeps it.

   The clamp itself stays — a runaway description still has to be bounded, and
   Read more still appears on the services that are actually truncated. */
#so-booking-widget[data-template="timeline"] .so-bw-card--service:not(.so-bw-sp__panel) .so-bw-card-meta--clamp {
    min-height: 0;
}
/* The TITLE carries the same two-line reserve, for the same reason and with the
   same answer. `min-height: 39px` levels a one-line name with a two-line one so
   cards in a multi-column grid stay flush; this template is a single column,
   there is no card beside it to be level with, and the reserve was 20px of
   empty box on every service whose name fits one line. Measured: it was half of
   why the description sat 65px below its own title.
   The clamp stays — a runaway name still has to be bounded at two lines. */
#so-booking-widget[data-template="timeline"] .so-bw-card--service:not(.so-bw-sp__panel) .so-bw-card-title {
    min-height: 0;
}
/* The figures are TEXT here, not controls. The catalogue gives every badge a
   30px min-height because badges are usually chips with a background and a
   border — tappable-looking things that need a floor. §17.10 already strips the
   background, the border and the padding from these two, so the floor was
   reserving 30px for a 16px line and stacking two of them into a 64px column
   that set the whole row's height. Nothing here is interactive, so nothing
   needs the floor. */
#so-booking-widget[data-template="timeline"] .so-bw-card--service:not(.so-bw-sp__panel) .so-bw-card-badges .so-bw-badge {
    min-height: 0;
}
#so-booking-widget[data-template="timeline"] .so-bw-card--service:not(.so-bw-sp__panel) .so-bw-card-more--ghost {
    display: none;
}
/* The commit control holds its own column for the full height of the row, so
   it lands in the same place on a card with a description and one without.

   `.so-bw-card-pick` is `display: none` catalogue-wide (section 12b) and only
   Wide Canvas and Week Board opt back in, so this has to turn it ON rather
   than merely place it — positioning a hidden element leaves the row with no
   commit control at all, which is what happened when the tick below was hidden
   first. Same worded vocabulary as Week Board's, quieter: on a flat diary row
   a filled button per line would out-shout the bars, so the resting state is
   an outline and only the chosen row fills. */
#so-booking-widget[data-template="timeline"] .so-bw-card--service:not(.so-bw-sp__panel) .so-bw-card-pick {
    grid-column: 3;
    grid-row: 1 / -1;
    align-self: center;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    /* Sized to the comp, and to the design system's 44px tap floor rather than
       under it — `--so-bw-ds-tap` is a whole-file floor, not a mobile-only one,
       and this is a control the customer has to hit. Stated HERE and only here:
       this selector is (1,4,0) and beats anything written in §17.10, so a
       second description of the button up there would lose silently. */
    min-width: 120px;
    min-height: var(--so-bw-ds-tap, 44px);
    padding: 0 16px;
    border: 1px solid var(--so-bw-ds-n200);
    border-radius: var(--so-bw-ds-radius-chip);
    background: var(--so-bw-ds-n0);
    color: var(--so-bw-text);
    font-family: inherit;
    font-size: 14px;
    font-weight: 600;
    line-height: 1;
    white-space: nowrap;
    cursor: pointer;
    transition: background .15s, border-color .15s, color .15s;
}
#so-booking-widget[data-template="timeline"] .so-bw-card--service:not(.so-bw-sp__panel) .so-bw-card-pick:hover {
    border-color: var(--so-bw-primary);
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}
#so-booking-widget[data-template="timeline"] .so-bw-card--service:not(.so-bw-sp__panel) .so-bw-card-pick--on {
    background: var(--so-bw-primary);
    border-color: var(--so-bw-primary);
    color: var(--so-bw-on-primary, #fff);
}
#so-booking-widget[data-template="timeline"] .so-bw-card--service:not(.so-bw-sp__panel) .so-bw-card-pick--on:hover {
    color: var(--so-bw-on-primary, #fff);
}
/* The floating tick is redundant once the row carries a labelled
   Select/Selected control AND a plum edge mark — three signals for one state. */
#so-booking-widget[data-template="timeline"] .so-bw-card--service:not(.so-bw-sp__panel) .so-bw-card-check {
    display: none;
}

/* Narrow: the figures drop under the bar rather than stealing width from it —
   a 90px column beside a bar on a 360px phone leaves the bar too short to
   express anything. The commit control goes full width beneath. */

    #so-booking-widget:where([data-cw~="560"])[data-template="timeline"] .so-bw-card--service:not(.so-bw-sp__panel) .so-bw-card-body{
        grid-template-columns: minmax(0, 1fr);
    }
    #so-booking-widget:where([data-cw~="560"])[data-template="timeline"] .so-bw-card--service:not(.so-bw-sp__panel) .so-bw-card-title,
    #so-booking-widget:where([data-cw~="560"])[data-template="timeline"] .so-bw-card--service:not(.so-bw-sp__panel) .so-bw-card-meta,
    #so-booking-widget:where([data-cw~="560"])[data-template="timeline"] .so-bw-card--service:not(.so-bw-sp__panel) .so-bw-card-more,
    #so-booking-widget:where([data-cw~="560"])[data-template="timeline"] .so-bw-card--service:not(.so-bw-sp__panel) .so-bw-card-bar,
    #so-booking-widget:where([data-cw~="560"])[data-template="timeline"] .so-bw-card--service:not(.so-bw-sp__panel) .so-bw-card-badges,
    #so-booking-widget:where([data-cw~="560"])[data-template="timeline"] .so-bw-card--service:not(.so-bw-sp__panel) .so-bw-card-pick{
        grid-column: 1;
        grid-row: auto;
    }
    #so-booking-widget:where([data-cw~="560"])[data-template="timeline"] .so-bw-card--service:not(.so-bw-sp__panel) .so-bw-card-badges{
        flex-direction: row;
        align-items: baseline;
        justify-content: flex-start;
        gap: 10px;
    }
    #so-booking-widget:where([data-cw~="560"])[data-template="timeline"] .so-bw-card--service:not(.so-bw-sp__panel) .so-bw-card-pick{
        justify-self: stretch;
        margin-top: 8px;
    }


/* ==========================================================================
   17.2b · WIDE CANVAS — THE CHOICE STEP, MEASURED
   --------------------------------------------------------------------------
   The same argument as 17.10b, made for a different architecture.

   Wide Canvas is not a diary; it is a COMPARISON TABLE — a flex row per
   service, text left, the row's right-hand fact held to the right. So this is
   a port, not a copy: none of Timeline's flat-diary treatment comes across
   (Wide already has hairline rows), and the avatar, the table grid and the
   right-hand alignment are all left exactly as they are.

   What does come across is the part that is about TIME rather than about
   Timeline: a duration is a length, not a number, and this template — the
   densest in the catalogue, the one built to be scanned — is where a column
   of comparable bars pays best.

   The bar is drawn against the VISIBLE set (see _longestServiceDuration in
   booking-widget.js), so it re-fits when the category chips or search narrow
   the list. Both templates read the same getter; neither can drift.
   ========================================================================== */

/* The bar goes in column 1, under the name and description, where the row's
   own text lives. NOT in column 2 with the figures: that column is pinned to
   the right edge and shrink-wraps its contents, so a bar in it would be a
   different length on every row for reasons that have nothing to do with
   duration — which is precisely the misreading the bar exists to prevent. */
#so-booking-widget[data-template="wide"] .so-bw-card--service:not(.so-bw-sp__panel) .so-bw-card-bar {
    display: block;
    grid-column: 1;
    width: clamp(120px, 40%, 260px);
    height: 5px;
    margin: 4px 0 0;
    border-radius: 999px;
    background: var(--so-bw-ds-n100);
    overflow: hidden;
}
#so-booking-widget[data-template="wide"] .so-bw-card--service:not(.so-bw-sp__panel) .so-bw-card-bar-fill {
    display: block;
    height: 100%;
    width: var(--so-bw-bar-pct, 0%);
    border-radius: 999px;
    background: var(--so-bw-primary);
    transition: width var(--so-bw-ds-motion);
}
/* The duration joins the bar's scale, so it is set in the same tabular mono
   the rest of this template's figures use and sits with the price in column 2
   — the table's right-hand fact stays the table's right-hand fact. */
#so-booking-widget[data-template="wide"] .so-bw-card--service:not(.so-bw-sp__panel) .so-bw-badge--duration {
    font-variant-numeric: tabular-nums;
}
/* The figures share the bar's left margin.
   The service step re-lays the body as a flex COLUMN (search
   `.so-bw-step--service .so-bw-card-body`), which quietly retires the
   comparison-table grid this template documents further up: `grid-column: 2`
   and `justify-self: end` are grid properties and do nothing to a flex item,
   so the duration and price were left sitting ~360px in from the text they
   describe, at an indent decided by nothing. Now that a bar annotates the
   duration, that gap reads as a mistake rather than as a column. Both are
   pulled back onto the same left margin as the title, description and bar. */
#so-booking-widget[data-template="wide"] .so-bw-card--service:not(.so-bw-sp__panel) .so-bw-card-badges {
    align-self: flex-start;
    justify-self: start;
    flex-direction: row;
    align-items: baseline;
    gap: 10px;
    margin: 2px 0 0;
    padding-top: 0;
}

/* ── The step progress bar stays HIDDEN ──────────────────────────────────
   This slot held a rule that brought the slim progress bar back for Wide
   Canvas with `display: block !important` — needed to outrank the catalogue's
   global `.so-bw-progress { display: none !important }` (~7767), whose own
   comment reads "the numbered step bar replaces the old slim progress bar
   everywhere".

   Removed, because the two say the same thing. The step bar directly above it
   already names every step, numbers them, ticks the done ones and marks the
   current one; a 280px fill underneath adds no fact the customer does not have
   and, measured on the live render, read as a stray accent rule floating under
   the stepper with nothing aligned to it. On a compact embedded widget the
   space it costs is the more valuable thing.

   Deleting it rather than re-hiding it: a `display: none` here would be a
   third declaration fighting over one element, and the global kill already
   does the job the moment nothing overrides it. */


/* NB SECTIONS 17 AND 18 ARE DELIBERATELY ABOVE THIS BANNER.
   The popup section has to stay LAST in source order: several of its rules
   beat a per-template rule of equal specificity on source order alone, and a
   section appended after it would break them silently — which is exactly what
   happened when section 17 was first written and appended to the end of the
   file. widget's staff-profile-popup.test.ts asserts the ordering, and it
   caught it. Anything new goes above this line. */
/* ==========================================================================
   DUSK — HOW MUCH OF THE CATALOGUE YOU CAN SEE
   --------------------------------------------------------------------------
   WHY IT SITS HERE, ABOVE THE PROFILE-POPUP BLOCK RATHER THAN AT THE END.
   Two invariants are asserted about everything after that popup block:
   tests/unit/staff-profile-popup.test.ts requires it to be the LAST section
   (several of its rules outrank a per-template rule of equal specificity on
   source order alone), and requires nothing after it to hardcode a colour —
   and this section states the Dusk canvas as a literal in three places where
   no token can reach (an ancestor shell cannot read a descendant's custom
   properties). Both hold with the section placed here.

   The cost of being earlier is that "later in source wins the tie" is no
   longer free against the BOUNDED SHELL block below, so the two rules that
   relied on it — the sticky search field's background and the shell height
   caps — say so at (1,3,0) and !important instead. Both are commented in
   place.

   EVERY SELECTOR IN THIS SECTION IS GATED ON DUSK. The one exception is the
   base rule that HIDES the new `.so-bw-rail-row--tally` markup, which exists
   so the other eighteen templates never render it — the whole point of it
   being there. Nothing else here can match another template, by construction.

   THE MEASUREMENT THIS ANSWERS. At 1440x900, medium, inline, with a hostile
   eight-service catalogue: the container is bounded at 760px, of which the
   header took 155, the docked rail 85 and the commit bar 80. That left 438px
   of scroller, and the step's own furniture (heading, search field, padding)
   took another 136 of THAT — so 302px of actual card area against a 169px
   card. 1.7 services visible. On a 360x740 phone the same arithmetic gave a
   141px scroller against a 165px card: not one whole service on screen, ever.

   Three things were spending the height and none of them was the list:

     THE FOOTER WAS TWO STACKED BARS. A summary strip and a commit bar, one
     above the other, each with its own padding and its own border. Docked
     along the bottom is Dusk's idea (section 14) and it stays — but as ONE
     bar, with the running total on the left and Continue on the right, which
     is what a strip that is already horizontal was always shaped for.

     THE STRIP LISTED FOUR ROWS THAT SAID "—". Service / Specialist / Date /
     Time are the shared rail's running order, drawn for the templates that
     dock it as a tall COLUMN. On a horizontal bar, before the customer has
     chosen anything, that is four em dashes and their labels. They come back
     one at a time as they are actually answered, and the bundle itself —
     "3 services · 2 h 15 m" — takes the space they were holding.

     THE CARD SPENT A WHOLE ROW ON "READ MORE". A 32px line under the
     description, on every card in the list, holding one 9-character link.
     It moves onto the badge line beside the price, which had the room.

   None of this repaints anything. Same tokens, same champagne, same
   hairlines, same flow — the list just gets the height back.

   AND ONE THING THAT WAS NEVER PLACED AT ALL. Naming the grid rows exposed a
   bug the four-row layout had been hiding: the container has NINE in-flow
   children, not four. The status banners (membership, hold timer, error,
   payment retry) sit between the header and the body in the DOM and were
   never given a row, so they auto-flowed into implicit rows AFTER the nav —
   measured at y=705 with the footer ending at 663, two banners overlapping
   each other in the same cell, a pale-yellow "Slot held for 6:00" strip
   hanging off the bottom of an ink-dark card. 26.2 places all nine.
   ========================================================================== */

/* ── 26.1 · The tally row belongs to Dusk and to nothing else ─────────────
   booking-widget.js renders `.so-bw-rail-row--tally` inside the shared rail
   for every template, because the rail is one component. This is the rule
   that makes that safe: hidden at (1,1,0) everywhere, revealed at (1,2,0)
   below for the one template whose rail is a horizontal strip with room for
   it. The eighteen others render exactly the markup they rendered before.

   The `--unset` marker the same commit adds to the four value rows has NO
   base rule on purpose — it is a hook, and 26.4 is its only consumer. A
   template that wants a fixed four-row running order keeps one. */
#so-booking-widget .so-bw-rail-row--tally { display: none; }

/* ── 26.2 · One footer, not two ──────────────────────────────────────────
   Section 14 laid the container out as four stacked rows (header · body ·
   rail · nav). The rail and the nav become ONE row from 600px of container
   width up: the strip takes the free column, the commit bar takes an `auto`
   one sized to its buttons.

   Named areas rather than `grid-row` numbers, because the two layouts differ
   in their ROW COUNT as well as their placement — with numbers, the wide case
   would need every child restated to renumber it, and a child that was missed
   would land in the wrong row silently. `grid-area` also resets the
   `grid-row` the section-14 rules set, so those do not have to be unwound.

   Below 600px the two go back to being stacked, which is not a fallback but
   the correct answer: a phone cannot give a total, a duration and two buttons
   one line each other can read. */
#so-booking-widget[data-template="dusk"] .so-bw-container {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto auto auto auto minmax(0, 1fr) auto auto auto auto;
    grid-template-areas:
        "head"
        "err"
        "member"
        "hold"
        "body"
        "retry"
        "rail"
        "nav"
        "powered";
}
/* All nine in-flow children, named. Enumerated rather than left to
   auto-placement, because auto-placement is what put the status banners below
   the footer: `grid-template-areas` describes a finite grid, and an item with
   no area lands in an implicit row appended AFTER the last one named.

   The four banner rows are `auto` and every banner is `display: none` until it
   has something to say, so an empty row is a zero-height row — the layout with
   nothing active is pixel-identical to the four-row one this replaces.

   Not listed, and correctly so: `.so-bw-loading` (absolute), `.so-bw-sr-only`
   (absolute), the two `.so-bw-sp` popups (fixed) and the x-if `<template>`
   (display: none) are not grid items at all. */
#so-booking-widget[data-template="dusk"] .so-bw-header        { grid-area: head; }
#so-booking-widget[data-template="dusk"] .so-bw-error         { grid-area: err; }
#so-booking-widget[data-template="dusk"] .so-bw-member-banner { grid-area: member; }
#so-booking-widget[data-template="dusk"] .so-bw-hold-timer    { grid-area: hold; }
#so-booking-widget[data-template="dusk"] .so-bw-body          { grid-area: body; }
#so-booking-widget[data-template="dusk"] .so-bw-pay-retry     { grid-area: retry; }
#so-booking-widget[data-template="dusk"] .so-bw-rail          { grid-area: rail; }
#so-booking-widget[data-template="dusk"] .so-bw-nav           { grid-area: nav; }
#so-booking-widget[data-template="dusk"] .so-bw-powered       { grid-area: powered; }

    /* Container width, not viewport width — a 400px floating panel on a 1440px
       desktop has to take the stacked footer, and a 768px tablet in Full Width
       has to take the side-by-side one. data-cwmin is stamped from the
       measured container (see _wireContainerWidth), so both get what their own
       box can carry rather than what the screen behind it can. */
    #so-booking-widget:where([data-cwmin~="600"])[data-template="dusk"] .so-bw-container {
        grid-template-columns: minmax(0, 1fr) auto;
        grid-template-rows: auto auto auto auto minmax(0, 1fr) auto auto auto;
        grid-template-areas:
            "head    head"
            "err     err"
            "member  member"
            "hold    hold"
            "body    body"
            "retry   retry"
            "rail    nav"
            "powered powered";
    }
    /* The seam between the two halves of the bar. Only in the side-by-side
       layout — stacked, they are separated by the nav's own top border and a
       second line would double it. */
    #so-booking-widget:where([data-cwmin~="600"])[data-template="dusk"] .so-bw-nav {
        border-left: 1px solid var(--so-bw-border);
        justify-content: flex-end;
    }

/* ── 26.3 · The strip, at strip height ───────────────────────────────────
   It was drawing at 85px on a desktop and 198px on a phone — the phone case
   because section 14 stacks the rows below 560px, which was the right answer
   for four rows of key/value and is the wrong shape for one line of tally.
   With 26.4 leaving at most two rows to show, it can stay horizontal at every
   width and simply wrap when it has to.

   The vertical padding is fluid on the container so the bar tightens with the
   box rather than at a breakpoint; the horizontal padding reads --so-bw-gutter
   so the strip keeps the same left edge as the header and the list above it. */
#so-booking-widget[data-template="dusk"] .so-bw-rail {
    min-width: 0;
    align-content: center;
    gap: 2px 18px;
    padding: clamp(8px, calc(var(--so-bw-cw, 820px) * 0.013), 12px) var(--so-bw-gutter);
}
#so-booking-widget[data-template="dusk"] .so-bw-rail-list {
    min-width: 0;
    align-items: baseline;
    gap: 2px 18px;
}
/* Section 14's phone rule stacked the strip into a column. Restated as a row
   at (1,3,0) so it survives at every width now that there is one row in it. */
#so-booking-widget:where([data-cw~="560"])[data-template="dusk"] .so-bw-rail,
#so-booking-widget:where([data-cw~="560"])[data-template="dusk"] .so-bw-rail-list {
    flex-direction: row;
    flex-wrap: wrap;
    align-items: baseline;
}
#so-booking-widget[data-template="dusk"] .so-bw-rail-row {
    min-width: 0;
    gap: 6px;
}
#so-booking-widget[data-template="dusk"] .so-bw-rail-v {
    text-align: left;
    /* The bundle line and the four value rows all live on one strip now, so a
       long service name has to be able to give way rather than push the total
       off the end. */
    overflow-wrap: anywhere;
}
/* 22px is a column heading's size and this is a strip. The figure stays the
   loudest thing on the bar — it is the only one in champagne — without being
   taller than the buttons beside it. */
#so-booking-widget[data-template="dusk"] .so-bw-rail-total {
    gap: 8px;
    width: auto;
}
#so-booking-widget[data-template="dusk"] .so-bw-rail-total span:last-child {
    font-size: clamp(15px, calc(var(--so-bw-cw, 820px) * 0.022), 19px);
    font-weight: 700;
}

/* ── 26.4 · What the strip says, and when ────────────────────────────────
   THE BUNDLE, ALWAYS: "3 services · 2 h 15 m", from the same two getters the
   in-list running total used. It replaces the SERVICE row (the next sibling —
   selected by adjacency so the running order in the markup stays the source of
   truth) because a strip cannot carry four wrapped service names, and the
   count plus the duration is what the customer is actually tracking.

   EVERYTHING ELSE, ONLY ONCE ANSWERED. `--unset` is bound in the markup, so
   Specialist / Date / Time appear as they are chosen and the bar grows into
   the journey instead of starting full of dashes. */
#so-booking-widget[data-template="dusk"] .so-bw-rail-row--tally { display: flex; }
#so-booking-widget[data-template="dusk"] .so-bw-rail-row--unset { display: none; }
#so-booking-widget[data-template="dusk"] .so-bw-rail-row--tally + .so-bw-rail-row { display: none; }

/* The in-list copy of the same numbers. It sat at the END of the service list,
   which is both where it is least likely to be read and 49px off the height of
   the list it was reporting on — and with the tally on the docked bar it would
   now be the second place on screen quoting the same price. Dusk only: the
   other eighteen have no docked strip to move it to. */
#so-booking-widget[data-template="dusk"] .so-bw-multi-summary { display: none; }

/* ── 26.4b · THE TWO STATUS CARDS ────────────────────────────────────────
   With 26.2 placing them, the membership banner and the hold timer sit where
   the DOM always meant them to: directly under the header, above the list.
   That is the header band as far as the customer is concerned — standing
   conditions on the booking — and it leaves the footer holding only the
   summary, the total and the two buttons.

   They arrive drawn for different templates and it shows: the membership
   banner is a violet-to-indigo gradient with a 12px radius and a coloured
   drop shadow, and the hold timer is a full-bleed amber strip with a bottom
   border and no radius at all. Side by side on ink they read as two
   components from two products.

   ONE CARD SHAPE, STATED ONCE, AND BOTH TAKE IT. `--so-bw-dusk-status-*` is
   local to this block: radius, inset, min-height and the gap below are each
   written once and referenced by both rules, so "consistent height, spacing
   and alignment" is a fact about the code rather than two sets of numbers
   that happen to match today.

   Neither card is repainted arbitrarily — the membership card takes the
   champagne accent the whole template runs on, and the hold timer keeps the
   warning/danger tokens, because that escalation is semantic (see
   `.so-bw-hold-timer--urgent`) and must survive the restyle. What changes is
   the SURFACE both are drawn on: ink with a hairline, like everything else
   here. */
/* THREE TOKENS DUSK NEVER RE-DECLARED, WHICH IS WHY THE STATUS SURFACES KEEP
   COMING OUT WHITE. Section 15 inverts the neutral ramp and the text tokens
   for this template but stops there, so measured on the Dusk container:

       --so-bw-primary-light   #eff6ff   near-white blue
       --so-bw-primary-hover   #2563eb   blue
       --so-bw-warning-light   #fef3c7   pale yellow

   — the stock light-theme values, untouched, on a #14161d canvas. Every
   consumer of them renders as a white box with blue text: the "Welcome back —
   Pro Wellness (15% off applied)" chip under the email field, the "Already a
   member?" unlock row, the email-mismatch warning, and the 4px ring on the
   active step dot. Several of those are inline-styled with no class to target,
   so they cannot be reached one at a time — and they should not have to be.
   The tokens are the defect.

   Derived from the template's own accent and canvas rather than picked, so
   they follow the Studio's colour control the way every other Dusk token
   does. `-hover` lifts the champagne rather than tinting the surface, because
   its consumers use it as INK. */
#so-booking-widget[data-template-id="dusk-champagne"] .so-bw-container {
    --so-bw-primary-light: color-mix(in srgb, var(--so-bw-primary) 14%, #14161d);
    --so-bw-primary-hover: color-mix(in srgb, var(--so-bw-primary) 78%, #ffffff);
    --so-bw-warning-light: color-mix(in srgb, var(--so-bw-warning) 14%, #14161d);
    --so-bw-danger-light: color-mix(in srgb, var(--so-bw-error) 14%, #14161d);
}

#so-booking-widget[data-template="dusk"] .so-bw-container {
    --so-bw-dusk-status-radius: var(--so-bw-ds-radius-chip, 8px);
    --so-bw-dusk-status-min-h: 40px;
    --so-bw-dusk-status-pad-y: 8px;
    --so-bw-dusk-status-gap: 10px;
}

/* The membership card. Every declaration carries !important because the
   banner's styling is an inline `style` attribute, which no selector outranks
   without it — see the note beside the markup in booking-widget.js. */
#so-booking-widget[data-template="dusk"] .so-bw-member-banner {
    gap: 10px !important;
    flex-wrap: nowrap !important;
    min-height: var(--so-bw-dusk-status-min-h) !important;
    padding: var(--so-bw-dusk-status-pad-y) 12px !important;
    /* Top margin, not bottom: the space UNDER the last card is already the
       step's own padding-top, so a bottom margin would double it. Spacing
       between two stacked cards is then one value, not the sum of two. */
    margin: var(--so-bw-dusk-status-gap) var(--so-bw-gutter) 0 !important;
    border-radius: var(--so-bw-dusk-status-radius) !important;
    border: 1px solid color-mix(in srgb, var(--so-bw-primary) 34%, transparent) !important;
    background: color-mix(in srgb, var(--so-bw-primary) 10%, var(--so-bw-ds-n25)) !important;
    color: var(--so-bw-ds-n900) !important;
    box-shadow: none !important;
}
/* The 36px filled disc is a badge on a violet banner and a lit dot on ink.
   A 22px champagne outline mark keeps the tick and stops it competing with
   the discount figure, which is the number the card is for. */
#so-booking-widget[data-template="dusk"] .so-bw-member-banner__mark {
    width: 22px !important;
    height: 22px !important;
    background: none !important;
    border: 1px solid var(--so-bw-primary) !important;
    color: var(--so-bw-primary-ink, var(--so-bw-primary)) !important;
}
#so-booking-widget[data-template="dusk"] .so-bw-member-banner__mark svg {
    width: 13px;
    height: 13px;
}
/* Two stacked lines (an 11px eyebrow over a 14px plan name) is a banner's
   shape and it is what made this 66px tall. One line: eyebrow, plan, tier.
   `min-width: 0` replaces the inline `min-width: min(200px, 100%)`, which on
   a 292px floating panel is what forced the wrap this row now refuses. */
#so-booking-widget[data-template="dusk"] .so-bw-member-banner__body {
    display: flex !important;
    align-items: baseline !important;
    flex-wrap: wrap !important;
    gap: 2px 8px !important;
    min-width: 0 !important;
}
#so-booking-widget[data-template="dusk"] .so-bw-member-banner__label {
    font-size: 10px !important;
    letter-spacing: 0.07em !important;
    color: var(--so-bw-ds-n500) !important;
    opacity: 1 !important;
}
#so-booking-widget[data-template="dusk"] .so-bw-member-banner__plan {
    margin-top: 0 !important;
    font-size: 13px !important;
    min-width: 0 !important;
    overflow-wrap: anywhere;
}
#so-booking-widget[data-template="dusk"] .so-bw-member-banner__tier {
    padding: 1px 7px !important;
    font-size: 9.5px !important;
    background: none !important;
    border-color: var(--so-bw-ds-n300) !important;
    color: var(--so-bw-ds-n500) !important;
}
/* "15%" over "off your booking" is 32px of stacked type for six characters of
   information. One champagne figure, the words beside it, on the row. */
#so-booking-widget[data-template="dusk"] .so-bw-member-banner__off {
    display: flex !important;
    align-items: baseline !important;
    gap: 5px !important;
    flex-shrink: 0 !important;
    margin-left: auto !important;
    padding-left: 10px !important;
    border-left-color: var(--so-bw-ds-n200) !important;
}
#so-booking-widget[data-template="dusk"] .so-bw-member-banner__pct {
    font-size: 15px !important;
    font-weight: 700 !important;
    font-family: var(--so-bw-mono);
    color: var(--so-bw-primary-ink, var(--so-bw-primary)) !important;
}
#so-booking-widget[data-template="dusk"] .so-bw-member-banner__offlabel {
    margin-top: 0 !important;
    font-size: 9.5px !important;
    color: var(--so-bw-ds-n500) !important;
    opacity: 1 !important;
}
/* MEASURED at a 338px container (390px phone, Medium, Inline): the row needs
   about 321px of content in 266px of usable width, so it wrapped — and because
   `__off` does not shrink, everything gave way in `__body` instead. Three
   words of eyebrow went to three lines, the plan name to two, and the card
   came out 120px tall beside a 40px timer. The two cards stopped agreeing at
   exactly the width where agreeing matters most.

   TWO LABELS CARRY THE COST, BECAUSE THEY ARE THE TWO THINGS THE ROW ALREADY
   SAYS. The champagne tick is what "member benefits active" means, and a
   discount sitting in a membership card is off the booking. Without them the
   row needs ~212px and fits on one line at 40px — the timer's height, which is
   the whole point of the pair. The plan name and the tier, which are the
   information, never truncate.

   560 rather than 480, because the Popup shell is the case that decides it:
   its dialog is capped at 560px and measured a 532px container, where the full
   row needs ~459px of a 460px line and wrapped on the last pixel. A breakpoint
   that lands a card in its two-line state only inside a popup is a breakpoint
   set from the phone rather than from the widths that exist. */
    #so-booking-widget:where([data-cw~="560"])[data-template="dusk"] .so-bw-member-banner__label,
    #so-booking-widget:where([data-cw~="560"])[data-template="dusk"] .so-bw-member-banner__offlabel {
        display: none !important;
    }
    /* Kept as the backstop for a plan name long enough to wrap anyway: it
       breaks INSIDE the same card rather than the card growing a second
       shape — border, radius and inset are unchanged, so the pair still
       reads as a pair even in the case the rule above does not fit. */
    #so-booking-widget:where([data-cw~="560"])[data-template="dusk"] .so-bw-member-banner {
        flex-wrap: wrap !important;
    }
    #so-booking-widget:where([data-cw~="560"])[data-template="dusk"] .so-bw-member-banner__off {
        margin-left: auto !important;
        padding-left: 10px !important;
    }

/* The hold timer, to the same card. It keeps its own colour — amber while
   there is time, the danger token under a minute — because that escalation is
   the component's whole job; only the surface it is drawn on changes. */
#so-booking-widget[data-template="dusk"] .so-bw-hold-timer {
    min-height: var(--so-bw-dusk-status-min-h);
    padding: var(--so-bw-dusk-status-pad-y) 12px;
    margin: var(--so-bw-dusk-status-gap) var(--so-bw-gutter) 0;
    border: 1px solid color-mix(in srgb, currentColor 34%, transparent);
    border-radius: var(--so-bw-dusk-status-radius);
    background: color-mix(in srgb, currentColor 10%, var(--so-bw-ds-n25));
    font-size: 12.5px;
    /* A long "Slot held for" translation plus a countdown plus "Keep this
       time" is more than a 292px floating panel gives. Wrapping inside the
       card is the only outcome that neither clips the countdown nor pushes a
       scrollbar onto the widget. */
    flex-wrap: wrap;
    gap: 6px 8px;
}
#so-booking-widget[data-template="dusk"] .so-bw-hold-extend {
    padding: 3px 9px;
    font-size: 11px;
    border-radius: var(--so-bw-dusk-status-radius);
}
/* The other two banners that share these rows are not restyled — they carry
   their own semantics and their own colours — but they do have to clear the
   header rule and sit on the same left edge as the cards above and the list
   below, which the full-bleed layout they ship with does not. */
#so-booking-widget[data-template="dusk"] .so-bw-error,
#so-booking-widget[data-template="dusk"] .so-bw-pay-retry {
    margin: var(--so-bw-dusk-status-gap) var(--so-bw-gutter) 0;
    border-radius: var(--so-bw-dusk-status-radius);
}

/* ── 26.4c · A taller card, because Dusk is a list ───────────────────────
   The shared bound is `min(760px, 100dvh - 112px)`, set once for all
   nineteen. 760px is the right ceiling for a template whose steps are a
   calendar or a six-field form — those have a natural end, and height past it
   is empty card. Dusk's choice step is a scrolling INDEX with no natural end,
   so every pixel of ceiling is another fraction of the catalogue on screen.

   840, not "as tall as it will go". The point of a bounded card is that the
   widget is a pane on someone's page rather than the page itself; 1000px of
   ink would be the page. 80px is about one more hairline row, which is the
   size of the change being asked for.

   THE SECOND TERM IS UNCHANGED AND IS WHAT KEEPS THIS SAFE. `100dvh - 112px`
   still wins on any window shorter than ~950px, so a laptop renders exactly
   what it rendered before and only a tall window sees the extra height. dvh
   rather than vh so a phone measures the collapsed-URL-bar viewport.

   (1,3,0) plus !important: this section sits above the BOUNDED SHELL block
   that sets the shared cap, so it needs to outrank it on specificity rather
   than on source order, and that rule carries !important of its own. */
#so-booking-widget[data-template="dusk"][data-template-id] .so-bw-container {
    max-height: min(840px, calc(100dvh - 112px)) !important;
}

/* ── 26.5 · The commit bar ───────────────────────────────────────────────
   Same buttons, same order, less air. 16px of padding above and below a 47px
   button is a 79px bar; the fluid value lands at 10-12px on the widths that
   have a list to show and never goes below 8px. */
#so-booking-widget[data-template="dusk"] .so-bw-nav {
    padding: clamp(8px, calc(var(--so-bw-cw, 820px) * 0.013), 12px) var(--so-bw-gutter);
    gap: 10px;
}

/* ── 26.6 · The card gets its "Read more" row back ───────────────────────
   The body was a flex COLUMN, so the link had a line to itself: 32px on every
   card, times a whole catalogue. As a wrapping ROW, the title, the description
   and the duration bar each take a full line (`flex: 0 0 100%`) and the badges
   and the link share the last one.

   `order` rather than grid areas, deliberately. The bar between the
   description and the badges is conditional (`usesDurationBars`), and with
   named areas an item nobody placed auto-flows into whichever cell is free —
   which is a layout that changes shape depending on a setting. Ordering only
   says "these two go last, badges first", which stays true whether the bar is
   there or not. */
#so-booking-widget[data-template="dusk"] .so-bw-card--service .so-bw-card-body {
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
    column-gap: 12px;
    row-gap: 2px;
}
#so-booking-widget[data-template="dusk"] .so-bw-card--service .so-bw-card-title,
#so-booking-widget[data-template="dusk"] .so-bw-card--service .so-bw-card-meta,
#so-booking-widget[data-template="dusk"] .so-bw-card--service .so-bw-card-bar {
    flex: 0 0 100%;
}
#so-booking-widget[data-template="dusk"] .so-bw-card--service .so-bw-card-badges {
    order: 1;
    flex: 1 1 auto;
    min-width: 0;
    /* The base rule pushes the badges down with `margin-top: auto` to level
       them across a GRID of cards. Dusk's list is one column — there is
       nothing beside a card to level against — and the auto margin on a
       wrapping row just reintroduces the gap this rule is removing. */
    margin-top: 0;
    padding-top: 2px;
}
#so-booking-widget[data-template="dusk"] .so-bw-card--service .so-bw-card-more {
    order: 2;
    align-self: center;
    flex: 0 0 auto;
    margin: 0;
    padding: 0;
}
/* Tighter than the shared `--so-bw-card-pad - 4 / - 2`. A hairline row is the
   one card shape with no border and no fill to hold, so its padding is doing
   less work and can be less of it. */
#so-booking-widget[data-template="dusk"] .so-bw-card--service .so-bw-card-top {
    padding-top: clamp(8px, calc(var(--so-bw-card-pad) - 6px), 12px);
    padding-bottom: clamp(8px, calc(var(--so-bw-card-pad) - 6px), 12px);
}
/* MEASURED: `Extraordinarilylongunbrokenservicenamewithnospacesatall` reported
   scrollWidth > clientWidth on the card title at 360px and 390px — a service
   name clipped mid-word with no scrollbar to reach the rest. A name with no
   spaces in it has to be allowed to break somewhere. */
#so-booking-widget[data-template="dusk"] .so-bw-card-title {
    overflow-wrap: anywhere;
}
/* The 2-line clamp above it stays — a name that does not fit is still
   truncated with an ellipsis and still readable in full from the title
   attribute and the Review step. What goes is the RESERVED second line
   (`min-height: calc(2 * 1.3em)`), which exists to level a title against the
   card BESIDE it in a multi-column grid. Dusk's list is one column and already
   sets `grid-auto-rows: auto` for exactly this reason, so there is nothing to
   level against and the reservation is ~20px of blank line under every
   single-line service name, on every row, in a list whose problem is height.
   (1,3,0) against the (1,2,0)/(1,3,0) pair that sets it; later in source. */
#so-booking-widget[data-template="dusk"] .so-bw-card--service .so-bw-card-title {
    min-height: 0;
}
/* The description box reserves two lines for the same levelling reason and
   costs the same blank line — measured under "A cut." and every other short
   description in the catalogue. The 2-line CLAMP stays; only the floor goes,
   so a long description is still previewed at two lines with Read more beside
   the price, and a short one stops paying for a line it does not use. */
#so-booking-widget[data-template="dusk"] .so-bw-card-meta--clamp {
    min-height: 0;
}

/* ── 26.7 · The step's own furniture ─────────────────────────────────────
   24px above the heading, 20px under it and 12px under the search field is a
   third of the scroller spent before the first service. Trimmed, not removed —
   the list still starts clear of the header rule. */
#so-booking-widget[data-template="dusk"] .so-bw-step {
    padding-top: clamp(10px, calc(var(--so-bw-cw, 820px) * 0.02), 16px);
}
#so-booking-widget[data-template="dusk"] .so-bw-step-desc {
    margin-bottom: clamp(8px, calc(var(--so-bw-cw, 820px) * 0.015), 12px);
}
#so-booking-widget[data-template="dusk"] .so-bw-search {
    margin-bottom: 8px;
}
/* The search field is sticky at the top of the scroller (see the block above
   this section), and the rule that pins it paints it `--so-bw-card-bg` so the
   list cannot read through it. On Dusk that token is #1a1d26 while the body it
   sits on is #14161d — close enough to look like a rendering fault and far
   enough to see, as a lighter band across the top of the list that only
   appears once you scroll. Matches the body instead. (1,2,0) beats the (1,1,0)
   / (1,2,0)-with-[data-template] pair that sets it.

   (1,3,0) via `[data-template-id]` — the same attribute pair section 23.2 uses
   for the size scale, and load-bearing for the same reason. This section sits
   BEFORE the bounded-shell block that pins the field (see the banner), so a
   (1,2,0) rule here would lose the tie on source order rather than win it. */
#so-booking-widget[data-template="dusk"][data-template-id] .so-bw-search {
    background: #14161d;
}

/* ── 26.8 · LOW LIGHT, INCLUDING WHILE IT LOADS ──────────────────────────
   The loading overlay is `rgba(255, 255, 255, 0.85)` — a near-white sheet over
   a #14161d canvas, which on this template is not a veil but a flash. The
   dark-theme variant beside it is gated on `[data-theme="dark"]`, and Dusk is
   a dark-FIRST template rendered at `data-theme="light"` with an inverted
   ramp, so it never matched.

   The overlay is `position: absolute; inset: 0` inside the container, so this
   covers the header, the list, the docked strip and Continue in one sheet —
   the whole widget stays in register while it loads rather than the middle of
   it turning white. n0 IS the canvas, so the veil reads as the canvas dimmed. */
#so-booking-widget[data-template="dusk"] .so-bw-loading {
    background: color-mix(in srgb, var(--so-bw-ds-n0) 88%, transparent);
    /* n700, not the n500 the muted copy elsewhere takes: this label sits on a
       canvas that has itself been dimmed to 88%, so the rung that reads as
       "quiet" against the undimmed surface reads as "nearly gone" here. n700
       is still muted against champagne and still clears 4.5:1 on the veil. */
    color: var(--so-bw-ds-n700);
}
#so-booking-widget[data-template="dusk"] .so-bw-loading-inline {
    color: var(--so-bw-ds-n700);
}
/* The track was --so-bw-border (#2a2f3d) against a champagne head, which is
   the right idea and about a point of contrast short of showing the ring at
   all on ink. n200 is the same ramp, one rung up. */
#so-booking-widget[data-template="dusk"] .so-bw-spinner {
    border-color: var(--so-bw-ds-n200);
    border-top-color: var(--so-bw-primary);
}

/* ── 26.9 · …and the shell it loads inside ───────────────────────────────
   `BookingShortcode::positionStyles()` paints both non-inline shells white —
   `.so-bw-popup__dialog { background: #fff }` and `.so-bw-floating__panel
   { background: #fff; padding: 16px }`. That is the correct default for
   eighteen templates and it is a white card behind (and, on Floating, a 16px
   white border around) a widget whose whole proposition is the dark. It is
   also the first thing painted, before the widget has booted — the white flash
   at the start of every popup and every floating open.

   :has() rather than a class, because the shell is an ANCESTOR of the element
   that knows which template this is: nothing inside the widget can reach it,
   and the shell markup is emitted by PHP that is deliberately
   template-agnostic. Scoped to the Dusk template id, so the other eighteen
   shells are byte-for-byte what they were. A browser without :has() simply
   keeps today's white shell — the widget itself still renders correctly. */
.so-bw-popup__dialog:has(> #so-booking-widget[data-template-id="dusk-champagne"]),
.so-bw-floating__panel:has(> #so-booking-widget[data-template-id="dusk-champagne"]) {
    background: #14161d;
}
/* The close button is drawn for a light shell: a near-black glyph on a 5%
   black chip, which on ink is invisible furniture over an invisible button. */
.so-bw-popup__dialog:has(> #so-booking-widget[data-template-id="dusk-champagne"]) .so-bw-popup__close,
.so-bw-floating__panel:has(> #so-booking-widget[data-template-id="dusk-champagne"]) .so-bw-floating__close {
    background: rgba(255, 255, 255, 0.10);
    color: #e9e6df;
}
/* The panel's own 16px inset is a white frame in the default shell and a dead
   dark one here — the widget already carries the gutter. Dropped so the card
   reaches the panel edge, which is what makes the two read as one surface.

   `overflow` is deliberately NOT set to `hidden` to round the corners with:
   the panel is capped at 80vh and the widget card at `min(760px, 100dvh -
   112px)`, and on a phone the card is the taller of the two — clipping would
   put the bottom of the widget, Continue included, permanently out of reach.
   The card draws its own radius, so the corners are rounded anyway. */
.so-bw-floating__panel:has(> #so-booking-widget[data-template-id="dusk-champagne"]) {
    padding: 0;
}

/* MEASURED, and the reason this block is not just about colour. The widget
   card is capped at `min(760px, 100dvh - 112px)` — 760px on a 900px-tall
   window — while the shells that hold it are capped at 90vh (popup) and 80vh
   (floating), which is 720px on the same window. The card was 40px taller than
   the box around it, so the shell grew its own scrollbar and the last 40px of
   the widget sat below its fold.

   Continue survived that because `.so-bw-nav` is `position: sticky; bottom: 0`
   and pinned itself to the shell's viewport. The docked strip is not sticky —
   so on Floating the SELECTED / TOTAL bar this section just built was scrolled
   out of sight the moment it existed, which is how it was caught.

   Capping the card at the shell's own height is the fix, and it belongs here
   rather than on the shared rule because these two selectors are the only
   place in the stylesheet that knows what that height IS. (1,3,0) — :has()
   scores its most specific argument, the id — over the (1,1,0)/(1,2,0) pair
   that sets the shared cap with !important, so `!important` here is not
   competing with that so much as required to answer it. */
.so-bw-popup__dialog:has(> #so-booking-widget[data-template-id="dusk-champagne"]) .so-bw-container {
    max-height: 90vh !important;
}
.so-bw-floating__panel:has(> #so-booking-widget[data-template-id="dusk-champagne"]) .so-bw-container {
    max-height: 80vh !important;
}

/* ── 26.10 · Counter Sand in the same two shells ─────────────────────────
   The same problem as 26.9 and the same three answers, for the same reason:
   the shells are painted white by PHP that cannot know which template it is
   wrapping, and Counter Sand's canvas is warm sand with a dark band across
   the top. Left alone, the popup shows a white margin around a sand card and
   the floating panel adds a 16px white frame — the one arrangement that makes
   a deliberately warm surface read as a mistake.

   The colour here is the LITERAL sand rather than the `--so-bw-counter-sand`
   token the architecture derives, and that is not an oversight: the token is
   declared on `.so-bw-container`, which is a DESCENDANT of the shell. Custom
   properties inherit downward, so the shell cannot read a value its own child
   defines. A tint mixed from the accent is a nicety inside the card; matching
   the stock accent's mix here is what stops the flash before boot, which is
   the thing this block exists for.

   Scoped to the template id, so the other twenty-one shells are unchanged. */
.so-bw-popup__dialog:has(> #so-booking-widget[data-template-id="counter-sand"]),
.so-bw-floating__panel:has(> #so-booking-widget[data-template-id="counter-sand"]) {
    background: #fbf7ef;
}
/* No white frame between the panel and a card that already carries the
   gutter. `overflow` stays unset for the reason 26.9 gives — the card can be
   taller than the panel on a phone, and clipping would put Continue out of
   reach. The card draws its own radius. */
.so-bw-floating__panel:has(> #so-booking-widget[data-template-id="counter-sand"]) {
    padding: 0;
}
/* The shells' close control is drawn for a LIGHT shell — a near-black glyph on
   a 5% black chip — and it is positioned at the top right, which on this
   template is squarely on the ink band. Measured on the floating panel: an
   invisible glyph on an invisible chip, i.e. the only way out of the widget
   rendered as nothing at all. Same fix and same reason as Dusk's in 26.9;
   Dusk's own rule cannot cover this because it is scoped to its template id. */
.so-bw-popup__dialog:has(> #so-booking-widget[data-template-id="counter-sand"]) .so-bw-popup__close,
.so-bw-floating__panel:has(> #so-booking-widget[data-template-id="counter-sand"]) .so-bw-floating__close {
    background: rgba(255, 255, 255, 0.16);
    color: #fff;
}
/* Same measured mismatch as 26.9: the card is capped at min(760px, …) and the
   shells at 90vh / 80vh, so on a tall window the card is the taller of the
   two and the shell grows a second scrollbar. These selectors are the only
   place that knows the shell's height. (1,3,0) via :has(), and !important
   because the shared cap it answers carries one. */
.so-bw-popup__dialog:has(> #so-booking-widget[data-template-id="counter-sand"]) .so-bw-container {
    max-height: 90vh !important;
}
.so-bw-floating__panel:has(> #so-booking-widget[data-template-id="counter-sand"]) .so-bw-container {
    max-height: 80vh !important;
}


/* ==========================================================================
   27 · EDITORIAL — THE INDEX THAT SCROLLS
   ==========================================================================

   Nothing here repaints the template. Same Newsreader masthead, same ivory
   page, same gold, same hairline index. What it fixes is that Editorial was
   the ONE architecture the bounded shell never reached.

   THE ROOT CAUSE, AND IT IS ONE LINE. Section 14 opens Editorial with
   `display: block` on `.so-bw-container` at (1,2,0) — written when the widget
   was a column that grew down the page, and never revisited. The BOUNDED
   SHELL block at the end of this file declares the column at (1,1,0), so
   Editorial lost that tie on specificity, and every consequence of the shell
   followed it out:

     - `.so-bw-body` kept `overflow-y: auto` but sized to its content, so it
       never became a scroll region. MEASURED at 1440x900 with 13 services:
       container 760px tall, body 2908px tall inside it.
     - `.so-bw-nav` is `position: sticky; bottom: 0`. With no ancestor scroll
       container it resolved against the PAGE and pinned itself to the bottom
       of the VIEWPORT — measured at y=802 with the body starting at y=835,
       i.e. floating ON TOP of the list, cutting the second service card in
       half. That is the "cards hidden behind Continue" symptom exactly.
     - The step's furniture (header 40/18, body 26, step 24, description 14)
       stacked to 82px of pure padding between the masthead and the first word
       of the description, on a card that had no height to spare.

   Restoring the column (27.1) fixes all three at once, because the shell
   block was always correct — it simply could not bind here.

   THE SECOND FAULT IS THE LINE LENGTH, AND IT IS NOT A LAYOUT FAULT. At
   Widget Size = Full the card spans its column — which is exactly what that
   setting asks for — and because the template's inset was a literal 8px, so
   did every line in it: the service description measured 1240px wide on a
   1440px screen, roughly 190 characters a line.

   THE FIRST FIX FOR THAT WAS WRONG AND IS WORTH RECORDING, because it is the
   obvious one. It bounded the CONTENT COLUMN — a centred 720px measure inside
   whatever width the card had — which fixed the line length by narrowing the
   entire booking interface, leaving ~360px of empty ivory down each side of a
   layout the admin had explicitly set to 100%. A template may not answer a
   width setting with a narrower width. 27.2 is now plain padding that spans
   at every setting, and the only bound left is on the DESCRIPTION itself
   (27.6), which is the one run of long prose on the page and the only thing
   the 190-character measure was ever about.

   AND IT SETTLES THE GUTTER EXCEPTION. The note in section 14 says Editorial
   routes its inset through `.so-bw-body` while `.so-bw-step` adds its own on
   top, so the masthead sat at 8px and the cards at 32px — "a per-architecture
   change with a visible effect on the content column, so it belongs with the
   architecture work rather than this cascade pass". This is that work: the
   inset is owned by header / body / nav, the step's is zeroed, and the
   heading, the search field, the index and Continue all sit on one left edge.
   `tests/unit/widget-css-cascade.test.ts` exempts Editorial from the
   literal-x-inset rule; nothing here reintroduces a literal.

   WHERE IT SITS. Above the staff-profile block that closes this file, which
   must stay last (staff-profile-popup.test.ts locates it by its banner text,
   so that exact phrase is deliberately not repeated here — writing it earlier
   moves the slice and points the test's hardcoded-colour scan at whatever
   section did). That puts this one BEFORE the BOUNDED SHELL block,
   so anything answering a rule there wins on specificity rather than source
   order — `[data-template="editorial"][data-template-id]` at (1,3,0), the
   same device section 26 uses for Dusk.
   ========================================================================== */

/* ── 27.1 · The shell is a column again ──────────────────────────────────
   (1,2,0), matching section 14's `display: block` and later in source. One
   declaration, and the shell block downstream does the rest: header pinned,
   body the single scroll region, nav a flex item at the foot of the card
   instead of a bar floating over the page. */
#so-booking-widget[data-template="editorial"] .so-bw-container {
    display: flex;
    flex-direction: column;
}

/* ── 27.2 · One inset, and everything sits on it ─────────────────────────
   THE WIDTH SETTING IS THE WIDTH. The first cut of this section resolved
   `--so-bw-ed-inset` as `max(gutter, (100% - 720px) / 2)` — a centred 720px
   column of type inside whatever width the card had. That reads well on a
   680px card and is simply WRONG at Widget Size = Full: it took a deliberate
   100%-width setting and rendered a narrow strip with ~360px of empty ivory
   down each side, which is the setting being overridden rather than honoured.
   The size control is the admin's answer to how wide this should be; nothing
   here may quietly re-decide it.

   So the inset is PADDING and never a cap. The header, the list, the search
   field, the running-total bar and the commit bar all span the container at
   every setting — Small, Medium, Large and Full alike — and the only thing
   still bounded for readability is the service DESCRIPTION, which is the one
   run of long text on the page (27.6).

   Fluid rather than a flat 24px because the inset is the card's margin, not
   its content: 24px of air beside a 1400px band reads as a rendering fault,
   40px reads as a page. It is bounded at both ends and cannot become a
   measure — at its widest it takes 80px out of the container, which is 5%
   of a 1440px card. The floor is `--so-bw-gutter`, so the phone tiers
   (14/16/20px) still govern there and there is no breakpoint to maintain.

   The vertical halves stay fluid on the container width for the reason
   section 26.5 gives: the bar tightens with the box rather than at a
   breakpoint. */
#so-booking-widget[data-template="editorial"] .so-bw-container {
    --so-bw-ed-inset: clamp(
        var(--so-bw-gutter),
        calc(var(--so-bw-cw, 720px) * 0.028),
        40px
    );
}
/* The masthead. 40px above the headline was set for a template that opened at
   the top of a page; inside a bounded card it is a tenth of the height before
   a word is read. The rule under it stays full-bleed — it is the masthead
   rule, and a hairline that stops short of the card edge reads as a mistake. */
#so-booking-widget[data-template="editorial"] .so-bw-header {
    padding:
        clamp(18px, calc(var(--so-bw-cw, 720px) * 0.032), 32px)
        var(--so-bw-ed-inset)
        clamp(10px, calc(var(--so-bw-cw, 720px) * 0.018), 15px);
}
/* The scroll region. Horizontal padding only — the vertical is zero because
   `.so-bw-step` immediately inside it carries that (27.3), and two boxes each
   contributing padding is how the 82px accumulated. */
#so-booking-widget[data-template="editorial"] .so-bw-body {
    padding: 0 var(--so-bw-ed-inset);
}
/* The commit bar, on the same left edge and at bar height. 18/32 around a
   47px button is a 98px footer — measured, and the single largest fixed cost
   in the card after the masthead. The fluid value lands at 11-14px on the
   widths that have a list to show and never goes below 9px, which keeps
   Continue a comfortable target while giving ~40px back to the index. */
#so-booking-widget[data-template="editorial"] .so-bw-nav {
    padding: clamp(9px, calc(var(--so-bw-cw, 720px) * 0.015), 14px) var(--so-bw-ed-inset);
}
/* The step's horizontal inset is now the body's, so it is zeroed here rather
   than added to it — the decision the gutter-exception note deferred. Only
   the x-axis: the vertical values are 27.3's. */
#so-booking-widget[data-template="editorial"] .so-bw-step {
    padding-left: 0;
    padding-right: 0;
}

/* ── 27.3 · The space between the masthead and the list ──────────────────
   Trimmed, not removed. The description still clears the masthead rule and
   the search field still clears the description; what goes is the third
   helping of the same gap. Measured heading-bottom to first card: 190px
   before, ~110px after, all of it returned to the scroller. */
#so-booking-widget[data-template="editorial"] .so-bw-step {
    padding-top: clamp(10px, calc(var(--so-bw-cw, 720px) * 0.02), 16px);
}
#so-booking-widget[data-template="editorial"] .so-bw-step-desc {
    /* Zero, not trimmed: it is the first thing in the step and the step's own
       padding-top is already the space above it. */
    margin-top: 0;
    margin-bottom: clamp(8px, calc(var(--so-bw-cw, 720px) * 0.014), 12px);
}
/* Padding, not margin, and that is load-bearing rather than a preference: the
   field is `position: sticky` inside the scroller (27.4), and only its own
   box is painted. A 10px MARGIN under it is transparent, so a card scrolling
   past showed through the band between the field and the first row. */
#so-booking-widget[data-template="editorial"] .so-bw-search {
    margin-bottom: 0;
    padding-bottom: 10px;
}
/* The last hairline row must not sit hard against the commit bar's top rule —
   two lines 1px apart read as one thick one. (1,3,0) because the BOUNDED
   SHELL block sets `padding-bottom: 16px !important` and is later in source. */
#so-booking-widget[data-template="editorial"][data-template-id] .so-bw-step {
    padding-bottom: clamp(16px, calc(var(--so-bw-cw, 720px) * 0.03), 28px) !important;
}

/* ── 27.4 · The scrollbar, in the template's own ink ─────────────────────
   The shell block gives the body `overflow-y: auto`, `overscroll-behavior:
   contain` and `-webkit-overflow-scrolling: touch`, so touch scrolling and
   the momentum stop are already right on phones and tablets and there is
   nothing to add for them. What is worth stating is the BAR: the shared
   tokens mix it from `--so-bw-text`, which on ivory is a neutral grey stripe.
   Mixed from the accent instead it is a bronze hairline — present enough to
   say "there is more below", quiet enough for the page. The phone block
   further up hides the bar outright, which is correct: a touch surface should
   not paint one.

   Set as the three shared tokens rather than as scrollbar properties, so the
   `scrollbar-color` rule, the WebKit pseudo-elements and the hover state all
   follow from one place. (1,2,0) over the (1,1,0) declaration. */
#so-booking-widget[data-template="editorial"] .so-bw-container {
    --so-bw-sb-track: transparent;
    --so-bw-sb-thumb: color-mix(in srgb, var(--so-bw-primary) 32%, transparent);
    --so-bw-sb-thumb-hover: color-mix(in srgb, var(--so-bw-primary) 58%, transparent);
}
/* The search field is pinned to the top of the scroller by the shell block,
   which paints it `--so-bw-card-bg` so cards cannot read through it. On
   Editorial that token is white while the page under it is the ivory canvas —
   a band that only appears once you scroll, which is worse than none. Same fault
   Dusk had (26.7) and the same (1,3,0) answer, for the same reason: this
   section is before the block that pins it. */
#so-booking-widget[data-template="editorial"][data-template-id] .so-bw-search {
    background: var(--so-bw-bg);
}
/* And the field itself set the way Editorial sets every other field — a rule
   under the line, no box, no corner (see the `.so-bw-input` block in F6).
   A 1240px rounded control was the one piece of app chrome left on the page. */
#so-booking-widget[data-template="editorial"] .so-bw-search-input {
    border-width: 0 0 1px 0;
    border-radius: 0;
    background: transparent;
    padding-left: 26px;
}
#so-booking-widget[data-template="editorial"] .so-bw-search-icon {
    left: 0;
}
#so-booking-widget[data-template="editorial"] .so-bw-search-input:focus {
    border-bottom-color: var(--so-bw-primary);
    box-shadow: 0 1px 0 var(--so-bw-primary);
}

/* ── 27.5 · The index reads as a list, not as a levelled grid ────────────
   `grid-auto-rows: 1fr` levels every row to the tallest card. That is right
   for a multi-column grid, where a short card beside a tall one is a ragged
   row — and it is nothing but cost in ONE column, where every card was
   stretched to the height of the most verbose service in the catalogue.
   MEASURED: all 13 cards at 196px against a 108px shortest.

   Three reservations go with it, all of which exist to level a card against
   the card BESIDE it and have nothing beside them here: the title's second
   line, the description's second line, and the ghost "Read more" row that
   section 17.9 explicitly re-shows for Editorial. Together they were ~60px on
   every short row. The CLAMP stays in all three cases — a long description is
   still previewed at two lines with the full text one click away in the Read
   more modal, which is what the card is meant to show. */
#so-booking-widget[data-template="editorial"] .so-bw-card-grid {
    grid-auto-rows: auto;
}
#so-booking-widget[data-template="editorial"] .so-bw-card--service .so-bw-card-title {
    min-height: 0;
}
#so-booking-widget[data-template="editorial"] .so-bw-card-meta--clamp {
    min-height: 0;
}
/* (1,3,0): 17.9 re-shows the ghost at (1,2,0) and is later in source. */
#so-booking-widget[data-template="editorial"][data-template-id] .so-bw-card-more--ghost {
    display: none;
}
/* The base rule pushes the badges down with `margin-top: auto` so they level
   across a grid. One column, auto rows: there is nothing to level against and
   the auto margin only reopens the gap the reservations above just closed. */
#so-booking-widget[data-template="editorial"] .so-bw-card--service .so-bw-card-badges {
    margin-top: 0;
    padding-top: 4px;
}

/* ── 27.6 · The ONE thing that is bounded, and why it is only this ───────
   THE DESCRIPTION, AND NOTHING ELSE. 27.2 deliberately leaves the layout to
   the width setting, so this is the only cap in the section — and it is on
   the only run of long prose on the card. At Full Width on a 1440px screen
   the description measured 1240px, roughly 190 characters a line, which is
   about three times what anyone reads comfortably and the exact opposite of
   what a serif template is for. 62ch is around eleven words a line, and close
   enough to the two-line clamp that the preview and its ellipsis agree about
   where the text stops.

   `ch` rather than px so the bound tracks the font the Studio resolved
   instead of assuming Newsreader's metrics, and so it shrinks with the
   type on a phone rather than needing a breakpoint.

   NOT applied to the title, the badges, the search field or any wrapper. A
   service NAME is short by nature — capping it would bound the layout through
   the back door, which is the fault this revision exists to remove. It gets
   `overflow-wrap: anywhere` and nothing else, for the reason 26.6 records: a
   name with no spaces in it reported scrollWidth > clientWidth and was
   clipped mid-word with no way to reach the rest. */
#so-booking-widget[data-template="editorial"] .so-bw-card-meta {
    max-width: 62ch;
    overflow-wrap: anywhere;
}
#so-booking-widget[data-template="editorial"] .so-bw-card-title {
    overflow-wrap: anywhere;
}

/* ── 27.7 · Selection, marked the way a printed listing marks an entry ───
   Section 16's C2 block already says what this should look like — "a rule
   down its left edge" — and it has never rendered. Two later rules erase it:
   the SELECTION FEEDBACK block sets `box-shadow: 0 0 0 2px` !important at
   (1,3,0), and the ring block after it replaces that with a 2px `::after`
   border and `box-shadow: none !important`. So Editorial's chosen line got
   the same gold rectangle every other template draws — a box, on the one
   template built out of rules instead of boxes — and the left rule and the
   14px indent in C2 were both dead letters.

   (1,4,0) via the second template attribute plus the `.so-bw-card` compound,
   which is what it takes to answer a (1,3,0) !important. The marking is three
   things that agree: a 3px gold rule down the left edge, the hairline under
   the row promoted to the accent, and a 7% wash of the accent — well under
   the shared 15%, because on ivory 15% is a yellow band and the point here is
   that the row still reads as type on a page. */
#so-booking-widget[data-template="editorial"][data-template-id] .so-bw-card.so-bw-card--selected {
    box-shadow: inset 3px 0 0 var(--so-bw-primary) !important;
    background: color-mix(in srgb, var(--so-bw-primary) 7%, transparent) !important;
    border-bottom-color: var(--so-bw-primary) !important;
}
/* The ring, off. It is the mechanism the other templates need because their
   cards are boxes; here it draws the box this template exists without. */
#so-booking-widget[data-template="editorial"][data-template-id] .so-bw-card.so-bw-card--selected::after {
    display: none;
}
/* The indent C2 asked for, restated at a specificity that reaches: the card's
   own `padding: 16px 2px !important` is (1,3,0). 2px + the 3px rule + 10px of
   air, so the chosen line steps out from the column exactly as far as the
   rule is wide plus a readable gap. */
#so-booking-widget[data-template="editorial"][data-template-id] .so-bw-card--service.so-bw-card--selected {
    padding-left: 15px !important;
}
/* Hover is the same gesture at a lower weight — a wash and no rule — so the
   two never read as the same state. The shared hover paints an 8% inset
   flood plus a drop shadow and a 1px ring, which is a lifted box. */
#so-booking-widget[data-template="editorial"][data-template-id] .so-bw-card:hover:not(.so-bw-card--selected) {
    box-shadow: inset 0 0 0 999px color-mix(in srgb, var(--so-bw-primary) 4%, transparent) !important;
}
#so-booking-widget[data-template="editorial"][data-template-id] .so-bw-card:hover:not(.so-bw-card--selected)::after {
    display: none;
}
/* The tick, in the accent rather than as a filled disc, so the state is
   legible without a second solid on a page that has none. */
#so-booking-widget[data-template="editorial"] .so-bw-card--selected .so-bw-card-title {
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}

/* ── 27.8 · Continue says which of the two states it is in ───────────────
   The shared disabled treatment is `opacity: 0.5`, which on a bronze fill
   over ivory is a paler bronze fill — still obviously a button, still
   obviously the primary action, and about two points of contrast away from
   the live one. On a step whose whole job is "pick something first", that is
   the one control that has to be unambiguous.

   So the two states differ in KIND, not in strength: disabled is an outline
   with muted ink and no fill, enabled is the solid gold the template uses for
   every other commitment. Same size, same position, same border box — nothing
   moves when it flips, only what it is made of. */
#so-booking-widget[data-template="editorial"] .so-bw-nav .so-bw-btn--primary:disabled,
#so-booking-widget[data-template="editorial"] .so-bw-nav .so-bw-btn--secondary:disabled,
#so-booking-widget[data-template="editorial"] .so-bw-nav .so-bw-btn--confirm:disabled {
    opacity: 1;
    background: transparent;
    border-color: var(--so-bw-ds-n300);
    color: var(--so-bw-ds-n500);
    box-shadow: none;
    cursor: not-allowed;
}
/* And the live state stated rather than left to the base, so the pair is
   legible in one place. The hairline lift is the only shadow on the page and
   is what separates the bar from the list scrolling under it. */
#so-booking-widget[data-template="editorial"] .so-bw-nav .so-bw-btn--primary:not(:disabled),
#so-booking-widget[data-template="editorial"] .so-bw-nav .so-bw-btn--confirm:not(:disabled) {
    background: var(--so-bw-primary);
    border-color: var(--so-bw-primary);
    color: var(--so-bw-on-primary, #ffffff);
}
/* Hover darkens the gold with the template's own INK rather than with black,
   so the pressed state stays in the palette the Studio resolved — and so no
   literal enters a file whose colour discipline is asserted. */
#so-booking-widget[data-template="editorial"] .so-bw-nav .so-bw-btn--primary:not(:disabled):hover,
#so-booking-widget[data-template="editorial"] .so-bw-nav .so-bw-btn--confirm:not(:disabled):hover {
    background: color-mix(in srgb, var(--so-bw-primary) 86%, var(--so-bw-text));
    border-color: color-mix(in srgb, var(--so-bw-primary) 86%, var(--so-bw-text));
}


/* ── 27.9 · THE ROW: plate · name and description · duration and price ───
   Editorial's choice step is an INDEX — one service per hairline-ruled row —
   so the row has three jobs to place, left to right, and until now it placed
   two of them. Section 25 gave every list template a 40px circular avatar,
   which is the right primitive for a set of INITIALS and the wrong one for a
   PHOTOGRAPH: a landscape service photo cropped into a 40px circle is a
   thumbnail of nothing. The duration and price sat under the description at
   the far left, so a 1346px row put its two facts in the first 120px and left
   the other 1200 empty.

     media   a 4:3 plate when there is a photo, the circle when there is not
     body    name over description, the reading column
     facts   duration and price, right-aligned against the row's right edge

   ONE MEDIA COLUMN WIDTH, WHICHEVER OF THE TWO IT HOLDS. The column is fixed
   at `--so-bw-ed-thumb` and the circle is centred in it, so the service NAME
   starts at the same x on every row whether or not that service has an image.
   Left to `auto` the column would size to its content and the text would step
   left and right down the list — the exact ragged edge an index may not have.
   This is the same reasoning the markup comment gives for rendering the media
   band on every card in the first place.

   FLUID, NOT FIXED. The plate tracks the container so a 390px phone is not
   handed the same 96px it takes at 1440px, and the circle is derived from it
   rather than stated, so the two can never drift apart. */
#so-booking-widget[data-template="editorial"] .so-bw-container {
    --so-bw-ed-thumb: clamp(72px, calc(var(--so-bw-cw, 720px) * 0.07), 96px);
}
#so-booking-widget[data-template="editorial"] .so-bw-card-media {
    /* 0.6 of the plate's width, so a 96px plate sits beside a 58px circle —
       both substantial, neither pretending to be the other. Consumed by the
       initials tile through --so-bw-icon-size (see section 25's primitive),
       which is why it has to be this token and not a bare width. */
    --so-bw-svc-avatar: calc(var(--so-bw-ed-thumb) * 0.6);
    /* Section 25 offsets the circle by this inset; the fixed column below
       supplies the alignment now, so a second offset would only push the
       media out of its own column. */
    --so-bw-svc-avatar-inset: 0px;
}
/* The three-column row. The card is already `display: grid !important` with
   named areas from section 25 (media / body / extras / panel); this only
   fixes the media track's width — the areas, and therefore the extras strip
   and the add-ons panel below, are untouched. */
#so-booking-widget[data-template="editorial"] .so-bw-card--service:not(.so-bw-sp__panel) {
    grid-template-columns: var(--so-bw-ed-thumb) minmax(0, 1fr);
    /* A floor, not a height — the requirement is a consistent rhythm down the
       list, not identical rows. Plate depth (3/4 of its width) plus the card's
       own 16px top and bottom, so a one-line service is exactly as tall as its
       plate needs and anything longer simply grows. Paired with the
       `grid-auto-rows: auto` in 27.5, which is what stopped every row being
       levelled to the tallest card in the catalogue. */
    min-height: calc(var(--so-bw-ed-thumb) * 0.75 + 32px);
}

/* ── The plate. Only when there is actually a photo ──────────────────────
   `.so-bw-card-media--placeholder` is bound in the markup whenever
   serviceImageCount() is 0, so `:not()` on it is a reliable "this row has an
   image" test with no new markup and no new binding.

   (1,3,0) — the `:not()` contributes its argument's class — which is what it
   takes to answer THREE (1,2,0) rules that all describe a different shape:
   section 25's circle (`aspect-ratio: 1; border-radius: 999px`), the cover
   band's `aspect-ratio: 2 / 1; max-height: 176px`, and Editorial's own 40px
   sizing. Each is correct for the layout it was written for; none of them is
   a 4:3 plate. */
#so-booking-widget[data-template="editorial"] .so-bw-card-media:not(.so-bw-card-media--placeholder) {
    width: var(--so-bw-ed-thumb);
    height: auto;
    aspect-ratio: 4 / 3;
    max-height: none;
    /* Subtle: this is a plate on a page of rules, not a rounded app tile. Two
       pixels reads as "finished" at 72px and does not announce itself. */
    border-radius: var(--so-bw-r-2);
    /* Behind a transparent PNG or a still-loading image, in the template's own
       ivory rather than the shared grey. */
    background: color-mix(in srgb, var(--so-bw-primary) 8%, var(--so-bw-bg));
    justify-self: start;
    align-self: start;
}
/* `object-fit: cover` and the 100%/100% box come from the base
   `.so-bw-card-media-img` rule and need no restating; what does need stating
   is that the crop follows the plate's corner rather than the circle's. */
#so-booking-widget[data-template="editorial"] .so-bw-card-media:not(.so-bw-card-media--placeholder) .so-bw-card-media-img {
    object-fit: cover;
    border-radius: inherit;
}
/* The fallback is unchanged in KIND — section 25's circle, with the service's
   initials — and only centred in the wider column so it shares the plate's
   centre line and the text beside it starts where the text on a photo row
   does. */
#so-booking-widget[data-template="editorial"] .so-bw-card-media--placeholder {
    justify-self: center;
    align-self: start;
}
/* Both variants hang from the top of the row, level with the service name.
   Section 25 pushes the circle down by `--so-bw-card-pad - 4` to sit it
   optically on a title that has a card-top padding above it; a plate three
   times the height reads as sunk with the same offset, so both take the
   card-top's padding exactly and start on the title's line. */
#so-booking-widget[data-template="editorial"] .so-bw-card-media {
    margin: 12px 0 0 0;
}

/* ── The body: reading column, then the facts, right-aligned ─────────────
   `.so-bw-card-badges` is two levels down from the card (card > card-top >
   card-body > badges), so it cannot be placed by the card's own grid areas.
   The body becomes the two-column grid instead: everything that reads
   left-to-right stays in column one, and the badges take column two and span
   the rows, which puts the duration and the price on the service name's line
   at the right-hand end of the row.

   `grid-row: 1 / -1` with `align-self: start` rather than a fixed row, so the
   facts stay level with the NAME however many rows the description takes —
   and so a row where the description is absent entirely does not leave them
   stranded in a cell that no longer exists. */
/* THE BODY STAYS A FLEX COLUMN, AND THAT IS NOT A STYLE PREFERENCE.
   The first cut of this made `.so-bw-card-body` a two-column grid and put the
   badges in column two — which silently destroyed the description. GRID AND
   FLEX ITEMS BLOCKIFY their `display`, and the description's two-line preview
   is `display: -webkit-box` with `-webkit-line-clamp: 2` (see the clamp block
   further up). Blockified to `flow-root`, the line clamp stops applying: the
   box measured `display: flow-root` and 0x0, so every long description
   vanished from the list with no ellipsis and no Read more — the exact
   content-destroying failure the clamp block's own note says it exists to
   avoid. Anything that re-parents the description into a grid or flex track
   breaks it, so the facts have to reach the right-hand edge without touching
   the layout the description sits in.

   ABSOLUTE, against the card. `.so-bw-card--selected` already sets
   `position: relative` on the card and the base card rule sets it too, so the
   containing block is there and is the ROW — which is what the facts should
   align to anyway, not to the text column's flow. Out of flow means the
   description keeps its own display value, and it means the badges cannot add
   a line to the card's height.

   `right` clears the 28px selection tick plus its gutter, so the price never
   collides with the check when a row is picked. */
#so-booking-widget[data-template="editorial"] .so-bw-card--service:not(.so-bw-sp__panel) .so-bw-card-badges {
    position: absolute;
    top: 14px;
    right: 46px;
    justify-content: flex-end;
    /* The facts are short and must not wrap into two lines while there is a
       thousand pixels of empty row beside them. */
    flex-wrap: nowrap;
    white-space: nowrap;
    margin-top: 0;
    padding-top: 0;
    /* The two differ in size (below), so they sit on a shared baseline rather
       than on their box centres — otherwise the smaller one floats. */
    align-items: baseline;
    gap: 14px;
}
/* The reading column then stops where the facts begin, so a long service name
   cannot run underneath the price. Both are already bounded for readability
   (27.6); this is the narrower of the two bounds on a wide row and the same
   62ch on a narrow one. */
#so-booking-widget[data-template="editorial"] .so-bw-card--service .so-bw-card-title,
#so-booking-widget[data-template="editorial"] .so-bw-card--service .so-bw-card-meta {
    max-width: min(62ch, calc(100% - 210px));
}

/* ── The two facts, at readable sizes and in the right order of weight ───
   Both were 11px mono in the muted ink: the same size, the same colour and
   the same weight, so the price — the fact the customer is actually scanning
   for — had no more presence than the duration beside it and neither was
   comfortable to read at arm's length.

   The price leads on all three counts: two and a half points larger,
   semi-bold, and in full-strength ink against the duration's secondary. The
   MONO FACE STAYS on both, deliberately — section 13 sets every quantity in
   the widget in mono, and a mono figure under a Newsreader headline is the
   contrast this register is built on. Making the price a serif to enlarge it
   would have bought prominence by leaving the type system.

   FLUID, so "responsive" is not a second set of numbers at a breakpoint. Both
   multipliers are set so the template's own 720px measure lands mid-band —
   13.5px and 15.5px — the desktop cap is the top of each band, and a narrow
   container falls to a floor rather than scaling down without limit. */
#so-booking-widget[data-template="editorial"] .so-bw-badge--duration {
    font-size: clamp(12.5px, calc(var(--so-bw-cw, 720px) * 0.0187), 14px);
    font-weight: 500;
    color: var(--so-bw-text-secondary);
}
#so-booking-widget[data-template="editorial"] .so-bw-badge--price,
#so-booking-widget[data-template="editorial"] .so-bw-badge--free {
    font-size: clamp(14px, calc(var(--so-bw-cw, 720px) * 0.0215), 16px);
    font-weight: 600;
    color: var(--so-bw-text);
    letter-spacing: -0.01em;
}
/* "Read more" keeps its 44px tap floor — that is a WCAG target size and not a
   spacing choice — but it no longer buys that height with a row of its own at
   the far left of an otherwise empty band. It sits directly under the
   description in column one, where the badges spanning column two absorb the
   row it occupies. Its own margins go, so the 44px is the whole cost rather
   than 44 plus four. */
#so-booking-widget[data-template="editorial"] .so-bw-card--service .so-bw-card-more {
    margin-top: 0;
    margin-bottom: 0;
    align-self: start;
}

/* ── Narrow: the facts go back under the name ────────────────────────────
   Two columns need a reading column wide enough to be worth having. Below a
   560px container the row is roughly 340px of content, and holding 90px of it
   for a right-aligned price leaves the description wrapping every four words.
   So the body collapses to one column and the facts return to their own line
   under the description — the layout this template already had on a phone.

   `:where([data-cw…])` contributes nothing, so these tie with the rules above
   at (1,3,0) and win on source order, exactly as the rest of the file's
   container-width tiers do. */
#so-booking-widget:where([data-cw~="560"])[data-template="editorial"] .so-bw-card--service:not(.so-bw-sp__panel) .so-bw-card-badges {
    position: static;
    justify-content: flex-start;
    padding-top: 5px;
}
#so-booking-widget:where([data-cw~="560"])[data-template="editorial"] .so-bw-card--service .so-bw-card-title,
#so-booking-widget:where([data-cw~="560"])[data-template="editorial"] .so-bw-card--service .so-bw-card-meta {
    max-width: 62ch;
}
/* The plate shrinks with the row rather than keeping a desktop footprint on a
   390px screen, where 96px of it would be a quarter of the width. The clamp
   above already floors at 72px; this only tightens the gap it leaves. */
#so-booking-widget:where([data-cw~="480"])[data-template="editorial"] .so-bw-container {
    --so-bw-ed-thumb: 64px;
}


/* ── 27.10 · The running total, in the template's own register ───────────
   REPORTED AS "a small yellow glitch inside the selected service layout",
   and it is not a glitch — it is `.so-bw-multi-summary`, the running-total
   bar that appears the moment a second service is ticked. It is drawn as a
   solid `var(--so-bw-primary)` pill with white text, a 10px radius and
   `box-shadow: 0 4px 12px rgba(99, 102, 241, 0.18)` — an INDIGO shadow left
   over from the violet accent it was designed against, which is still in the
   file. On Editorial that resolves to a solid gold slab with a faintly blue
   shadow, `position: sticky; bottom: 0` inside the scroller, floating over
   the list: MEASURED at 1350x45 sitting across the "Full Body Massage" row
   and cutting it in half. On a page whose entire proposition is that type
   does the work colour does elsewhere, it is the only solid fill on screen.

   Not hidden — Dusk can hide it (26.4) because it has a docked strip quoting
   the same two numbers, and Editorial has nowhere else to put them, so the
   customer would simply lose the running total. Restated instead in the
   register everything else here uses: no fill, no radius, no shadow, an
   accent rule above it and the total set in the accent.

   It stays sticky, and the opaque canvas is what makes that safe: the band
   now reads as a docked summary line with a rule under the list rather than
   as a slab lying on top of it. */
#so-booking-widget[data-template="editorial"] .so-bw-multi-summary {
    margin-top: 0;
    padding: 11px 0 3px;
    background: var(--so-bw-bg);
    color: var(--so-bw-text);
    border-radius: 0;
    border-top: 1.5px solid var(--so-bw-primary);
    box-shadow: none;
}
#so-booking-widget[data-template="editorial"] .so-bw-multi-summary-label {
    font-family: 'Newsreader', Georgia, serif;
    font-size: 15px;
    font-weight: 500;
}
/* The two figures in the accent and in mono, which is how every other
   quantity in this template is set (see the dateline note in section 14). */
#so-booking-widget[data-template="editorial"] .so-bw-multi-summary-meta {
    font-family: var(--so-bw-mono);
    font-size: 12.5px;
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}


/* ── 27.11 · The card is the same size on every step ─────────────────────
   The shell is bounded above (`max-height: min(760px, 100dvh - 112px)`) and
   floored well below it (`min-height: clamp(360px, 52vh, 560px)`), so between
   those two the card is as tall as whatever step is showing. MEASURED at
   1440x900: Service 760, Date & Time 725, Your Details 701, Choose Staff 511.
   Advancing from the list to a two-card staff step collapses the widget by a
   quarter of its height and drags the Continue button 250px up the page with
   it — the customer's eye has to re-find the one control the step exists for,
   on every step, and anything below the widget on the host page jumps too.

   THE FLOOR IS RAISED TO MEET THE CEILING, rather than a height being pinned.
   `min-height` and `max-height` now differ by 60px on a tall desktop, so the
   card breathes with its content instead of jumping with it, and the header
   and the commit bar hold their positions across the whole flow.

   THE SECOND TERM IS THE SAME EXPRESSION THE CEILING USES, and that is what
   keeps it honest on a phone. `calc(100dvh - 112px)` is the viewport minus the
   embed's own chrome, so on any window shorter than ~810px BOTH bounds resolve
   to it, the floor and the ceiling meet exactly, and the card is precisely as
   tall as the screen allows — no rigid pixel height, nothing overhanging, and
   `.so-bw-body` scrolling inside it as 27.1 arranged. `dvh` not `vh` so a
   phone measures the collapsed-URL-bar viewport rather than the taller one it
   only has mid-scroll.

   700 against the ceiling's 760: the floor must never cross the ceiling, and
   because the two share their second term the only comparison that can matter
   is 700 < 760. A single `height` would have been simpler and wrong — it
   would stop the short steps growing when a long service name or a validation
   message needs the room.

   (1,3,0): the RESPONSIVE AUTO-HEIGHT block and its phone tier both set this
   at (1,2,0), and the second attribute is what beats them without touching
   the other twenty-one templates. */
#so-booking-widget[data-template="editorial"][data-template-id] .so-bw-container {
    min-height: min(700px, calc(100dvh - 112px));
}


/* ==========================================================================
   FULL WIDTH: THE SHELL SQUARES OFF, THE CONTENTS DO NOT
   --------------------------------------------------------------------------
   At "Full Width" the widget stops being a card on a page and becomes a band
   across it — BookingShortcode emits `alignfull` at this size precisely so a
   block theme cancels its own gutter and the widget reaches both edges. A
   rounded corner only reads as a corner when there is background on the other
   side of it. Spanning the viewport there is none, so the radius stops
   describing a card and starts looking like a rendering fault: a sliver of
   page colour notching the top two corners of a band that otherwise runs edge
   to edge.

   This is the same call the mobile tier already makes at the top of section
   22, and for the same reason — that tier squares the container when the
   container IS the viewport. Full Width is that situation on a desktop, so it
   gets the same answer rather than a different one.

   WHAT THIS DOES NOT TOUCH: the tokens. Not one line here writes
   --so-bw-radius*, --so-bw-ds-radius-*, or the step ladder, so every card,
   button, input, calendar day, time slot, panel and summary inside the shell
   goes on reading the Border Radius setting exactly as it does at every other
   size. Only the shell's own outer edge is squared, which is the whole point:
   the setting keeps working, it just stops being applied to a corner that has
   nothing to sit against.

   WHAT THE SHELL IS. The container's outer edge, plus the two elements that
   exist to sit flush inside it: `.so-bw-header` carries `X X 0 0` on seven
   templates for no reason other than to meet the container's top corners, and
   `.so-bw-nav` mirrors it along the bottom. Leave those rounded against a
   squared container and the container's own background shows through as two
   notches — worse than the corner this removed.

   The pseudo-elements are Deck's and Swipe's stacked-card illusion, offset
   copies of the container edge peeking out behind it. They square with it or
   they show rounded ghosts around a straight edge.

   `.so-bw-card-media` and `.so-bw-summary-row--total` carry the same
   top-only / bottom-only shape and are deliberately absent. They round
   against the CARD they cap, not against the page, so by the definition above
   they are contents and keep following the setting.

   THE EXCLUSION IS NOT A HEDGE. Frost's container is `background:
   transparent` and its header, body and nav are three separately rounded
   frosted panels FLOATING inside it (section 14, ~line 10781) — its header is
   a card, not an edge, and squaring it would flatten the one thing that makes
   the template legible as glass. Minimal, retired, insets its header the same
   way at 8px with a 16px margin. Compact declares the same inset but a later
   rule in its own block already squares that header at every size, so listing
   it here changes nothing today and is kept for the day that rule moves.

   Their CONTAINERS still square, because a container is a shell on every
   template; only the header/nav claim needs the carve-out.

   An exclusion list rather than an inclusion list because a flush header is
   the norm and an inset one the exception: a new template gets the right
   behaviour by default, and only a new FLOATING-PANEL header has to be added
   here. If you add one and its corners disappear at Full Width, this is why.

   `!important`, reluctantly and specifically: ten templates set their
   container corner with `!important` of their own (wheel 28, glass 24,
   diagonal, gradient, classic, dark, bubble, minimal, boarding-pass, and
   timeline's asymmetric `0 8px 8px 0`). Specificity cannot answer an
   important declaration no matter how many attributes it matches, so without
   this the rule would work on thirteen templates and quietly fail on the ten
   that state their corner most emphatically.

   The value is a token so "square" can become "a small fixed radius" in one
   place without touching these selectors — see --so-bw-fullwidth-shell-radius
   in section 13a. It is a FIXED value by design, not a step on the ladder:
   the point is that the outer edge stops tracking the setting here.

   Nothing in this block sets width, padding, margin, display or position, and
   it is gated on [data-size="full"] alone, so every layout, every container
   query and every other size behaves exactly as before.
   ========================================================================== */
#so-booking-widget[data-size="full"] .so-bw-container,
#so-booking-widget[data-size="full"] .so-bw-container::before,
#so-booking-widget[data-size="full"] .so-bw-container::after {
    border-radius: var(--so-bw-fullwidth-shell-radius) !important;
}

/* THE ROOT IS THE OUTER EDGE ON TWO TEMPLATES, and the block above never
   reached it. `.so-bw-container` is the shell on twenty-one architectures, so
   squaring it squares what the visitor sees — but Card Deck and Glass Frost
   paint `#so-booking-widget` ITSELF (`border-radius: var(--so-bw-r-20)`, and
   on Frost that root is the gradient field the whole template rests on, with
   its container transparent above it). On those two the container's corner is
   invisible and the ROOT's is the one on screen, so Full Width squared a
   corner nobody could see and left the real one rounded: MEASURED on the live
   page at 1440, Frost at Full Width still showed 20px curves at all four
   corners of a band running edge to edge.

   Same gate, same token, same reasoning as the block above — at Full Width the
   outer edge stops tracking the radius setting, because there is no page
   margin left for a corner to sit against. INSIDE is untouched: Frost's three
   frosted panels, every card, every button and every media band keep reading
   the global radius, which is the whole point of squaring only the edge.

   `!important` for the same reason the block above needs it — both
   declarations it overrides are plain, but the rule sits in a cascade where
   ten templates state their corner emphatically, and matching the block it
   extends is what keeps the two behaving alike. */
#so-booking-widget[data-size="full"] {
    border-radius: var(--so-bw-fullwidth-shell-radius) !important;
}

#so-booking-widget[data-size="full"]:not([data-template="frost"]):not([data-template="compact"]):not([data-template="minimal"]) .so-bw-header,
#so-booking-widget[data-size="full"]:not([data-template="frost"]):not([data-template="compact"]):not([data-template="minimal"]) .so-bw-nav {
    border-radius: var(--so-bw-fullwidth-shell-radius) !important;
}


/* ==========================================================================
   STAFF PROFILE POPUP  ("View profile" on a staff card)
   ==========================================================================

   MUST be the last section in this file. Almost every rule below relies on
   source order to outrank a per-template rule of equal specificity (see the
   panel-surface note), and a section appended after it would silently break
   that.

   -- How this inherits all 19 templates without 19 blocks -------------------

   Three mechanisms, in order of how much they carry:

   1. TOKENS. The panel, its type, its chips and its dividers read
      --so-bw-card-bg / -border / -card-radius / -shadow-lg / -font /
      -text-* etc. Templates that express identity by re-declaring tokens on
      .so-bw-container (Dusk Champagne's dark canvas, the tinted templates,
      every radius/typography variant) are therefore inherited for free --
      the popup is a descendant of the container, so it sees those values.

   2. THE .so-bw-card CLASS on the panel element. Some templates skip tokens
      and set properties DIRECTLY on .so-bw-card (Glass Frost's translucent
      fill and pale edge is the clearest case). Carrying the class means the
      panel picks those up as well, so it reads as glass on Glass Frost and
      as paper everywhere else -- without this file naming either template.

   3. THE REAL BUTTON AND AVATAR PRIMITIVES. The footer uses
      .so-bw-btn--primary / --secondary and the header uses
      .so-bw-card-icon--avatar rather than popup-specific copies, so button
      shape, weight, letter-spacing, hover and the avatar tile are whatever
      the active template already decided they are.

   What remains here is STRUCTURE ONLY -- overlay, centring, the panel's
   scroll region, the hours grid, spacing. That is the layout the brief wants
   constant while the styling changes.

   Consequence of (2): the panel is a .so-bw-card, so it arrives with card
   LAYOUT too (display:flex, align-items:center, cursor:pointer, hover lift).
   Those are neutralised below at (1,2,0) -- id + two classes -- which
   outranks every `#so-booking-widget ... .so-bw-card` rule in this file at
   (1,1,0), !important included. Do not lower that specificity. */

/* -- The "View profile" affordance on the card ---------------------------- */

/* A button, not a link: it opens a dialog, it does not navigate.
   It sits in the card's normal flow under the title so it never covers the
   card's own tap target, and it stops propagation in JS so tapping it cannot
   also select the staff member. */
#so-booking-widget .so-bw-card-profile {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    margin-top: 4px;
    padding: 2px 0;
    background: none;
    border: 0;
    font: inherit;
    font-size: 12px;
    font-weight: 600;
    line-height: 1.3;
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
    cursor: pointer;
    text-align: left;
    /* 44px targets are the rule for controls the flow depends on; this one is
       secondary and sits INSIDE another target, where a 44px box would eat the
       card. The touch block below widens the hit area instead. */
    transition: opacity var(--so-bw-transition);
}
#so-booking-widget .so-bw-card-profile:hover { opacity: 0.75; }
#so-booking-widget .so-bw-card-profile:focus-visible {
    outline: 2px solid var(--so-bw-primary);
    outline-offset: 2px;
    border-radius: var(--so-bw-r-4);
}
#so-booking-widget .so-bw-card-profile svg {
    width: 12px;
    height: 12px;
    flex-shrink: 0;
}
@media (hover: none) {
    /* Fingers, not cursors: grow the hit area without changing the visual box. */
    #so-booking-widget .so-bw-card-profile { padding: 6px 0; }
}

/* -- Overlay -------------------------------------------------------------- */

/* position:fixed, so the popup is centred in the WINDOW rather than inside the
   card list -- which is a scroll container with a bounded height, i.e. the one
   place a centred dialog cannot be centred.
   Safe here because nothing between the root and this element establishes a
   containing block: no ancestor carries transform / filter / will-change.
   (Glass Frost's backdrop-filter is on .so-bw-body and .so-bw-nav -- this
   element is a sibling of those, inside .so-bw-container, which is exactly
   where it has to be to still see the container's token overrides.) */
#so-booking-widget .so-bw-sp {
    position: fixed;
    inset: 0;
    z-index: 2147483000;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 20px;
    /* A neutral scrim on purpose. Tinting it per template would fight the
       panel for attention, and the templates where it matters most are the
       dark ones, where an accent-tinted scrim reads as a colour cast. */
    background: rgba(16, 18, 26, 0.55);
    -webkit-backdrop-filter: blur(2px);
    backdrop-filter: blur(2px);
    /* The one case that scrolls: a window too short for the panel. */
    overflow-y: auto;
    overscroll-behavior: contain;
    font-family: var(--so-bw-font);
    color: var(--so-bw-text);
}

/* -- Panel ---------------------------------------------------------------- */

/* Neutralises the card LAYOUT that the .so-bw-card class brings, while keeping
   everything it contributes visually. (1,2,0) -- see the section note. */
#so-booking-widget .so-bw-card.so-bw-sp__panel {
    display: flex !important;
    flex-direction: column !important;
    align-items: stretch !important;
    justify-content: flex-start !important;
    gap: 0 !important;
    cursor: default !important;
    transform: none !important;
    width: 100%;
    max-width: 420px;
    /* Auto margins, not one-axis centring: they centre the panel AND resolve to
       0 against negative free space, so a panel taller than a short window
       stays fully reachable instead of having its top cut off. */
    margin: auto !important;
    padding: 0 !important;
    max-height: calc(100vh - 40px);
    overflow: hidden;
    /* The lifted elevation, from the active template's own scale. */
    box-shadow: var(--so-bw-shadow-lg, 0 24px 48px -20px rgba(16, 18, 26, 0.35)) !important;
    text-align: left !important;
}

/* Card HOVER is for a pickable card; the panel is a surface you read. Without
   this, resting the cursor anywhere on the popup triggered
   `.so-bw-card:hover { box-shadow: inset 0 0 0 999px color-mix(primary 8%) }`,
   which floods the entire panel with the accent tint — and the pale chips and
   pale section rules disappear into it. Matching the aggressor's shape
   (id + [data-template] + 2 classes + :hover) so this outranks its !important
   rather than tying with it. */
#so-booking-widget[data-template] .so-bw-card.so-bw-sp__panel:hover,
#so-booking-widget[data-template] .so-bw-card.so-bw-sp__panel:hover:not(.so-bw-card--selected) {
    border-color: var(--so-bw-border) !important;
    box-shadow: var(--so-bw-shadow-lg, 0 24px 48px -20px rgba(16, 18, 26, 0.35)) !important;
}

/* The panel takes focus on open so a screen reader announces the dialog before
   any control (see openStaffProfile). Chrome then draws its default ring around
   the whole 420px panel, which reads as a stray black border on every open.

   Suppressed unconditionally, not via :focus:not(:focus-visible): Chrome DOES
   match :focus-visible on programmatic focus of a tabindex="-1" element (checked
   in-browser), so the carve-out never fired. No accessibility cost — the panel is
   a surface, not a control, and Close / Select Staff / the schedule toggle / the
   contact links all keep their own :focus-visible rings, which is what a keyboard
   user actually moves between. */
#so-booking-widget .so-bw-card.so-bw-sp__panel:focus {
    outline: none;
}

/* -- Regions -------------------------------------------------------------- */

/* A centred hero: the photo is the subject of this dialog, so it leads at a
   size worth looking at instead of sitting in a list row. position:relative for
   the close button, which is taken out of the flow so it cannot be pushed
   around by a long name. */
#so-booking-widget .so-bw-sp__head {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 10px;
    padding: 22px 18px 16px;
    border-bottom: 1px solid var(--so-bw-border);
    flex-shrink: 0;
    text-align: center;
}

/* Enlarges the template's own avatar primitive; everything else about the tile
   (fill, radius, photo clipping, icon colour) still comes from
   .so-bw-card-icon--avatar.

   Done by re-declaring --so-bw-icon-size on the element rather than by setting
   width/height: .so-bw-card-icon pins all six of width/height/min-/max- to that
   token, so a plain `height: 84px` was silently clamped back to 48 by its
   max-height and the hero rendered as an 84x48 oval. Overriding the token
   resolves every one of them at once, and keeps the size expressed in the same
   currency the templates use. */
#so-booking-widget .so-bw-sp__head .so-bw-card-icon.so-bw-sp__avatar {
    --so-bw-icon-size: 84px;
    /* Initials / icon scale with the tile — at 84px the card-sized type looked
       lost in the middle of it. */
    font-size: 27px;
    /* A hairline ring lifts a photo off the panel on the light templates and
       keeps a dark avatar from merging into the dark ones. currentColor would
       track the initials, so this uses the surface border token. */
    box-shadow: 0 0 0 3px var(--so-bw-card-bg, #fff), 0 0 0 4px var(--so-bw-border);
}
#so-booking-widget .so-bw-sp__head .so-bw-card-icon.so-bw-sp__avatar svg {
    width: 34px;
    height: 34px;
}

#so-booking-widget .so-bw-sp__ident {
    min-width: 0;
    /* Room for the absolutely-positioned close button on both sides, so a
       centred name stays centred against the panel rather than against the
       leftover space. */
    padding: 0 28px;
}
#so-booking-widget .so-bw-sp__name {
    margin: 0;
    font-size: 18px;
    font-weight: 700;
    line-height: 1.25;
    color: var(--so-bw-ds-n900, var(--so-bw-text));
    /* Long names wrap rather than push the close button off the edge. */
    overflow-wrap: anywhere;
}
#so-booking-widget .so-bw-sp__role {
    margin: 3px 0 0;
    font-size: 13px;
    font-weight: 600;
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}
#so-booking-widget .so-bw-sp__x {
    position: absolute;
    top: 10px;
    right: 10px;
    flex-shrink: 0;
    width: 32px;
    height: 32px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: none;
    border: 0;
    border-radius: var(--so-bw-ds-radius-chip, 8px);
    color: var(--so-bw-text-secondary, var(--so-bw-ds-n500));
    cursor: pointer;
    font: inherit;
    line-height: 1;
    transition: background var(--so-bw-transition), color var(--so-bw-transition);
}
#so-booking-widget .so-bw-sp__x:hover {
    background: var(--so-bw-bg-tertiary, var(--so-bw-ds-n100));
    color: var(--so-bw-text);
}
#so-booking-widget .so-bw-sp__x:focus-visible {
    outline: 2px solid var(--so-bw-primary);
    outline-offset: 1px;
}

/* The ONE scroll region -- the widget's own convention (pinned header and
   footer, scrolling middle), so a long bio behaves the way every other long
   list in this widget does. */
#so-booking-widget .so-bw-sp__body {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    padding: 14px 18px 16px;
    scrollbar-width: thin;
    scrollbar-color: var(--so-bw-sb-thumb, var(--so-bw-border)) transparent;
}
#so-booking-widget .so-bw-sp__body::-webkit-scrollbar { width: 5px; }
#so-booking-widget .so-bw-sp__body::-webkit-scrollbar-track { background: transparent; }
#so-booking-widget .so-bw-sp__body::-webkit-scrollbar-thumb {
    background: var(--so-bw-sb-thumb, var(--so-bw-border));
    border-radius: 999px;
}

/* Sections render only when they have content (the widget's x-show gates), so
   this gap never collapses onto an empty block. */
#so-booking-widget .so-bw-sp__sec + .so-bw-sp__sec { margin-top: 14px; }
#so-booking-widget .so-bw-sp__label {
    margin: 0 0 5px;
    font-size: 10.5px;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--so-bw-text-tertiary, var(--so-bw-ds-n500));
}
#so-booking-widget .so-bw-sp__text {
    margin: 0;
    font-size: 13.5px;
    line-height: 1.55;
    color: var(--so-bw-text-secondary, var(--so-bw-ds-n700));
    overflow-wrap: anywhere;
}

/* Hours: a two-column grid so the times align down the column instead of
   ragging after variable-length day names. Mono for the numbers, matching
   every other time in the widget. */
#so-booking-widget .so-bw-sp__hours {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: 3px 12px;
    margin: 0;
    font-size: 13px;
}
#so-booking-widget .so-bw-sp__day {
    font-weight: 600;
    color: var(--so-bw-text-secondary, var(--so-bw-ds-n700));
}
#so-booking-widget .so-bw-sp__time {
    margin: 0;
    font-family: var(--so-bw-mono);
    font-size: 12.5px;
    color: var(--so-bw-text-secondary, var(--so-bw-ds-n700));
    font-variant-numeric: tabular-nums;
}

/* "View full schedule" — a text affordance, deliberately not a .so-bw-btn: it
   reveals detail inside a section and must not compete with Select Staff in the
   footer. Same treatment as the card's own "View profile" link, so the two read
   as the same class of control. */
#so-booking-widget .so-bw-sp__toggle {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    margin-top: 8px;
    padding: 4px 0;
    background: none;
    border: 0;
    font: inherit;
    font-size: 12.5px;
    font-weight: 600;
    line-height: 1.3;
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
    cursor: pointer;
    transition: opacity var(--so-bw-transition);
}
#so-booking-widget .so-bw-sp__toggle:hover { opacity: 0.75; }
#so-booking-widget .so-bw-sp__toggle:focus-visible {
    outline: 2px solid var(--so-bw-primary);
    outline-offset: 2px;
    border-radius: var(--so-bw-r-4);
}
#so-booking-widget .so-bw-sp__toggle svg {
    flex-shrink: 0;
    transition: transform var(--so-bw-transition);
}
@media (hover: none) {
    #so-booking-widget .so-bw-sp__toggle { padding: 8px 0; }
}

#so-booking-widget .so-bw-sp__chips {
    display: flex;
    flex-wrap: wrap;
    gap: 5px;
}
#so-booking-widget .so-bw-sp__chip {
    padding: 3px 9px;
    border-radius: var(--so-bw-ds-radius-chip, 8px);
    background: var(--so-bw-primary-light, var(--so-bw-bg-tertiary));
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
    font-size: 11.5px;
    font-weight: 600;
}

/* Sits inside the hero now, so it centres with the name. */
#so-booking-widget .so-bw-sp__rating {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    margin-top: 6px;
    font-size: 13px;
    font-weight: 600;
    color: var(--so-bw-text-secondary, var(--so-bw-ds-n700));
}

/* The stars are aria-hidden decoration; this carries the same information as
   text for a screen reader without printing it twice on screen. */
#so-booking-widget .so-bw-sp__sr {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}
#so-booking-widget .so-bw-sp__stars {
    display: inline-flex;
    gap: 1px;
    color: var(--so-bw-warning, #f59e0b);
}
#so-booking-widget .so-bw-sp__stars svg { width: 13px; height: 13px; }
#so-booking-widget .so-bw-sp__score { font-family: var(--so-bw-mono); }
#so-booking-widget .so-bw-sp__count {
    font-weight: 500;
    color: var(--so-bw-text-tertiary, var(--so-bw-ds-n500));
}

#so-booking-widget .so-bw-sp__contact {
    display: flex;
    flex-direction: column;
    gap: 4px;
    font-size: 13px;
}
#so-booking-widget .so-bw-sp__contact a {
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
    text-decoration: none;
    overflow-wrap: anywhere;
}
#so-booking-widget .so-bw-sp__contact a:hover { text-decoration: underline; }

/* -- Footer --------------------------------------------------------------- */

/* The buttons are the template's own .so-bw-btn primitives, so only the row is
   described here. */
#so-booking-widget .so-bw-sp__foot {
    display: flex;
    gap: 8px;
    padding: 12px 18px;
    border-top: 1px solid var(--so-bw-border);
    flex-shrink: 0;
}
#so-booking-widget .so-bw-sp__foot .so-bw-btn { flex: 1 1 auto; }

/* -- States --------------------------------------------------------------- */

#so-booking-widget .so-bw-sp__state {
    padding: 26px 18px;
    text-align: center;
    font-size: 13px;
    color: var(--so-bw-text-tertiary, var(--so-bw-ds-n500));
}
#so-booking-widget .so-bw-sp__spinner {
    width: 22px;
    height: 22px;
    margin: 0 auto 10px;
    border: 2px solid var(--so-bw-border);
    border-top-color: var(--so-bw-primary);
    border-radius: 50%;
    animation: so-bw-sp-spin 700ms linear infinite;
}
@keyframes so-bw-sp-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
    #so-booking-widget .so-bw-sp__spinner { animation-duration: 2s; }
}

/* -- Phone ---------------------------------------------------------------- */

/* A bottom sheet, which is what a dialog is on a handset. Same 480px
   breakpoint the rest of the widget goes edge-to-edge at, and the radius still
   comes from the template, so a square-cornered template stays square. */
/* was @media (max-width: 480px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    #so-booking-widget:where([data-cw~="480"]) .so-bw-sp{
        padding: 0;
        align-items: flex-end;
    }
    #so-booking-widget:where([data-cw~="480"]) .so-bw-card.so-bw-sp__panel{
        max-width: none;
        max-height: 88vh;
        margin: 0 !important;
        border-bottom-left-radius: 0 !important;
        border-bottom-right-radius: 0 !important;
    }
    #so-booking-widget:where([data-cw~="480"]) .so-bw-sp__foot{ padding-bottom: 16px; }
    /* Slightly smaller hero on a handset: the sheet is capped at 88vh, and the
       photo should not spend that budget at the expense of the About text. */
    #so-booking-widget:where([data-cw~="480"]) .so-bw-sp__head{
        padding: 18px 16px 14px;
    }
    #so-booking-widget:where([data-cw~="480"]) .so-bw-sp__head .so-bw-card-icon.so-bw-sp__avatar{
        /* The token again, for the same reason as the desktop rule. */
        --so-bw-icon-size: 72px;
        font-size: 23px;
    }
    #so-booking-widget:where([data-cw~="480"]) .so-bw-sp__head .so-bw-card-icon.so-bw-sp__avatar svg{
        width: 30px;
        height: 30px;
    }
    #so-booking-widget:where([data-cw~="480"]) .so-bw-sp__name{ font-size: 17px; }


/* -- Service description modal ------------------------------------------- */

/* The other end of "Read more" on a clamped service card.
 *
 * It is styled as a variant of the staff popup above, not as a new component,
 * and that placement is deliberate: it reuses .so-bw-sp (overlay) and
 * .so-bw-card.so-bw-sp__panel (surface), which is what gives it every
 * template's card colour, radius, typography and phone bottom-sheet treatment
 * for free — the three mechanisms described at the top of this section. Rules
 * that narrow it MUST come after the ones they narrow, which is why this is
 * appended here rather than filed with the rest of the consistency pass.
 *
 * Nothing above targets .so-bw-sd, so the staff popup is unaffected. */

#so-booking-widget .so-bw-card.so-bw-sd__panel {
    max-width: 460px;
    display: flex;
    flex-direction: column;
}
#so-booking-widget .so-bw-sd__head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 12px;
    padding: 18px 20px 12px;
    border-bottom: 1px solid var(--so-bw-border);
}
#so-booking-widget .so-bw-sd__title {
    margin: 0;
    font-family: var(--so-bw-font);
    font-size: 16px;
    font-weight: 700;
    line-height: 1.3;
    color: var(--so-bw-text);
    min-width: 0;
}
/* The staff popup's close button is absolutely positioned over its hero photo.
   There is no hero here, so it returns to the flow beside the title. */
#so-booking-widget .so-bw-sp__x.so-bw-sd__x {
    position: static;
    flex: 0 0 auto;
    margin: -2px -4px 0 0;
}
#so-booking-widget .so-bw-sd__body {
    padding: 14px 20px 4px;
    /* The description is the only content, so this is the scroll region — and
       it is capped rather than left to the panel so the Close button below
       cannot be pushed off a short screen by a very long description. */
    overflow-y: auto;
    overscroll-behavior: contain;
    max-height: min(52vh, 420px);
}
#so-booking-widget .so-bw-sd__text {
    margin: 0;
    font-family: var(--so-bw-font);
    font-size: 14px;
    line-height: 1.6;
    color: var(--so-bw-text-secondary);
    overflow-wrap: anywhere;
}
/* Rich-text body. Descriptions are authored in the admin's WYSIWYG and stored
   as HTML, so the paragraphing arrives as <p>/<ul>/<br> rather than as raw
   newlines — `white-space: pre-wrap` used to be how this block paragraphed and
   is deliberately gone: kept, it would render the newlines BETWEEN the tags as
   extra blank lines. Legacy plain-text descriptions come through soDescHtml(),
   which turns their newlines into <br>, so they paragraph the same way.

   The widget's reset zeroes list styling, so it has to be restored explicitly
   or a bulleted description arrives as an unmarked run of lines. */
#so-booking-widget .so-bw-rt > :first-child {
    margin-top: 0;
}
#so-booking-widget .so-bw-rt > :last-child {
    margin-bottom: 0;
}
#so-booking-widget .so-bw-rt p {
    margin: 0 0 0.7em;
}
#so-booking-widget .so-bw-rt h2,
#so-booking-widget .so-bw-rt h3,
#so-booking-widget .so-bw-rt h4 {
    margin: 0 0 0.5em;
    color: var(--so-bw-text);
    font-weight: 600;
    line-height: 1.35;
}
#so-booking-widget .so-bw-rt h2 { font-size: 16px; }
#so-booking-widget .so-bw-rt h3 { font-size: 15px; }
#so-booking-widget .so-bw-rt h4 { font-size: 14px; }
#so-booking-widget .so-bw-rt ul,
#so-booking-widget .so-bw-rt ol {
    margin: 0 0 0.7em;
    padding-left: 1.4em;
    list-style-position: outside;
}
#so-booking-widget .so-bw-rt ul { list-style-type: disc; }
#so-booking-widget .so-bw-rt ol { list-style-type: decimal; }
#so-booking-widget .so-bw-rt li {
    display: list-item;
    margin: 0 0 0.25em;
}
#so-booking-widget .so-bw-rt a {
    color: var(--so-bw-primary);
    text-decoration: underline;
}
#so-booking-widget .so-bw-rt b,
#so-booking-widget .so-bw-rt strong {
    font-weight: 700;
    color: var(--so-bw-text);
}
#so-booking-widget .so-bw-rt img {
    max-width: 100%;
    height: auto;
}
/* One button, so it goes to the right rather than stretching across the foot
   the way the popup's Close / Select pair does. */
#so-booking-widget .so-bw-sp__foot.so-bw-sd__foot {
    justify-content: flex-end;
}
#so-booking-widget .so-bw-sp__foot.so-bw-sd__foot .so-bw-btn {
    flex: 0 0 auto;
}
/* was @media (max-width: 480px) — now gated on the CONTAINER width
   via data-cw / data-cwmin (see booking-widget.js _wireContainerWidth).
   The ID stays OUTSIDE :where() so the selector still scores (1,0,0) — the
   specificity the media query it replaces had. Inside, :where() would zero it
   and any plain #so-booking-widget rule would outrank the override. */

    #so-booking-widget:where([data-cw~="480"]) .so-bw-card.so-bw-sd__panel{ max-width: none; }
    #so-booking-widget:where([data-cw~="480"]) .so-bw-sd__body{ max-height: 56vh; }

/* ==========================================================================
   23 · THE BOUNDED SHELL — the widget is a pane, not a page
   ==========================================================================

   The widget container does NOT scroll. It is a fixed-height card, and exactly
   one region inside it scrolls: `.so-bw-body`. Header, step bar, search field
   and commit bar all sit outside that region and therefore never move.

   This RESTORES an architecture the file already had. The base container rule
   (~9555) has always been `display: flex; flex-direction: column;
   max-height: min(760px, 88vh)` — a bounded column — and the four grid
   architectures (Split, Accordion, Tabbed, Agenda) declare the same shape as
   `grid-template-rows: auto 1fr auto`. What broke it was the RESPONSIVE
   AUTO-HEIGHT block overriding that bound with `max-height: none !important`
   and the body with `overflow-y: visible !important`, which handed scrolling
   to the PAGE. That is why the whole widget scrolled and the search field
   scrolled away with it.

   Why the earlier release is not simply restored to its original form: it was
   written because the bounded card put a ~5px scrollbar on a 660px pane while
   the page itself did not scroll — two scroll regions competing. That fault
   came from bounding the card AND keeping ceilings on the lists inside it. The
   list ceilings are gone now (section above), so `.so-bw-body` is the only
   scroller in the widget and there is nothing left to compete with it.

   Placed last in the file: these must beat the RESPONSIVE AUTO-HEIGHT block at
   equal specificity and equal !important, which source order decides.
   ========================================================================== */

/* ── 1 · The shell is bounded, and bounded to fit the PAGE ────────────────
   Not `88dvh`. The widget is not the only thing on the page: the embed route
   renders a 16px body gutter top and bottom, and an identity banner above the
   card (~56px plus its 14px margin). That is ~102px the card does not get.
   `88dvh + 102px` exceeds the viewport on anything under ~850px tall, and the
   overflow — a few pixels — is what put a scrollbar on the page beside a
   widget that was already scrolling internally.

   `calc(100dvh - 112px)` subtracts that chrome instead of guessing a
   percentage that happens to clear it, with 10px of slack so a slightly taller
   banner does not put the bar straight back.

   THE 112 IS A TOKEN, because it is a fact about the ROUTE and not about the
   widget. It is right for the embed page, where the widget is the only thing
   on screen and any overflow is a second scrollbar for a page with nothing
   else to show. It is pure loss on a customer's own page: there is no banner
   to clear, the page scrolls for its own reasons anyway, and the subtraction
   just leaves a band of empty screen under the card. The default stays 112 so
   nothing changes for any template that does not opt out, the embed route pins
   it (see BookingShortcode::renderEmbed) so its guarantee cannot be weakened
   from here, and a template that knows better states its own — Coral Pop does,
   in section 39.

   `dvh` not `vh`: on a phone a `vh` bound measures against the URL-bar-
   collapsed viewport, so the card overhangs the screen until you scroll.
   The min-height floor from the release block stays — a two-card step should
   still read as a card, not a strip. Below roughly a 500px-tall viewport that
   floor wins and the page scrolls again, which is the honest outcome. */
#so-booking-widget[data-template] .so-bw-container,
#so-booking-widget .so-bw-container {
    max-height: min(760px, calc(100dvh - var(--so-bw-vph-reserve, 112px))) !important;
}

/* ── 2 · …and the body is the one thing inside it that scrolls ────────────
   `min-height: 0` is what makes this work at all. A flex item's default
   `min-height: auto` refuses to shrink below its content, so without it the
   body grows to fit every card, pushes the commit bar out of the card, and
   nothing ever scrolls — the classic flexbox overflow trap.

   overflow-x stays `clip`: releasing Y must not let one long unbroken service
   name put a horizontal bar on the page. `clip` rather than `hidden` because
   `hidden` would make this a scroll container on both axes and re-enable the
   very behaviour above. */
#so-booking-widget[data-template] .so-bw-body,
#so-booking-widget .so-bw-body {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto !important;
    overflow-x: clip;
    overscroll-behavior: contain;
    -webkit-overflow-scrolling: touch;
}

/* ── 3 · The search field holds its place ────────────────────────────────
   It lives inside `.so-bw-step`, which is inside the scroll region, so it
   would scroll away with the cards. Sticky pins it to the top of that region
   while the list moves underneath — the behaviour the DOM would otherwise
   require restructuring to get.

   Opaque background, or cards would read through it as they pass under. */
#so-booking-widget[data-template] .so-bw-search,
#so-booking-widget .so-bw-search {
    position: sticky;
    top: 0;
    z-index: 4;
    background: var(--so-bw-card-bg);
}

/* ── 4 · The commit bar no longer needs a landing strip ───────────────────
   `.so-bw-step` carried `padding-bottom: clamp(72px, 10vw, 96px)` so the last
   card was not hidden under a nav that floated over it. The nav is outside the
   scroll region now and cannot overlap anything, so that padding is just a
   band of dead space at the end of every list. */
#so-booking-widget[data-template] .so-bw-step,
#so-booking-widget .so-bw-step {
    padding-bottom: 16px !important;
}


/* ==========================================================================
   28 · FROST — THE READING PASS
   --------------------------------------------------------------------------
   Frost shipped as a material study and reads as one. What it never got was a
   pass for the customer who is actually USING it: a sticky field that let the
   catalogue slide into it, cards whose text was clamped before the price, a
   commit bar with more air than button, a 12px system scrollbar drawn straight
   through the glass, and a slot grid whose chips changed size from one daypart
   to the next.

   Everything here is spacing, containment and state. The material is
   untouched: same fills, same blur, same accent, same fallback ladder
   (@supports floor for the browsers without backdrop-filter), same card shape,
   same avatar fallbacks, same behaviour. Nothing below changes what the widget
   DOES; it changes what can be read while it does it.

   PLACED LAST, deliberately. Four of these rules have to beat the bounded-shell
   block immediately above — which is itself `!important` at (1,2,0) — so they
   are written at (1,3,0) via the `[data-template-id]` pair the file already
   uses for exactly this (see Dusk 26.7 and Editorial 27.4), or at (1,3,0) via
   the `--service` / `:not(.so-bw-sp__panel)` guards the section 17 rules carry.
   ========================================================================== */

/* ── 28.1 · The pane gets its height back ────────────────────────────────
   The shell bounds every template at `min(760px, 100dvh - 112px)`. That is a
   sensible default for a two-field step and the wrong one for this template:
   Frost is the widest card in the catalogue (720px) laid out two-up with a
   photo band on every tile, so 760px shows a row and a half and the customer
   scrolls from the first frame.

   THE TARGET IS THE LIST, NOT THE CARD. Only `.so-bw-body` takes the extra
   height — the progress header and the commit bar are `flex: 0 0 auto` either
   side of it and keep the sizes they had. Sized so the service step shows two
   full rows of the two-up grid rather than a row and a sliver: a card is
   ~280px after the shallower header band (29.1), so two rows plus their 12px
   gutter is 572px, and the search band, the step line and the body's own
   padding account for ~135px more. 980px of shell leaves the body ~790px,
   which is two rows and the top of the third — the "there is more below this"
   cue that a list cut exactly at a row boundary loses.

   The viewport term drops to 64px because Frost's own sheets already stand
   12px in from the container edge — part of the clearance the shell's 112px
   was buying is inside the template. It stays a `min()` against `dvh`, so on
   anything under ~1044px tall the screen decides and the card cannot overflow
   it; `dvh` rather than `vh` so a phone's collapsing URL bar is measured
   honestly. Below the release block's min-height floor the page scrolls, which
   is unchanged. */
#so-booking-widget[data-template="frost"][data-template-id] .so-bw-container {
    max-height: min(980px, calc(100dvh - 64px)) !important;
}

/* ── 28.2 · The search field stops being a hole ──────────────────────────
   REPORTED: the catalogue reads THROUGH and INTO the search bar as it scrolls.

   Two faults, one symptom. The shell pins the field with `background:
   var(--so-bw-card-bg)` — an OPAQUE WHITE box on a sheet that is 62% white, so
   on Frost the pin is visible as a bright band before a single card reaches
   it. And the 12px below the field is MARGIN, which is transparent: a card
   scrolling past is painted in it, hard against the field's bottom edge, which
   is the overlap that was reported.

   The band is painted by a `::before` rather than by padding on the element
   itself, because the magnifier and the clear "×" are positioned absolutely
   against `.so-bw-search` — padding it would carry them off the input. The
   overlay bleeds -24px sideways (the step's gutter) so the band spans the full
   sheet rather than stopping short of it, and -18px up / -14px down so it
   covers both the region above the pin and the gap the cards were showing
   through.

   `backdrop-filter` rather than a solid fill is the Frost-native answer to
   "opaque": a card passing under is blurred into the sheet instead of being
   hidden by a white rectangle, so the pin still reads as glass. The @supports
   floor below raises the fill for the browsers that have no blur to give. */
/* The MARGIN is deliberately not set here. It was, at 14px, and being (1,3,0)
   it silently outranked the 10px that section 31 sets to tighten this area —
   the figure has to live in one place, and 31 is where it is derived from a
   measurement. Leaving it out lets 31.1 (1,2,0) beat the base rule and own it.
   Everything this rule DOES set is about the pin and the paint.

   `top` IS SET IN 31.2, NEGATIVE, and that is not a tweak — see the note there.
   A sticky element in this scroller pins to the scroll container's CONTENT box,
   so `top: 0` leaves it the body's whole padding-top below the visible edge,
   with the catalogue passing through the strip above it. */
#so-booking-widget[data-template="frost"][data-template-id] .so-bw-search {
    position: sticky;
    z-index: 6;
    isolation: isolate;
    background: transparent;
}
#so-booking-widget[data-template="frost"] .so-bw-search::before {
    content: '';
    position: absolute;
    inset: -18px -24px -14px;
    z-index: -1;
    /* 0.68 is the value between two failures. Too opaque and the band reads as
       a white bar laid across the glass AT REST, which is when it is looked at
       most; too sheer and a catalogue of photographs pushes colour through it —
       blurred, but a magenta wash sliding under the search field is noise
       whatever its edges are doing. 22px of blur does the hiding, this does the
       muting, and neither alone was enough. */
    background: rgba(255, 255, 255, 0.68);
    -webkit-backdrop-filter: blur(22px) saturate(160%);
    backdrop-filter: blur(22px) saturate(160%);
    border-bottom: 1px solid rgba(255, 255, 255, 0.75);
    pointer-events: none;
}
/* The field itself set the way F8 sets every other Frost control — translucent
   over the sheet. It was `--so-bw-card-bg`, i.e. the one opaque white box on a
   template that has no opaque white boxes. */
#so-booking-widget[data-template="frost"] .so-bw-search-input {
    background: rgba(255, 255, 255, 0.58);
    border-color: rgba(255, 255, 255, 0.85);
}
#so-booking-widget[data-template="frost"] .so-bw-search-input:focus {
    background: rgba(255, 255, 255, 0.86);
    border-color: var(--so-bw-primary);
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
    #so-booking-widget[data-template="frost"] .so-bw-search::before {
        background: rgba(255, 255, 255, 0.96);
    }
}
@media (forced-colors: active) {
    #so-booking-widget[data-template="frost"] .so-bw-search::before {
        background: Canvas;
        border-bottom-color: CanvasText;
    }
}

/* ── 28.3 · The card says everything it has to say ───────────────────────
   REPORTED: titles and descriptions cut with an ellipsis; the price sometimes
   below the fold of its own card.

   Two catalogue-wide clamps land on the service card — the title at two lines
   (14136) and the description at two (13674) — and both were written for the
   ONE-COLUMN phone list, where a verbose service otherwise doubles a card's
   height. Frost is two-up on a 720px sheet with room to spare, and a customer
   choosing between "Full Balayage with Toner, Gloss and Blow-Dry Finish" and
   "Full Balayage with Toner, Gloss and…" is choosing blind.

   Releasing a `-webkit-line-clamp` takes BOTH the clamp and the `-webkit-box`
   display that carries it; `display: block` is what actually undoes it. The
   grid already levels rows (`grid-auto-rows: 1fr`), so a longer card lifts its
   own row rather than leaving a ragged edge, and Read more stays in the markup
   for the descriptions that are genuinely long.

   The check tile is a flex sibling of the text, and at `align-items: center`
   it sat against the middle of the description — reading as an icon dropped on
   top of the words. Section 29.2 lifts it out of the row entirely, so all this
   block still owes it is the gap it left behind. */
#so-booking-widget[data-template="frost"] .so-bw-card--service .so-bw-card-title {
    display: block !important;
    overflow: visible !important;
    -webkit-line-clamp: none !important;
    line-clamp: none;
}
#so-booking-widget[data-template="frost"] .so-bw-card--service .so-bw-card-meta--clamp {
    display: block !important;
    overflow: visible !important;
    -webkit-line-clamp: none !important;
    line-clamp: none;
}
/* Duration and price are the two facts the customer compares across the row,
   so they are never allowed to be the thing that wraps out of sight.
   `margin-top` stays `auto` (13725) — that is what holds every card's badges
   on one line across the grid — so the lead comes from padding. */
#so-booking-widget[data-template="frost"] .so-bw-card--service .so-bw-card-badges {
    flex-wrap: wrap;
    padding-top: 10px;
}

/* ── 28.3b · The staff card, with less air between its four parts ─────────
   REPORTED as slightly loose. MEASURED at 200px on a 1440 embed: 32 of that is
   the card's own padding and 48 is the three 16px gaps between the avatar, the
   name, the bio and View profile — 80px of spacing around 120px of content.
   The shells already knew this: a staff card inside a popup or a floating
   panel is given `gap: 6px; padding: 12px 10px` (section 40), because the
   generic 16/16 was never sized for this card. Inline just never got the same
   treatment.

   12px padding and a 10px gap: still airier than the shell's 12/6, so the card
   reads as a roomy grid tile rather than a dense list row, and 26px shorter.
   NOTHING ELSE MOVES — no direction, no alignment, no order, no min-heights —
   so the avatar, the name and View profile keep the exact left edge and centre
   axis they have today, on every card in the roster.

   Not the empty two-line bio box, which is the other thing that looks like
   slack on a member with no biography: `grid-auto-rows: 1fr` (section 16)
   levels every card in the row to the tallest, so collapsing it on the members
   who have none does not shorten the row — it just moves the same gap to the
   bottom of their cards. The reservation is what keeps View profile on one line
   across the roster, which is the alignment this is asked to preserve. */
#so-booking-widget[data-template="frost"] .so-bw-card--staff {
    gap: 10px;
    padding: 12px;
}

/* ── 28.4 · The commit bar is a bar, not a band ──────────────────────────
   77px for a 47px button: 14px of Frost's own gutter above and below, plus the
   button's own generous pad. The button keeps its 44px tap target — it is the
   one control on the step — and the air around it comes down, which is given
   back to the list on every step of the journey.

   8px, was 10px. Reported as slightly tall along with the header. MEASURED at
   66px: 44 of that is the button's accessible floor and cannot move, and 2 is
   the frosted sheet's own edge, so the padding is the only height there is to
   give — 62px is this bar as short as it can honestly be. */
#so-booking-widget[data-template="frost"] .so-bw-nav {
    padding: 8px var(--so-bw-gutter);
    gap: 10px;
}
#so-booking-widget[data-template="frost"] .so-bw-nav .so-bw-btn {
    --so-bw-btn-pad-y: 10px;
    min-height: 44px;
}

/* ── 28.5 · A scrollbar the glass can carry ──────────────────────────────
   The body is the widget's one scroll region and it was drawing the platform's
   default — a 12px opaque gutter down the right edge of a translucent sheet,
   which is the single most solid thing on the template. `scrollbar-width:
   thin` plus a translucent thumb makes it a rule rather than a trough; the
   -webkit- pseudo-elements carry the same for the engines that ignore the
   standard property. Track transparent, so the sheet runs edge to edge. */
#so-booking-widget[data-template="frost"] .so-bw-body {
    scrollbar-width: thin;
    scrollbar-color: rgba(47, 110, 168, 0.28) transparent;
}
#so-booking-widget[data-template="frost"] .so-bw-body::-webkit-scrollbar {
    width: 6px;
}
#so-booking-widget[data-template="frost"] .so-bw-body::-webkit-scrollbar-track {
    background: transparent;
}
#so-booking-widget[data-template="frost"] .so-bw-body::-webkit-scrollbar-thumb {
    background: rgba(255, 255, 255, 0.8);
    border-radius: 999px;
}
#so-booking-widget[data-template="frost"] .so-bw-body:hover::-webkit-scrollbar-thumb {
    background: rgba(var(--so-bw-primary-rgb, 99, 102, 241), 0.32);
}

/* ── 28.6 · The times are a grid, not a row of survivors ─────────────────
   REPORTED: a daypart holding one opening renders one full-width chip beside a
   daypart whose four chips are a quarter of that.

   This is the KNOWN OPEN noted on `.so-bw-slot-list` (section 2). `auto-fit`
   collapses the empty tracks and lets the survivors stretch, so the chip size
   encodes how BUSY the daypart is — which is meaningless to the customer and
   reads as a rendering fault. `auto-fill` keeps the empty tracks, so every
   chip in the panel is the same size whatever the day looks like. The base
   comment declines to make that change catalogue-wide because it would move
   all nineteen templates; taken here for Frost alone, at a track floor
   measured at this template's own panel width.

   150px is where "10:30 AM to 11:30 AM" stops breaking mid-range at 14px —
   measured, not chosen — and three across is what the 570px panel then holds.
   Three chips that each read in one glance beat four that each take two. */
#so-booking-widget[data-template="frost"] .so-bw-slot-list {
    grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
    gap: 8px;
    align-items: stretch;
}
#so-booking-widget[data-template="frost"] .so-bw-slot-row {
    min-height: 46px;
    padding: 8px 10px;
}

/* ── 28.7 · The times read as times ──────────────────────────────────────
   The numerals rule (section 13) puts every quantity in the widget on IBM Plex
   Mono, and for a step count or a calendar figure that is exactly right. On a
   chip carrying a full RANGE — "10:30 AM to 11:30 AM" — mono is ~15% wider per
   glyph, which is what forced a 13px size and a two-line wrap on a control the
   customer is scanning a dozen of.

   The template's own face at 14px/500 fits the range on one line and reads as
   UI rather than as a readout. `tabular-nums` is kept explicitly (Inter sets
   proportional figures by default), so the column of times stays as straight
   as mono had it — which was the reason the mono rule existed. */
#so-booking-widget[data-template="frost"] .so-bw-slot-row,
#so-booking-widget[data-template="frost"] .so-bw-slot-time {
    font-family: var(--so-bw-font);
    font-size: 14px;
    font-weight: 500;
    font-variant-numeric: tabular-nums;
    font-feature-settings: 'tnum' 1;
    letter-spacing: 0;
}
#so-booking-widget[data-template="frost"] .so-bw-slot-till {
    font-family: var(--so-bw-font);
    font-size: 13px;
    font-weight: 500;
}

/* ── 28.8 · Chosen, and unmistakably so ──────────────────────────────────
   The selected chip was the accent fill and nothing else, which on a grid of
   translucent chips is a colour difference and no more — and a colour
   difference is the one cue a customer glancing back at the step can miss.
   It takes the fill, a 2px edge of its own accent, a lift, and a tick: three
   channels rather than one.

   The tick is a masked SVG on `currentColor`, so it inherits the chip's own
   ink and needs no second colour declared anywhere.

   WHY THE DOUBLED CLASS. The SELECTION FEEDBACK block (~9630) paints every
   template's selected chip as a 16% accent WASH with body-coloured ink, at
   (1,3,0) and `!important` — which is why Frost's own selected rule (F7,
   (1,2,0), no !important) has never actually applied: the chip a customer
   picked has been a pale tint the whole time. `.so-bw-slot-row` is repeated
   here purely to match that (1,3,0) so source order can decide it, and the
   three properties it claims are re-claimed `!important`. The wash is right
   for the templates whose chips sit on a dark or coloured card and would lose
   their label under a solid fill; Frost's chips are white glass on white
   glass, where a 16% tint is the weakest signal in the catalogue. */
#so-booking-widget[data-template="frost"] .so-bw-slot-row.so-bw-slot-row--selected,
#so-booking-widget[data-template="frost"] .so-bw-slot-row.so-bw-slot-row--selected:hover {
    gap: 6px;
    background: var(--so-bw-primary) !important;
    border: 2px solid var(--so-bw-primary);
    color: rgba(255, 255, 255, 1) !important;
    font-weight: 600;
    box-shadow: 0 6px 16px -8px rgba(var(--so-bw-primary-rgb, 99, 102, 241), 0.6) !important;
}
#so-booking-widget[data-template="frost"] .so-bw-slot-row--selected::after {
    content: '';
    flex: 0 0 auto;
    width: 14px;
    height: 14px;
    background-color: currentColor;
    -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'/%3E%3C/svg%3E") center / contain no-repeat;
    mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'/%3E%3C/svg%3E") center / contain no-repeat;
}
/* A 2px border on the selected chip against 1px everywhere else would move the
   chip's content by a pixel on selection. The unselected chip reserves the
   same box with a transparent second pixel. */
#so-booking-widget[data-template="frost"] .so-bw-slot-row:not(.so-bw-slot-row--selected) {
    border-width: 1px;
    outline: 1px solid transparent;
    outline-offset: -2px;
}

/* ── 28.9 · Taken is dimmed, not erased ──────────────────────────────────
   The shared booked chip is `opacity: 0.45` over an opaque card. Over Frost's
   62% sheet that composites to roughly a quarter of the ink the open chips
   carry — the 3:00 PM in the audit shot is legible only if you already know it
   is there, and a time the customer cannot READ is not a time they understand
   is taken. The fill goes flat, the ink goes tertiary, the border goes quiet,
   and the opacity comes up to where the label survives the glass. Cursor and
   the disabled attribute are untouched: it is exactly as unclickable. */
#so-booking-widget[data-template="frost"] .so-bw-slot-row--booked,
#so-booking-widget[data-template="frost"] .so-bw-slot-row--booked:hover {
    background: rgba(255, 255, 255, 0.34);
    border-color: rgba(var(--so-bw-primary-rgb, 99, 102, 241), 0.14);
    color: var(--so-bw-text-tertiary);
    opacity: 0.75;
    box-shadow: none;
    cursor: not-allowed;
}

/* ── 28.10 · The clock control, compacted ────────────────────────────────
   "Your local time" stacked its label above a 230px combobox and took 60px of
   the panel head — taller than the "Time Slot" heading it sits beside, and
   optically unrelated to it. Laid out as a row it is 34px and the label reads
   as what it is: a prefix to the value.

   `align-items: baseline`, and this is the load-bearing word. The picker's
   panel expands IN FLOW below the trigger (the slot panel is a scroll
   container, so the menu cannot float), and under `center` the label would
   slide down the moment the menu opened — the drift the base rule stacked the
   two to avoid. Baseline pins the label to the trigger's first line, so the
   menu opens underneath and nothing above it moves. */
#so-booking-widget[data-template="frost"] .so-bw-slot-panel-head {
    align-items: baseline;
    gap: 8px 12px;
    padding-bottom: 6px;
}
#so-booking-widget[data-template="frost"] .so-bw-tz-picker {
    flex-direction: row;
    align-items: baseline;
    gap: 8px;
    max-width: 300px;
}
#so-booking-widget[data-template="frost"] .so-bw-tz-picker-label {
    flex: 0 0 auto;
    max-width: 42%;
    font-size: 11.5px;
    font-weight: 500;
    color: var(--so-bw-text-tertiary, var(--so-bw-text-secondary));
}
#so-booking-widget[data-template="frost"] .so-bw-tz-combo {
    flex: 1 1 auto;
    min-width: 0;
}
/* The trigger takes the CHIP's tinted edge, not the sheet-field edge F8 gives
   the form inputs: this control sits on the slot panel's own head band, which
   is already near-white, and a white-on-white border is not a border. */
#so-booking-widget[data-template="frost"] .so-bw-tz-combo-trigger {
    min-height: 34px;
    padding-top: 5px;
    padding-bottom: 5px;
    background-color: rgba(255, 255, 255, 0.58);
    border-color: rgba(var(--so-bw-primary-rgb, 99, 102, 241), 0.28);
}
/* The conversion notice belongs to the grid below it, not to the head above:
   it was carrying the head's bottom margin as well as its own. */
#so-booking-widget[data-template="frost"] .so-bw-tz-hint {
    margin: 8px 0 4px;
}

/* ── 28.11 · Two columns is the phone's answer, one is the small phone's ─
   `auto-fill` at a 132px floor gives a 320px panel two tracks and a 280px
   panel one, which is the right shape at both — but only if the floor stops
   being 132px, because a track that cannot fit its own minimum overflows the
   panel rather than dropping a column. Stated as counts at the two container
   widths so it cannot be read any other way.

   was @media (max-width: 480 / 380) — gated on the CONTAINER width via data-cw
   (see booking-widget.js _wireContainerWidth). The ID stays OUTSIDE :where()
   so the selector still scores the specificity the media query it replaces
   had. */

    #so-booking-widget:where([data-cw~="480"])[data-template="frost"] .so-bw-slot-list{
        grid-template-columns: repeat(2, minmax(0, 1fr));
        gap: 8px;
    }
    #so-booking-widget:where([data-cw~="380"])[data-template="frost"] .so-bw-slot-list{
        grid-template-columns: 1fr;
    }
    /* A row that has to wrap its range needs the height to wrap INTO, and the
       tap target is the floor either way. */
    #so-booking-widget:where([data-cw~="480"])[data-template="frost"] .so-bw-slot-row{
        min-height: 48px;
    }
    /* The clock control goes back to stacked below the heading: at this width
       a row leaves the value ~120px, which truncates every zone longer than
       "Europe/London". */
    #so-booking-widget:where([data-cw~="480"])[data-template="frost"] .so-bw-tz-picker{
        flex-direction: column;
        align-items: flex-start;
        gap: 3px;
        max-width: 100%;
    }
    /* The sticky band's sideways bleed is the STEP's gutter, and the step
       narrows with the container — a -24px bleed here would paint 8px past the
       sheet's own edge. */
    #so-booking-widget:where([data-cw~="480"])[data-template="frost"] .so-bw-search::before{
        inset: -18px -16px -12px;
    }


/* ==========================================================================
   29 · FROST — THE SERVICE CARD, CLOSER
   --------------------------------------------------------------------------
   Section 28 made the card's text readable. This one makes the card itself
   read: a shallower header band, one unmistakable mark for "chosen", and a
   clear order of importance between the name, the description and the two
   numbers.

   NOT A REDESIGN. Same tile, same glass, same corner, same accent, same
   fallback ladder, same two-up grid, same behaviour — the whole card is still
   the button, and there is deliberately no Select control added: a button
   inside a button is two targets for one decision.
   ========================================================================== */

/* ── 29.1 · The header band is a header, not a hero ──────────────────────
   The band runs at `aspect-ratio: 2 / 1` capped at 176px, which on Frost's
   293px card is 147px — half the card's height spent on a photograph, or on a
   circle holding two letters, before the customer reads a word.

   2.4/1 takes it to 122px, ~17% off, and the cap comes down with it so the
   ratio still governs at every width the two-up grid produces.

   THE RATIO IS SET ON `.so-bw-card-media` AND NOWHERE ELSE, which is what
   keeps a photographed service and an initials-only one identical: the
   placeholder is the same element with a tint and a centred avatar, so both
   get the same box from the same declaration. Changing one and not the other
   is exactly the fault the band was introduced to fix (see the markup note in
   booking-widget.js) — so it is changed in one place on purpose. */
#so-booking-widget[data-template="frost"] .so-bw-card-media {
    aspect-ratio: 2.4 / 1;
    max-height: 144px;
}

/* ── 29.2 · Chosen, marked once, in the corner ───────────────────────────
   The tick was a 28px tile in the TEXT row, holding a 28px column open on
   every card whether or not anything was in it and sitting against the
   description on the cards where something was. Moved to the band's top-right
   it is out of the text's way entirely — the description gets the width back
   — and it lands where a selection mark is looked for.

   The white ring is not decoration: the badge sits on a photograph on the
   cards that have one, where a flat accent circle can land on a same-value
   region of the image and disappear. The ring is what guarantees an edge
   whatever is underneath.

   The fill and the border of the SELECTED CARD are untouched — the 15% accent
   wash and accent edge from the SELECTION FEEDBACK block, which is what the
   template has always shown. The badge is a second channel on top of it, not
   a replacement for it. */
#so-booking-widget[data-template="frost"] .so-bw-card--service .so-bw-card-check {
    position: absolute;
    top: 10px;
    right: 10px;
    z-index: 3;
    width: 24px;
    height: 24px;
    margin: 0;
    box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.92),
                0 2px 6px -1px rgba(20, 40, 80, 0.45);
}
#so-booking-widget[data-template="frost"] .so-bw-card--service .so-bw-card-check > svg {
    width: 14px;
    height: 14px;
}
/* Multi-pick renders the running order ("1, 2, 3…") in place of the tick, and
   it has 24px to say it in now rather than 28. */
#so-booking-widget[data-template="frost"] .so-bw-card--service .so-bw-card-check-order {
    font-size: 12px;
    font-weight: 700;
    line-height: 1;
}

/* ── 29.3 · Three levels, not one ────────────────────────────────────────
   Name, description and numbers were within a point and a weight of each
   other, so the card read as a paragraph. The name takes 15.5px/700 and the
   tighter tracking that goes with a heavier weight; the description drops to
   the secondary ink at a reading line-height; the badges stay quiet and equal.

   `align-items: stretch` on the row is restored deliberately — 28.3 set it to
   flex-start to get the tick off the description, which stopped the body
   filling the levelled card and so unpinned the badges from their shared
   bottom line (13717/13725). With the tick out of the row, stretch comes back
   and the numbers align across the grid again. */
#so-booking-widget[data-template="frost"] .so-bw-card--service .so-bw-card-top {
    align-items: stretch;
    padding-bottom: 16px;
}
#so-booking-widget[data-template="frost"] .so-bw-card--service .so-bw-card-title {
    font-size: 15.5px;
    font-weight: 700;
    line-height: 1.32;
    letter-spacing: -0.01em;
    color: var(--so-bw-text);
    margin-bottom: 5px;
}
#so-booking-widget[data-template="frost"] .so-bw-card--service .so-bw-card-meta--clamp {
    font-size: 13px;
    line-height: 1.5;
    color: var(--so-bw-text-secondary);
}
/* One shape for both numbers. They were reading as two different things —
   duration on the neutral chip, price on the accent one — when they are the
   same kind of fact about the same service, compared across the row. */
#so-booking-widget[data-template="frost"] .so-bw-card--service .so-bw-badge {
    padding: 3px 9px;
    border-radius: 999px;
    font-size: 12px;
    font-weight: 600;
    line-height: 1.4;
    background: rgba(255, 255, 255, 0.72);
    border: 1px solid rgba(var(--so-bw-primary-rgb, 99, 102, 241), 0.2);
    color: var(--so-bw-text-secondary);
}
/* Price keeps the accent — it is the one number a customer looks FOR.
   Through --so-bw-primary-ink, not --so-bw-primary: this is 12px text on a
   0.72-alpha white chip, and the raw brand hue is a FILL colour that carries no
   contrast guarantee as ink. A pale accent (a yellow, a mid teal) renders the
   price unreadable at exactly the size the customer is hunting for it. The ink
   token clamps lightness for the background it sits on and leaves the border,
   which is decoration, on the undarkened hue. */
#so-booking-widget[data-template="frost"] .so-bw-card--service .so-bw-badge--price,
#so-booking-widget[data-template="frost"] .so-bw-card--service .so-bw-badge--free {
    color: var(--so-bw-primary-ink);
    border-color: rgba(var(--so-bw-primary-rgb, 99, 102, 241), 0.38);
}

/* ── 29.4 · The whole tile is the target, and says so ────────────────────
   The card has been `role="button"` with the click handler on the root all
   along; what it lacked was the two things that tell a pointer so. The base
   card carries `cursor: pointer`, but the header band and the text row are
   separate boxes inside it and the band never inherited one — the half of the
   card most likely to be aimed at showed an arrow.

   Hover is one step of the same material the selected state uses, so the
   feedback and the commitment read as the same scale: the glass thickens
   slightly, the edge picks up the accent, the tile lifts 1px. Nothing moves
   that would reflow a neighbour. */
#so-booking-widget[data-template="frost"] .so-bw-card--service,
#so-booking-widget[data-template="frost"] .so-bw-card--service .so-bw-card-media,
#so-booking-widget[data-template="frost"] .so-bw-card--service .so-bw-card-top {
    cursor: pointer;
}
#so-booking-widget[data-template="frost"] .so-bw-card--service {
    position: relative;
    transition: background var(--so-bw-transition),
                border-color var(--so-bw-transition),
                box-shadow var(--so-bw-transition),
                transform var(--so-bw-transition);
}
#so-booking-widget[data-template="frost"] .so-bw-card--service:hover:not(.so-bw-card--selected) {
    background: rgba(255, 255, 255, 0.78);
    border-color: rgba(var(--so-bw-primary-rgb, 99, 102, 241), 0.45);
    box-shadow: 0 10px 26px -14px rgba(20, 40, 80, 0.55);
    transform: translateY(-1px);
}
/* Keyboard reaches the card by the same route, so it gets the same answer
   plus the ring the pointer does not need. */
#so-booking-widget[data-template="frost"] .so-bw-card--service:focus-visible {
    outline: 2px solid var(--so-bw-primary);
    outline-offset: 2px;
}
/* A hover that MOVES the tile is wrong for anyone who asked motion to stop;
   the colour and the edge carry it on their own. */
@media (prefers-reduced-motion: reduce) {
    #so-booking-widget[data-template="frost"] .so-bw-card--service:hover:not(.so-bw-card--selected) {
        transform: none;
    }
}
/* Touch has no hover to give, and a sticky :hover left the last-tapped card
   looking half-selected next to the one actually chosen. */
@media (hover: none) {
    #so-booking-widget[data-template="frost"] .so-bw-card--service:hover:not(.so-bw-card--selected) {
        background: rgba(255, 255, 255, 0.66);
        border-color: rgba(255, 255, 255, 0.78);
        box-shadow: 0 6px 24px -14px rgba(20, 40, 80, 0.4);
        transform: none;
    }
}


/* ==========================================================================
   30 · FROST — THE GRID EARNS THE WIDTH IT IS GIVEN
   --------------------------------------------------------------------------
   REPORTED: still one row of services, at Appearance → Size = FULL.

   Section 29 sized the card against Frost's NATURAL width — 720px, two-up,
   293px a card. `[data-size="full"]` (23.2) drops the container's max-width to
   100%, and the card grid is hard-coded `repeat(2, minmax(0, 1fr))` (C17), so
   at 1890px the same two cards were 870px WIDE each. A one-line description in
   an 870px box, a 144px band above it, and a row 355px tall — of which a
   ~500px scroll region shows one.

   No amount of extra shell height fixes that one, and this is the honest part:
   at that viewport the container is already `100dvh - 64px` and the screen is
   what is left. Two rows of 355px cards need a 710px scroll region, i.e. a
   ~1050px-tall browser window. The height is not the thing that is wrong — the
   COLUMN COUNT is. Six services in the first row beats two, and it is the same
   fix either way you state the goal.

   Two-up is preserved exactly where the template was designed for it, and the
   FLOOR IS WHAT DECIDES THAT — measured, not estimated. At Frost's natural
   720px the grid's content box is 604px (the container's 720 less the body's
   44 and the step's 48, less the 12px scrollbar when the list is long enough
   to have one), and the gutter is 14px. Two tracks therefore need a floor at
   or below 295px; the first draft of this rule used 300 and rendered ONE
   column at the default size — the fault it was written to fix, in the
   opposite direction. 280px carries ~15px of slack, which is what absorbs the
   scrollbar appearing and disappearing as a catalogue grows.

   That floor holds two-up from ~574px of content to ~867px, so `large` (the
   1.25 scale, ~900px container) stays two-up as well. Columns are gained only
   above that — i.e. only when an admin has explicitly asked for the width,
   which is what asking for Full means.
   ========================================================================== */

#so-booking-widget[data-template="frost"] .so-bw-card-grid {
    grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
}

/* The single column below 640px is unchanged and is restated here only because
   this section is later in the file than the rule that set it (21044). A 300px
   floor would have produced one track at that width anyway; stating it keeps
   the breakpoint where the file put it rather than leaving it to arithmetic. */

    #so-booking-widget:where([data-cw~="640"])[data-template="frost"] .so-bw-card-grid{
        grid-template-columns: 1fr;
    }

/* Nothing else about the card changes: same band ratio, same badge, same
   hierarchy, same hover, same behaviour. The width ceiling lives on the TRACK
   rather than on the card, so a three-service catalogue still fills the row it
   is given instead of leaving orphan cards against a 1200px gap. */


/* ==========================================================================
   31 · FROST — THE SEARCH AREA, TIGHTENED
   --------------------------------------------------------------------------
   REPORTED: too much white above and below the search field.

   The field is 37px and the band around it was 77px. Most of that is 28.2's
   own doing: the overlay bleeds `-18px` UPWARD, which was sized to cover the
   body's 18px of padding while the field sits at its natural position — and
   that is exactly the state in which the extra 18px is just white paint
   climbing into the gap under the helper text.

   The upward bleed can go almost to nothing without reopening the leak it was
   written for. When the field is PINNED, `top: 0` puts its top edge at the
   scroll port's own top edge, so there is no visible region above it for a
   card to appear in — anything the bleed covers up there is already clipped.
   The bleed only has to be non-zero so the field's own border does not sit on
   the band's edge. What still has to be paid in full is the bleed BELOW: that
   is the transparent margin the catalogue was scrolling into, and it stays at
   the full 14px.

   Sticky behaviour, the blur, the fallbacks, the phone bleed and the field's
   own translucency are all unchanged — this is the box, not the material.
   ========================================================================== */

/* ── 31.1 · A 46px field, and nothing spare around it ────────────────────
   `min-height` rather than a taller `padding`: a single-line text input
   centres its own value in the content box, so the placeholder and the typed
   text stay optically centred at any height the box ends up — which a padding
   pair has to be recalculated to keep. 46px sits mid-range of the 44–48px
   asked for and is a comfortable tap target in its own right.

   Horizontal padding goes symmetric at 38px. It was 38 left / 34 right, which
   is invisible on a placeholder and visible the moment a typed query runs to
   the end of the field. */
/* `padding: 0` is load-bearing, not tidying. The bounded-shell release gives
   the sticky field `padding-bottom: 4px` (~7826) to hold the list off its
   bottom edge, which made the WRAPPER 50px around a 46px control and put an
   unpaintable 4px strip below it. The 10px margin below owns that separation
   here, so the wrapper goes back to being exactly the height of the field it
   holds — which is what lets the control read as a control. */
#so-booking-widget[data-template="frost"] .so-bw-search {
    min-height: 46px;
    padding: 0;
    margin: 0 0 10px;
}
#so-booking-widget[data-template="frost"] .so-bw-search-input {
    min-height: 46px;
    padding: 8px 38px;
    font-size: 14px;
    line-height: 1.4;
}
/* The icon and the clear button are absolutely positioned against the wrapper
   with only a horizontal offset, so their vertical placement fell out of their
   static position — 1.5px above centre, and further off as soon as the field's
   height changed. Pinned to the centre instead, so the row stays centred at
   every size this field is rendered at. */
#so-booking-widget[data-template="frost"] .so-bw-search-icon {
    top: 50%;
    left: 14px;
    transform: translateY(-50%);
}
#so-booking-widget[data-template="frost"] .so-bw-search-clear {
    top: 50%;
    right: 10px;
    transform: translateY(-50%);
}

/* ── 31.2 · Where the field pins, so the band can stop being tall ────────
   MEASURED: pinned with `top: 0`, the field's top edge sat 19px below the top
   of the visible sheet — the scroll container's padding-top. A sticky element
   here resolves its offset against the scroller's CONTENT box, so `0` means
   "below the padding", and the catalogue scrolls up through the strip above it
   in full view. That strip is the only reason 28.2's overlay bled 18px UPWARD:
   not to space the field, but to reach the edge of the sheet from 19px down.

   Paying for it with paint meant paying at REST too, where those 18px are just
   white climbing into the gap under the helper text — the reported fault.

   So the pin moves instead of the paint. The scroll region's top padding is
   stated at a round 14px and the field pins at exactly `-14px`, which lands
   its top edge ON the top edge of the visible sheet: there is no strip above
   it for a card to appear in, so no paint has to cover one. (This block first
   used `-10px` and left 5px, which section 32 — where the band is removed
   altogether and nothing is covering anything — showed as two blue avatar caps
   poking above the field. Closed to zero.)

   The two figures are a PAIR, and equal ones. If the body's padding-top
   changes, this offset has to change with it, which is why both are stated
   here rather than left to the shared clamp they came from. */
#so-booking-widget[data-template="frost"] .so-bw-body {
    padding-top: 14px;
}
#so-booking-widget[data-template="frost"][data-template-id] .so-bw-search {
    top: -14px;
}

/* ── 31.3 · The band hugs the field ─────────────────────────────────────
   SUPERSEDED BY SECTION 32, which sets `content: none` on this pseudo-element
   — the band is gone and these insets paint nothing. Left as the record of
   what the geometry was, and correct if it is ever put back.

   6px above — an edge, not a margin. 14px below, and that figure is measured
   rather than chosen: it is the field's own 10px margin plus the 4px of
   padding-top that `.so-bw-scroll-wrap` puts above the first card. Both are
   transparent, both are what the catalogue scrolls into, so the paint has to
   reach the card's top edge exactly. 77px of band becomes 66px, and at rest
   the white stops 6px above the field instead of 18px. */
#so-booking-widget[data-template="frost"] .so-bw-search::before {
    inset: -6px -24px -14px;
}

    /* The phone keeps the same vertical figures — only the sideways bleed
       tracks the narrower step gutter (see 28.11). */
    #so-booking-widget:where([data-cw~="480"])[data-template="frost"] .so-bw-search::before{
        inset: -6px -16px -14px;
    }

/* ── 31.4 · Three gaps, all in the same range ────────────────────────────
   Helper text → field → cards, at 14 / 14. The helper text was carrying a
   20px bottom margin from the shared step rule, which is a paragraph's spacing
   below a paragraph, not a label's above a control. */
#so-booking-widget[data-template="frost"] .so-bw-step-desc {
    margin-bottom: 14px;
}


/* ==========================================================================
   32 · FROST — THE SEARCH FIELD LOSES ITS PANEL
   --------------------------------------------------------------------------
   The sticky field has been sitting on a painted band since 28.2 — a
   full-bleed overlay with its own fill and a hairline under it. Section 31
   shrank the band; this removes it. The field sits on the section's own glass
   and the only white left is the control itself: rounded, bordered, lifted.

   WHAT THE BAND WAS FOR, AND WHAT REPLACES IT. It did two jobs. Above the
   field it covered the strip between the pin and the top of the sheet — and
   31.2 already retired that job by moving the pin instead, so there are 5px
   there rather than 19px. Below it, it painted the 14px the catalogue scrolls
   into. That one is not replaced, it is ACCEPTED: a bordered control with a
   shadow, with content passing in the gap under it, is a floating field — the
   thing the band was imitating with paint. What it must not look like is a
   card touching the field's edge with nothing between them, which was the
   original report, and 14px of the section's own glass is that separation.

   The field's own translucency is what makes this work at the corners. The
   fill goes to 0.96 — white for every purpose except the two rounded corners,
   where the 8px blur keeps a passing card from reading as a hard edge through
   the curve.

   Sticky, the pin offset, the 46px height, the padding, the margins and the
   phone behaviour are all untouched. This section sets a fill, a border, a
   corner and a shadow, and turns one pseudo-element off.
   ========================================================================== */

/* `content: none` rather than a transparent background: it stops the element
   being generated at all, which also retires the @supports floor and the
   forced-colors variant that 28.2 gave it — both of those set only a
   `background`, and there is now no box for them to paint. */
#so-booking-widget[data-template="frost"] .so-bw-search::before {
    content: none;
}

/* The one white thing in the search area. `calc()` off the radius rung rather
   than a literal, so the Studio corner slider still moves it — the same
   derivation section 23.3 uses for every other corner in the template. */
#so-booking-widget[data-template="frost"] .so-bw-search-input {
    background: rgba(255, 255, 255, 0.96);
    -webkit-backdrop-filter: blur(8px);
    backdrop-filter: blur(8px);
    border: 1px solid rgba(var(--so-bw-primary-rgb, 99, 102, 241), 0.18);
    border-radius: calc(var(--so-bw-radius, 8px) * 1.25);
    box-shadow: 0 4px 12px -8px rgba(20, 40, 80, 0.4);
}
/* Focus was taking 28.2's `0.86` fill, which on a field that is now white
   reads as the control going DIMMER when you click into it. It brightens to
   the top of its range instead, and the accent moves to the edge and the ring. */
#so-booking-widget[data-template="frost"] .so-bw-search-input:focus {
    background: rgba(255, 255, 255, 0.99);
    border-color: var(--so-bw-primary);
    box-shadow: 0 0 0 3px rgba(var(--so-bw-primary-rgb, 99, 102, 241), 0.16),
                0 4px 12px -8px rgba(20, 40, 80, 0.4);
}
/* Without a band to sit on, the placeholder and the magnifier are reading
   against white rather than against a tinted composite — the tertiary ink is
   pitched for the latter and goes faint on the former. */
#so-booking-widget[data-template="frost"] .so-bw-search-input::placeholder,
#so-booking-widget[data-template="frost"] .so-bw-search-icon {
    color: var(--so-bw-text-secondary);
    opacity: 0.85;
}
/* And the magnifier has to be told to stay in front, which it did not have to
   be before. The icon and the clear button are absolutely positioned with
   `z-index: auto`, so they used to paint above their in-flow sibling for free.
   Giving the input a `backdrop-filter` makes it a STACKING CONTEXT — painted
   with the positioned descendants at z-index 0, and later in DOM order than
   both — so it covered them. It cost the magnifier entirely on the first pass
   of this section. A stacking order stated explicitly is cheaper than
   remembering that a filter reorders painting. */
#so-booking-widget[data-template="frost"] .so-bw-search-icon,
#so-booking-widget[data-template="frost"] .so-bw-search-clear {
    z-index: 2;
}

/* ==========================================================================
   WIDE CANVAS · THE TWO-COLUMN BOOKING SURFACE                    (C9-bis)
   ==========================================================================
   Wide Canvas is the desktop-first architecture: it has always put the month
   beside the times instead of swapping one for the other. This section extends
   that idea to the rest of the flow — the step's own work on the left, a
   running summary of the booking on the right — so the customer never loses
   what they have chosen while choosing the next thing.

   Applies to `[data-template="wide"]` ONLY. Every element it styles is emitted
   for all twenty-two architectures and switched off for the other twenty-one in
   section 12b; see the ordering note there for why that survives `x-show`.

   WHY THIS SECTION IS AT THE END OF THE FILE. Several rules below deliberately
   reverse earlier Wide Canvas rules — the single-column comparison table in C9
   is right for the service step and wrong for the staff gallery. Each override
   is written at a HIGHER specificity than the rule it replaces (a step-scoped
   `.so-bw-step--staff` adds a class, taking (1,2,0) to (1,3,0)), so none of
   them depends on source order. Being last is belt-and-braces, not the
   mechanism. Where a rule genuinely ties, it is called out.

   NO REASSURANCE COPY WITHOUT DATA. The panel's rescheduling line is driven by
   `scheduleos_portal_cancellation_window` — the option the customer portal
   actually enforces — and drops out entirely when the site has not set one.
   Nothing here asserts a security posture or a popularity claim, for the same
   reason section K's "what to expect" column was deleted rather than faked.
   ========================================================================== */

/* ── S1 · The shell ──────────────────────────────────────────────────────
   `.so-bw-body` is the widget's scroll region (a block box with overflow-y:
   auto, not a flex container), so turning it into a flex ROW is what puts the
   step beside the summary. Its only children are the recap, the login gate,
   the step-flow wrapper and the summary panel.

   FLEX, NOT GRID, and that is the load-bearing choice. The panel is absent on
   the date/time step (`summaryPanelSteps`) and on the review step, where the
   full width belongs to the calendar and to the money column respectively.
   With two explicit grid tracks the second one keeps its width AND its gap when
   its only item is `display: none`, leaving ~340px of dead canvas on the
   busiest screen in the flow. A flex container adds no gap for an item that is
   not rendered, so those steps get the whole width back with no per-step rule
   to remember. */
#so-booking-widget[data-template="wide"] .so-bw-body {
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
    align-items: flex-start;
    gap: 20px;
}
/* A full-bleed row for `.so-bw-recap` stood here, so a whole-widget takeover
   would span both columns. The recap markup is gone with Accordion and
   Conversation, and the login gate — the takeover this was really for — is
   inline-styled rather than carrying that class, so the rule matched nothing
   either way. */
/* `min-width: 0` is not optional: a flex item's automatic minimum size is its
   content, so a long service name or an unbroken URL in a bio would push the
   column past its share and shove the summary off the right edge instead of
   wrapping. Same trap the C7/C9 notes record for `.so-bw-card-body`. */
#so-booking-widget[data-template="wide"] .so-bw-step-host {
    flex: 1 1 0;
    min-width: 0;
}
#so-booking-widget[data-template="wide"] .so-bw-sumpanel {
    display: flex;
    flex-direction: column;
    gap: 14px;
    flex: 0 0 320px;
    box-sizing: border-box;
    padding: 18px;
    border: 1px solid var(--so-bw-border, var(--so-bw-ds-n200));
    border-radius: var(--so-bw-ds-radius-card);
    background: var(--so-bw-ds-n0);
    /* Sticks beside a scrolling list so the totals stay readable while the
       customer works down a long catalogue.

       `max-height` plus its own `overflow-y` are the guard against a failure
       this repo has already been bitten by: a sticky box TALLER than its
       scrollport pins at `top` and never releases, stranding whatever sits
       below it. Bounding the panel to the port means it can always release, and
       a panel long enough to need it scrolls itself.

       No scrollbar styling here on purpose — `.so-bw-body` already declares
       `scrollbar-width`/`scrollbar-color`, and setting those on a descendant
       silently kills every `::-webkit-scrollbar` rule in Chromium. */
    position: sticky;
    top: 0;
    max-height: 100%;
    overflow-y: auto;
    overscroll-behavior: contain;
}

/* Below this container width the two columns stop being two columns: at 900px a
   320px panel leaves the step under 560px, which is narrower than the service
   table needs. Gated on `data-cw` (the container width the widget measures for
   itself in _wireContainerWidth) rather than a viewport media query, because
   this widget is embedded and the viewport says nothing about the box it got. */
    #so-booking-widget:where([data-cw~="900"])[data-template="wide"] .so-bw-sumpanel {
        flex: 0 0 100%;
        position: static;
        max-height: none;
        overflow-y: visible;
        /* Stacked, the summary is a footer to the step rather than a column
           beside it, so it reads after the thing it summarises. */
        order: 2;
    }
    #so-booking-widget:where([data-cw~="900"])[data-template="wide"] .so-bw-step-host {
        flex: 0 0 100%;
    }

/* ── S2 · The titled section header ──────────────────────────────────────
   An icon tile, a heading, and the step's existing description. The wrapper is
   `display: contents` everywhere else (section 12b), so this is the only place
   it is a box at all.

   The icon spans both rows so the tile is optically centred against the pair of
   lines rather than against the heading alone. */
#so-booking-widget[data-template="wide"] .so-bw-svchead {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    column-gap: 14px;
    row-gap: 3px;
    align-items: center;
    margin: 0 0 18px;
}
#so-booking-widget[data-template="wide"] .so-bw-svchead__icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    grid-column: 1;
    grid-row: 1 / span 2;
    width: 44px;
    height: 44px;
    border-radius: var(--so-bw-ds-radius-card);
    /* The accent at low opacity, so the tile takes the template's palette (and
       any Studio theme) without a second colour needing to be declared. */
    background: color-mix(in srgb, var(--so-bw-primary) 10%, transparent);
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}
#so-booking-widget[data-template="wide"] .so-bw-svchead__title {
    display: block;
    grid-column: 2;
    margin: 0;
    font-size: 18px;
    font-weight: 700;
    line-height: 1.25;
    letter-spacing: -0.01em;
    color: var(--so-bw-ds-n900, var(--so-bw-text));
}
/* The description keeps its own class and its own copy — it is the same
   paragraph the other twenty-one templates render — and only its placement
   changes. `margin: 0` because the grid owns the spacing now. */
#so-booking-widget[data-template="wide"] .so-bw-svchead .so-bw-step-desc {
    grid-column: 2;
    margin: 0;
    font-size: 13.5px;
    line-height: 1.5;
    color: var(--so-bw-ds-n500, var(--so-bw-text-secondary));
}

/* ── S3 · Category chips ─────────────────────────────────────────────────
   Filter toggles over one list, not a tablist — see the markup note. Wide
   Canvas is the only architecture that shows them; everywhere else the
   categories stay as headings above their own groups. */
#so-booking-widget[data-template="wide"] .so-bw-catbar {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin: 0 0 14px;
}
#so-booking-widget[data-template="wide"] .so-bw-catchip {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    /* 32px is below the 44px tap floor this file applies to primary controls,
       and deliberately: a chip bar at 44px per row dominates the step it
       filters. They sit in a horizontal row with 8px between them, so the
       pointer target is wide where it is not tall, and every chip's job is also
       reachable by simply scrolling the list it filters. */
    min-height: 32px;
    padding: 0 11px;
    border: 1px solid var(--so-bw-border, var(--so-bw-ds-n200));
    border-radius: var(--so-bw-ds-radius-chip);
    background: var(--so-bw-ds-n0);
    color: var(--so-bw-ds-n700, var(--so-bw-text-secondary));
    font-family: inherit;
    font-size: 12.5px;
    font-weight: 500;
    line-height: 1;
    cursor: pointer;
    transition: background .15s, border-color .15s, color .15s;
}
#so-booking-widget[data-template="wide"] .so-bw-catchip__icon {
    flex: 0 0 auto;
    color: var(--so-bw-ds-n500);
}
#so-booking-widget[data-template="wide"] .so-bw-catchip:hover {
    background: var(--so-bw-ds-n25);
    border-color: var(--so-bw-ds-n300);
}
/* The ON chip is carried by fill AND weight, not by hue alone — the greyscale
   test this repo applies to its galleries applies to a filter bar too. */
#so-booking-widget[data-template="wide"] .so-bw-catchip--on {
    background: color-mix(in srgb, var(--so-bw-primary) 10%, transparent);
    border-color: color-mix(in srgb, var(--so-bw-primary) 45%, transparent);
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
    font-weight: 600;
}
#so-booking-widget[data-template="wide"] .so-bw-catchip--on .so-bw-catchip__icon {
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}
#so-booking-widget[data-template="wide"] .so-bw-catchip:focus-visible {
    outline: 2px solid var(--so-bw-primary);
    outline-offset: 2px;
}

/* ── S4 · The service cards ──────────────────────────────────────────────
   THIS SECTION REPLACED A COMPARISON TABLE, and the reversal is the point.

   Section C9 (~16124) frames the service list as a bordered single-column
   table: hairline-separated rows, no radius, no shadow, duration and price held
   to the right like columns. That reads as data. A booking step is not a
   spreadsheet — the customer is choosing a thing, and every product in this
   class (Calendly, Fresha, Square) presents that choice as a card with its own
   edge, because an edge is what makes something look pickable.

   So the rows become CARDS: each one bordered, rounded, with its own hover, and
   a named commit button instead of a glyph. C9's table rules are reversed below
   rather than deleted at source, because C9 also serves the location step,
   which is still legitimately a list.

   ONE selection control. C9 put a checkbox at the left edge AND a +/- button at
   the right, which is two affordances for one decision — the customer has to
   work out which one commits. The checkbox is switched off here and
   `.so-bw-card-pick` (the same button the staff cards use) carries the whole
   job, saying "Select" and then "Selected". */

/* Not a table frame: a stack of cards with air between them. */
#so-booking-widget[data-template="wide"] .so-bw-step--service .so-bw-card-grid {
    grid-template-columns: 1fr;
    gap: 10px;
    border: 0;
    border-radius: 0;
    overflow: visible;
    background: none;
}
/* The card itself. `!important` on the four box properties because the base
   `.so-bw-card--service` rule pins flex-direction/align-items/padding/gap with
   `!important` at higher specificity to protect the stacked mobile card — C9
   documents the same restatement. `row` is the one that matters: without it the
   avatar sits above the text and there is no card, just a column. */
#so-booking-widget[data-template="wide"] .so-bw-step--service .so-bw-card--service:not(.so-bw-sp__panel) {
    flex-direction: row !important;
    align-items: center !important;
    padding: 14px var(--so-bw-wide-leadpad) !important;
    gap: var(--so-bw-wide-leadgap) !important;
}
#so-booking-widget[data-template="wide"] .so-bw-step--service .so-bw-card:not(.so-bw-sp__panel) {
    display: flex;
    flex-direction: row;
    align-items: center;
    /* Lead gap and inset from the container's tokens — the section heading reads
       the same two, which is what keeps one text rail down the column. */
    gap: var(--so-bw-wide-leadgap);
    padding: 14px var(--so-bw-wide-leadpad);
    /* Warm neutral rather than the blue-grey hairline: at this density the
       border is a large share of what the eye reads as the card, and a cool
       line on white makes the surface look cold. */
    border: 1px solid #ECEAE6;
    border-bottom: 1px solid #ECEAE6;
    border-radius: var(--so-bw-r-13);
    background: var(--so-bw-ds-n0);
    box-shadow: 0 1px 2px rgba(16, 18, 26, .04);
    min-height: 0;
    transition: border-color .15s, box-shadow .15s, background .15s;
}
#so-booking-widget[data-template="wide"] .so-bw-step--service .so-bw-card:not(.so-bw-sp__panel):hover {
    border-color: #DCD8D2;
    box-shadow: 0 2px 8px rgba(16, 18, 26, .07);
}
/* Selected: a subtle brand wash and a brand border, so the state is carried by
   the whole card rather than by one glyph in a corner. C9 marked it with an
   inset bar on the left edge, which is a table-row idiom — there are no rows
   here to run a bar down. */
#so-booking-widget[data-template="wide"] .so-bw-step--service .so-bw-card--selected {
    border-color: color-mix(in srgb, var(--so-bw-primary) 55%, transparent);
    background: color-mix(in srgb, var(--so-bw-primary) 5%, var(--so-bw-ds-n0));
    box-shadow: 0 1px 2px rgba(16, 18, 26, .04);
}

/* `display: contents` flattens the wrapper so the body and the badges become
   items of the card's own flex row. Without it the price and duration are
   trapped one level down and cannot be pinned beside the button. */
#so-booking-widget[data-template="wide"] .so-bw-step--service .so-bw-card-top {
    display: contents;
}
/* Restated because the flattening changes which element is the flex child:
   C9's rule targets `.so-bw-card > .so-bw-card-body`, and with the wrapper
   flattened the body is still a DOM child of it, so that child selector stops
   matching. Miss this and the body's base `flex: 1` (a ZERO basis) applies and
   a long service name blows out of the card. */
/* THE CARD'S INTERIOR IS A GRID, and this is where the first attempt was
   wrong. It assumed the badges and the commit button were siblings of the body
   and ordered them with `order`, which did nothing: EVERY piece of the card
   except the avatar lives INSIDE `.so-bw-card-body` —

     .so-bw-card-body
       .so-bw-card-title
       .so-bw-card-meta      (the description, clamped)
       .so-bw-card-more      (Read more)
       .so-bw-card-badges    (duration + price)
       .so-bw-card-pick      (Select / Selected)

   — so `order` only shuffled them within the body's own column, which is why
   the render showed the name, then a rule, then the price stacked beneath it,
   with the button below that and the avatar floating beside the middle.

   Three columns: the text takes column 1 and stacks inside it, the duration
   and price sit in column 2, the button in column 3. Both right-hand items span
   the text's rows so they stay centred against it however many lines it runs
   to. `minmax(0, 1fr)` on the text column so a long name ellipsises instead of
   pushing the price and the button off the card. */
/* The third column is RESERVED, not content-sized, and that is the point.
   `auto` sized it to the button's label, and the label changes on selection:
   "Select" measures 84px, "✓ Selected" measures 106px (both measured). So
   picking a service widened that one row's button by 22px, dragged its left edge
   from 1118.9 to 1096.8 and shrank the title column with it — the chosen row
   stopped lining up with every other row in the list, which reads as the whole
   card resizing itself.
   A fixed track fixes it at the layout level rather than by tuning the button:
   every row reserves the same width whatever its state, so nothing reflows and
   the buttons form a straight edge down the list. */
#so-booking-widget[data-template="wide"] .so-bw-step--service .so-bw-card-body {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto var(--so-bw-wide-pickw, 108px);
    align-items: center;
    column-gap: 14px;
    row-gap: 2px;
    flex: 1 1 auto;
    min-width: 0;
}
#so-booking-widget[data-template="wide"] .so-bw-step--service .so-bw-card-title,
#so-booking-widget[data-template="wide"] .so-bw-step--service .so-bw-card-meta,
#so-booking-widget[data-template="wide"] .so-bw-step--service .so-bw-card-more {
    grid-column: 1;
}
/* Pinned to row 1 and spanning, rather than `1 / -1`: these rows are implicit
   (the body declares columns only), so -1 counts back from the end of the
   EXPLICIT grid and resolves to row 1 anyway. Saying `span 2` means it. */
#so-booking-widget[data-template="wide"] .so-bw-step--service .so-bw-card-badges {
    grid-column: 2;
    grid-row: 1 / span 2;
    align-self: center;
    justify-self: end;
}
#so-booking-widget[data-template="wide"] .so-bw-step--service .so-bw-card-pick {
    grid-column: 3;
    grid-row: 1 / span 2;
    align-self: center;
}
/* The ghost that reserves space for a "Read more" the card does not need. It
   exists so cards SIDE BY SIDE in a multi-column grid stay level when only some
   of them have one — this is a single-column stack, where unequal heights are
   just cards of different lengths. Left in, it cost every short card a 44px
   band of empty space (the tap-target floor on the real button).
   BOTH classes on purpose: written as `--ghost` alone it scores (1,3,0), the
   same as the tap-target rule further up the file, and would lose the tie. */
#so-booking-widget[data-template="wide"] .so-bw-step--service .so-bw-card-more.so-bw-card-more--ghost {
    display: none;
}
/* CIRCULAR, and sized as an avatar rather than as a thumbnail. */
#so-booking-widget[data-template="wide"] .so-bw-step--service .so-bw-card-media,
#so-booking-widget[data-template="wide"] .so-bw-step--service .so-bw-card-icon--avatar {
    order: -1;
    flex: 0 0 auto;
    /* Same token as the heading's tile: if these two sizes drift, so do the two
       text rails they push. */
    width: var(--so-bw-wide-leadsize);
    height: var(--so-bw-wide-leadsize);
    min-height: 0;
    border-radius: 50%;
    overflow: hidden;
    font-size: 13px;
}
#so-booking-widget[data-template="wide"] .so-bw-step--service .so-bw-card-media-img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    border-radius: 50%;
}
/* Service title — 15.5px semibold, per the hierarchy. */
#so-booking-widget[data-template="wide"] .so-bw-step--service .so-bw-card-title,
#so-booking-widget[data-template="wide"] .so-bw-step--service .so-bw-card--service .so-bw-card-title {
    font-size: 15.5px;
    font-weight: 600;
    line-height: 1.3;
    color: var(--so-bw-ds-n900, var(--so-bw-text));
}
/* The short description. One line: a card row is a summary, and "Read more"
   still opens the full text, so nothing is lost. */
#so-booking-widget[data-template="wide"] .so-bw-step--service .so-bw-card-meta {
    display: -webkit-box;
    -webkit-line-clamp: 1;
    -webkit-box-orient: vertical;
    overflow: hidden;
    font-size: 13px;
    line-height: 1.45;
    color: var(--so-bw-ds-n500, var(--so-bw-text-secondary));
}
/* Duration and price, pinned right of the text and left of the button. They
   were rendering as two filled pills in IBM Plex Mono, which on a card is two
   more boxes competing with the card's own edge. Plain text: the duration
   quiet, the price the heaviest thing after the name. */
#so-booking-widget[data-template="wide"] .so-bw-step--service .so-bw-card-badges {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 1px;
    margin: 0;
    white-space: nowrap;
}
#so-booking-widget[data-template="wide"] .so-bw-step--service .so-bw-badge {
    padding: 0;
    border: 0;
    border-radius: 0;
    background: none;
    font-family: inherit;
    font-variant-numeric: tabular-nums;
}
#so-booking-widget[data-template="wide"] .so-bw-step--service .so-bw-badge--duration {
    font-size: 12.5px;
    font-weight: 500;
    color: var(--so-bw-ds-n500, var(--so-bw-text-secondary));
}
#so-booking-widget[data-template="wide"] .so-bw-step--service .so-bw-badge--price {
    font-size: 15px;
    font-weight: 700;
    color: var(--so-bw-ds-n900, var(--so-bw-text));
}
#so-booking-widget[data-template="wide"] .so-bw-step--service .so-bw-badge--free {
    font-size: 13px;
    font-weight: 600;
    color: #067647;
}

/* THE one selection control, last in the row.
   `width: 100%` of the reserved track and `min-width: 0` so the LABEL can never
   drive the box — that is what made the row jump on selection. The track above
   is the single place the size is decided. */
#so-booking-widget[data-template="wide"] .so-bw-step--service .so-bw-card-pick {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    width: 100%;
    min-width: 0;
    min-height: 34px;
    padding: 0 14px;
    border: 1px solid #DCD8D2;
    border-radius: var(--so-bw-r-9);
    background: var(--so-bw-ds-n0);
    color: var(--so-bw-ds-n900, var(--so-bw-text));
    font-family: inherit;
    font-size: 13px;
    font-weight: 600;
    cursor: pointer;
    transition: background .15s, border-color .15s, color .15s;
}
#so-booking-widget[data-template="wide"] .so-bw-step--service .so-bw-card-pick:hover {
    border-color: var(--so-bw-primary);
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}
#so-booking-widget[data-template="wide"] .so-bw-step--service .so-bw-card-pick:focus-visible {
    outline: 2px solid var(--so-bw-primary);
    outline-offset: 2px;
}
/* Chosen: solid brand, so the committed card is unmistakable down a column of
   otherwise identical outlines. */
#so-booking-widget[data-template="wide"] .so-bw-step--service .so-bw-card-pick--on {
    background: var(--so-bw-primary);
    border-color: var(--so-bw-primary);
    color: var(--so-bw-on-primary, #fff);
}
#so-booking-widget[data-template="wide"] .so-bw-step--service .so-bw-card-pick--on:hover {
    color: var(--so-bw-on-primary, #fff);
}
/* The checkbox is OFF. The button above says "Selected" in words and carries
   the tick; a checkbox as well is a second control for the same decision, and
   the two states can disagree visually while meaning the same thing. */
#so-booking-widget[data-template="wide"] .so-bw-step--service .so-bw-card-check {
    display: none;
}

/* "Add extras" is demoted to a quiet text affordance under the title, so the
   right-hand end of the card belongs to the one commit button. Add-ons stay
   reachable inline here as well as on their own step. */
#so-booking-widget[data-template="wide"] .so-bw-step--service .so-bw-card-extras-toggle {
    order: 7;
    flex: 0 0 auto;
    min-height: 0;
    padding: 4px 6px;
    border: 0;
    background: transparent;
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
    font-size: 12px;
    font-weight: 600;
}
#so-booking-widget[data-template="wide"] .so-bw-step--service .so-bw-card-extras-toggle:hover {
    text-decoration: underline;
}
/* The inline add-ons panel spans the whole card once open, below everything
   else — it is a second row of content, not another item in the header row. */
#so-booking-widget[data-template="wide"] .so-bw-step--service .so-bw-card-extras {
    order: 10;
    flex: 0 0 100%;
    width: 100%;
}
/* Below this the card cannot hold five things on one line: the badges and the
   button drop under the text rather than squeezing the name to two characters. */
/* Narrow: the card cannot hold text, figures and a button on one line, so the
   interior drops to two columns and the button takes a row of its own under
   them. The avatar stays beside the text — it is what makes the card scannable
   and it costs 42px. */
    #so-booking-widget:where([data-cw~="560"])[data-template="wide"] .so-bw-step--service .so-bw-card-body {
        grid-template-columns: minmax(0, 1fr) auto;
        row-gap: 8px;
    }
    #so-booking-widget:where([data-cw~="560"])[data-template="wide"] .so-bw-step--service .so-bw-card-badges {
        grid-column: 2;
        grid-row: 1 / span 2;
    }
    #so-booking-widget:where([data-cw~="560"])[data-template="wide"] .so-bw-step--service .so-bw-card-pick {
        grid-column: 1 / -1;
        grid-row: auto;
        justify-self: stretch;
    }

/* ── S5 · The staff gallery ──────────────────────────────────────────────
   The staff step is the one place Wide Canvas is NOT a table. A staff member is
   a person with a face, a role and a bio, and a row strips all three down to a
   line of text — so this step gets a three-up card gallery instead.

   Every rule here reverses a C9 table rule, and each is scoped with
   `.so-bw-step--staff` to score (1,3,0) against the (1,2,0) it replaces. That
   is a real specificity win, so none of it relies on this section being last in
   the file. */
#so-booking-widget[data-template="wide"] .so-bw-step--staff .so-bw-card-grid {
    /* auto-fill with a 236px floor: three across the ~700px step at the default
       width, two when the panel forces it narrower, one on a phone — without a
       breakpoint per count. */
    grid-template-columns: repeat(auto-fill, minmax(236px, 1fr));
    gap: 14px;
    /* C9 frames the table with a border and clips it. A gallery has no frame:
       each card carries its own. */
    border: 0;
    border-radius: 0;
    overflow: visible;
}
/* C9 turns every wide card into a table row (row flex, hairline underneath, no
   border, no radius). Put all of it back for this step. */
#so-booking-widget[data-template="wide"] .so-bw-step--staff .so-bw-card:not(.so-bw-sp__panel) {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 10px;
    padding: 16px;
    border: 1px solid var(--so-bw-border, var(--so-bw-ds-n200));
    border-radius: var(--so-bw-ds-radius-card);
    background: var(--so-bw-ds-n0);
    min-height: 0;
    transition: border-color .15s, box-shadow .15s;
}
#so-booking-widget[data-template="wide"] .so-bw-step--staff .so-bw-card:not(.so-bw-sp__panel):hover {
    border-color: var(--so-bw-ds-n300);
    box-shadow: var(--so-bw-ds-shadow-sm);
}
/* C9 marks a selected ROW with an inset accent bar on its left edge, which is a
   row idiom. A card states it with its own border and a tinted fill. */
#so-booking-widget[data-template="wide"] .so-bw-step--staff .so-bw-card--selected {
    border-color: var(--so-bw-primary-ink, var(--so-bw-primary));
    box-shadow: inset 0 0 0 1px var(--so-bw-primary);
    background: color-mix(in srgb, var(--so-bw-primary) 4%, var(--so-bw-ds-n0));
}
/* The avatar leads the card, at portrait size — the whole reason this step is a
   gallery. C9 caps the media tile at 44px for the table. */
#so-booking-widget[data-template="wide"] .so-bw-step--staff .so-bw-card-icon--avatar {
    width: 56px;
    height: 56px;
    border-radius: 50%;
    font-size: 17px;
}
#so-booking-widget[data-template="wide"] .so-bw-step--staff .so-bw-card-body {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 4px;
    width: 100%;
    flex: 1 1 auto;
    min-width: 0;
}
#so-booking-widget[data-template="wide"] .so-bw-step--staff .so-bw-card-title {
    font-size: 15px;
    font-weight: 700;
    color: var(--so-bw-ds-n900, var(--so-bw-text));
}
/* The role, on its own line above the bio. Set in the accent because it is the
   card's classifying fact — what this person does — and the eye should reach it
   before the prose. */
#so-booking-widget[data-template="wide"] .so-bw-step--staff .so-bw-card-role {
    display: block;
    font-size: 12.5px;
    font-weight: 600;
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}
#so-booking-widget[data-template="wide"] .so-bw-step--staff .so-bw-card-meta {
    font-size: 12.5px;
    line-height: 1.5;
    color: var(--so-bw-ds-n500, var(--so-bw-text-secondary));
}
/* "View profile" and the pick button share the card's last line. `margin-top:
   auto` pushes the pair to the bottom so every card in the row ends on the same
   line however long its bio runs — the levelling the grid's equal-height rows
   make possible. */
#so-booking-widget[data-template="wide"] .so-bw-step--staff .so-bw-card-profile {
    order: 8;
    margin-top: auto;
    align-self: flex-start;
    padding: 0;
    border: 0;
    background: transparent;
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
    font-size: 12.5px;
    font-weight: 600;
}
#so-booking-widget[data-template="wide"] .so-bw-step--staff .so-bw-card-pick {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    order: 9;
    align-self: stretch;
    min-height: 36px;
    padding: 0 14px;
    border: 1px solid var(--so-bw-border, var(--so-bw-ds-n200));
    border-radius: var(--so-bw-ds-radius-ctl);
    background: var(--so-bw-ds-n0);
    color: var(--so-bw-ds-n900, var(--so-bw-text));
    font-family: inherit;
    font-size: 13px;
    font-weight: 600;
    cursor: pointer;
    transition: background .15s, border-color .15s, color .15s;
}
#so-booking-widget[data-template="wide"] .so-bw-step--staff .so-bw-card-pick:hover {
    border-color: var(--so-bw-primary-ink, var(--so-bw-primary));
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}
#so-booking-widget[data-template="wide"] .so-bw-step--staff .so-bw-card-pick:focus-visible {
    outline: 2px solid var(--so-bw-primary);
    outline-offset: 2px;
}
/* Chosen: solid accent, so the committed card is unmistakable at a glance
   across a row of otherwise identical outlines. */
#so-booking-widget[data-template="wide"] .so-bw-step--staff .so-bw-card-pick--on {
    background: var(--so-bw-primary);
    border-color: var(--so-bw-primary-ink, var(--so-bw-primary));
    color: var(--so-bw-on-primary, #fff);
}
/* The bare tick in the corner is redundant now that the button says "Selected"
   in words — and the two together read as two different states. */
#so-booking-widget[data-template="wide"] .so-bw-step--staff .so-bw-card-check {
    display: none;
}
/* "Any Available" is not a person, so it is drawn as an option rather than as a
   portrait: a dashed edge says "we choose for you" without a face to show. It
   is the FIRST card in the grid and the only one with no photo, which is why
   the dashed border alone is enough to tell it apart. */
#so-booking-widget[data-template="wide"] .so-bw-step--staff .so-bw-card-grid > .so-bw-card:first-child:not(.so-bw-sp__panel) {
    border-style: dashed;
    border-color: color-mix(in srgb, var(--so-bw-primary) 40%, transparent);
    background: color-mix(in srgb, var(--so-bw-primary) 4%, var(--so-bw-ds-n0));
}
/* …and because it is not a person it has no "View profile" — which is what
   carries the `margin-top: auto` that drops the last line to the card's floor.
   With no profile link on this one card there was nothing to hold that space,
   so its button climbed to sit directly under the description while every
   colleague's stayed on the bottom line. MEASURED at 1440 on a levelled row of
   equal 208.5px cards: Any Available's button started 126.3px down, priyanka's
   and jebin's 155.5px — a 29px step across three cards standing side by side.

   The auto margin moves to the BUTTON for this card only. Not added to the
   button generally: on a card that has both, two `margin-top: auto` in one
   flex column split the free space between them and would open a gap between
   the link and the button instead of closing one. */
#so-booking-widget[data-template="wide"] .so-bw-step--staff .so-bw-card--any .so-bw-card-pick {
    margin-top: auto;
}

/* ── S5a · An equal row may not be a row shorter than its cards ──────────
   REPORTED: the Select buttons hanging outside the staff cards, one card
   overlapping the next.

   The levelling rule (section 16) gives the card grid `grid-auto-rows: 1fr` so
   every card in a row ends on the same line. `1fr` distributes the container's
   height — and this template's step host is height-capped (`flex: 0 0 70%` of
   the body, itself a scroll region), so once the grid drops to TWO columns the
   same five people need three rows out of a fixed height instead of two out of
   it. Each row then gets a third of the space rather than as much as it needs.

   MEASURED at 1100x900, where the grid resolves to 2 x 347px columns and three
   rows: `grid-template-rows` came back `150.312px 150.312px 150.312px` while
   each card's own contents measured 164.5px plus 32px of padding — 196.5px in a
   150.3px row. The card does not clip, so the last thing in it, the Select
   button, was painted 41.2px below the card's own border and over the card
   beneath. At 1200 and above the grid is four columns, two rows fit, and
   nothing overflows — which is why this only shows in a band of widths.

   `minmax(min-content, 1fr)` keeps the levelling and adds the floor it was
   missing: where the height is sufficient every row still resolves to the same
   1fr and the rendering is unchanged, and where it is not, a row stops
   shrinking at its own content instead of letting the cards spill. The grid is
   already the scroll region, so the extra height is reachable.

   Scoped to this template's staff grid — the shared rule is left alone
   deliberately, because it is one line away from every card grid in the
   plugin and this is the one place the fault was reported and measured. */
#so-booking-widget[data-template="wide"] .so-bw-card-grid--staff:not(:has(.so-bw-card--has-extras-open)) {
    grid-auto-rows: minmax(min-content, 1fr);
}

/* ── S5b · The location row hangs from the top, not from its middle ──────
   REPORTED as misalignment on the location step, and it is the equal-height
   grid meeting a centred row. `.so-bw-card` is `display: flex; align-items:
   center`, which is right for a card sized by its own content; but
   `grid-auto-rows: 1fr` (section 16) levels every card in a row to the tallest,
   and a location's address is unbounded — it is the one card whose text is
   deliberately never clamped (see the note on `.so-bw-card-meta--clamp`).

   So one long address set the row's height and every SHORTER card centred its
   own contents in that height. MEASURED at 1440 across four cards in one row,
   all 360.6px tall: the four location names began 3.8, 22.7, 32.1 and 141.5px
   from the top of their cards — four names on four different lines, with up to
   142px of empty card beneath the shortest.

   Hanging the contents from the top puts every name, every pin and every
   address on the same line across the row, and collects the leftover height at
   the bottom of the cards where it reads as padding rather than as drift.
   Scoped to Wide Canvas's location step: it is the layout that shows this,
   because it is the one that puts four location cards across.

   `:not(.so-bw-sp__panel)` is carried for WEIGHT, not for meaning — the card
   this overrides is `[data-template="wide"] .so-bw-card:not(.so-bw-sp__panel)`
   (line 16462), which the negation scores at (1,3,0). Without the same guard
   this rule is (1,2,0) and loses however late it sits in the file. The staff
   profile panel is a `.so-bw-card` too and is excluded there for real reasons;
   matching the shape keeps the two rules answering the same question. */
#so-booking-widget[data-template="wide"] .so-bw-card--location:not(.so-bw-sp__panel) {
    align-items: flex-start;
}

/* ── S5b · The location list is a LIST ───────────────────────────────────
   The third step-scoped block, and the only one that exists to WIN A TIE
   rather than to restyle anything.

   C9 already says what the location step should be: `[data-template="wide"]
   .so-bw-card-grid { grid-template-columns: 1fr }`, and S4's own note is
   explicit that C9 is reversed for the service step rather than deleted
   "because C9 also serves the location step, which is still legitimately a
   list". That declaration was simply never reaching the element.

   Section 4's 960 tier packs the Location and Staff grids tighter through
   `:where([data-cwmin~="960"]) .so-bw-step:nth-child(1) .so-bw-card-grid` —
   :where() zeroes the attribute, but `.so-bw-step` + `:nth-child()` +
   `.so-bw-card-grid` still count, so it scores (1,3,0) against C9's (1,2,0)
   and wins however late C9 appears. It is the same trap Agenda (section 15),
   Roster and Counter each had to file their own exception against.

   S4 and S5 are immune WITHOUT KNOWING IT: `.so-bw-step--service` /
   `.so-bw-step--staff` take them to (1,3,0), which ties the tier and wins on
   source order. Location carried no modifier, so it was the one step in the
   architecture still being laid out by a rule meant for a 960px-wide pane.
   Inside S12's 70% column that resolved to three 183px tiles in a 580px row —
   a 44px pin plus a 14px gap leaves 97px of body, and a branch name set one
   character per line down fifteen rows.

   `.so-bw-step--location` (added to the markup in booking-widget.js beside the
   two names that already existed) restores the tie, and source order does the
   rest — exactly the mechanism S5 already relies on. Nothing else here: the
   row treatment, the frame and the rails are all C9's, which is the point. */
#so-booking-widget[data-template="wide"] .so-bw-step--location .so-bw-card-grid {
    grid-template-columns: 1fr;
}

/* ── S6 · The summary panel's contents ───────────────────────────────────
   Type and spacing only; every value in it is derived (see the getters in
   booking-widget.js). */
#so-booking-widget[data-template="wide"] .so-bw-sumpanel__head {
    display: flex;
    align-items: center;
    gap: 10px;
}
#so-booking-widget[data-template="wide"] .so-bw-sumpanel__title {
    margin: 0;
    font-size: 15.5px;
    font-weight: 700;
    letter-spacing: -0.01em;
    color: var(--so-bw-ds-n900, var(--so-bw-text));
}
#so-booking-widget[data-template="wide"] .so-bw-sumpanel__clear {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    margin-left: auto;
    width: 28px;
    height: 28px;
    padding: 0;
    border: 0;
    border-radius: var(--so-bw-ds-radius-chip);
    background: transparent;
    color: var(--so-bw-ds-n500);
    cursor: pointer;
    transition: background .15s, color .15s;
}
/* Destructive, so it earns a red on hover — but only on hover. A permanently
   red icon in a summary panel reads as an error state. */
#so-booking-widget[data-template="wide"] .so-bw-sumpanel__clear:hover {
    background: rgba(220, 38, 38, .08);
    color: #dc2626;
}
#so-booking-widget[data-template="wide"] .so-bw-sumpanel__clear:focus-visible {
    outline: 2px solid var(--so-bw-primary);
    outline-offset: 2px;
}
#so-booking-widget[data-template="wide"] .so-bw-sumpanel__empty {
    margin: 0;
    font-size: 12.5px;
    line-height: 1.5;
    color: var(--so-bw-ds-n500, var(--so-bw-text-secondary));
}
#so-booking-widget[data-template="wide"] .so-bw-sumpanel__list {
    margin: 0;
    padding: 0;
    list-style: none;
    display: flex;
    flex-direction: column;
    gap: 10px;
}
#so-booking-widget[data-template="wide"] .so-bw-sumpanel__item {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    column-gap: 10px;
    align-items: start;
}
#so-booking-widget[data-template="wide"] .so-bw-sumpanel__itemicon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 36px;
    height: 36px;
    border-radius: var(--so-bw-r-10);
    /* These two are the FALLBACK only. `serviceIconTileStyle()` supplies an
       inline `background` and `color` for every row — the service's own colour
       when it has one, the template accent when it does not — and inline wins,
       so the tile matches the service card's tile rather than inventing a
       second treatment for the same service. What is left here is what renders
       if that helper ever yields nothing, which is a readable tile rather than
       a transparent square. */
    background: color-mix(in srgb, var(--so-bw-primary) 10%, transparent);
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
    font-size: 11.5px;
    font-weight: 700;
    letter-spacing: .02em;
}
#so-booking-widget[data-template="wide"] .so-bw-sumpanel__itemtext {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
}
#so-booking-widget[data-template="wide"] .so-bw-sumpanel__itemname {
    font-size: 13px;
    font-weight: 600;
    color: var(--so-bw-ds-n900, var(--so-bw-text));
}
/* Two lines, then ellipsis. The panel reminds the customer what they chose; it
   is not a second place to read the catalogue — and an unclamped description
   would push the totals below the fold on a three-service booking. */
#so-booking-widget[data-template="wide"] .so-bw-sumpanel__itemdesc {
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    font-size: 11.5px;
    line-height: 1.45;
    color: var(--so-bw-ds-n500, var(--so-bw-text-secondary));
}
#so-booking-widget[data-template="wide"] .so-bw-sumpanel__itemmeta {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 2px;
    white-space: nowrap;
}
#so-booking-widget[data-template="wide"] .so-bw-sumpanel__itemdur {
    font-size: 11.5px;
    color: var(--so-bw-ds-n500, var(--so-bw-text-secondary));
}
/* Tabular figures so a column of prices aligns on its digits. */
#so-booking-widget[data-template="wide"] .so-bw-sumpanel__itemprice {
    font-size: 12.5px;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}

#so-booking-widget[data-template="wide"] .so-bw-sumpanel__facts {
    display: flex;
    flex-direction: column;
    gap: 12px;
    margin: 0;
    padding: 14px 0 0;
    border-top: 1px solid var(--so-bw-ds-n100);
}
#so-booking-widget[data-template="wide"] .so-bw-sumpanel__fact {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    column-gap: 10px;
    row-gap: 1px;
    align-items: center;
}
#so-booking-widget[data-template="wide"] .so-bw-sumpanel__facticon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    grid-column: 1;
    grid-row: 1 / span 2;
    width: 32px;
    height: 32px;
    border-radius: var(--so-bw-r-9);
    background: color-mix(in srgb, var(--so-bw-primary) 9%, transparent);
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}
#so-booking-widget[data-template="wide"] .so-bw-sumpanel__fact dt {
    grid-column: 2;
    font-size: 11px;
    font-weight: 500;
    letter-spacing: .01em;
    color: var(--so-bw-ds-n500, var(--so-bw-text-secondary));
}
#so-booking-widget[data-template="wide"] .so-bw-sumpanel__fact dd {
    grid-column: 2;
    margin: 0;
    font-size: 13.5px;
    font-weight: 600;
    color: var(--so-bw-ds-n900, var(--so-bw-text));
}

#so-booking-widget[data-template="wide"] .so-bw-sumpanel__totals {
    display: flex;
    flex-direction: column;
    gap: 8px;
    margin: 0;
    padding: 14px 0 0;
    border-top: 1px solid var(--so-bw-ds-n100);
}
#so-booking-widget[data-template="wide"] .so-bw-sumpanel__total {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 12px;
}
#so-booking-widget[data-template="wide"] .so-bw-sumpanel__total dt {
    font-size: 12.5px;
    color: var(--so-bw-ds-n500, var(--so-bw-text-secondary));
}
#so-booking-widget[data-template="wide"] .so-bw-sumpanel__total dd {
    margin: 0;
    font-size: 13.5px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    color: var(--so-bw-ds-n900, var(--so-bw-text));
}

#so-booking-widget[data-template="wide"] .so-bw-sumpanel__section {
    padding: 14px 0 0;
    border-top: 1px solid var(--so-bw-ds-n100);
}
#so-booking-widget[data-template="wide"] .so-bw-sumpanel__subtitle {
    margin: 0 0 8px;
    font-size: 12.5px;
    font-weight: 700;
    color: var(--so-bw-ds-n900, var(--so-bw-text));
}
#so-booking-widget[data-template="wide"] .so-bw-sumpanel__staff {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    column-gap: 10px;
    align-items: center;
    padding: 10px;
    border-radius: var(--so-bw-r-10);
    background: color-mix(in srgb, var(--so-bw-primary) 5%, var(--so-bw-ds-n0));
}
#so-booking-widget[data-template="wide"] .so-bw-sumpanel__staficon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    border-radius: 50%;
    background: color-mix(in srgb, var(--so-bw-primary) 12%, transparent);
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}
#so-booking-widget[data-template="wide"] .so-bw-sumpanel__staftext {
    display: flex;
    flex-direction: column;
    gap: 1px;
    min-width: 0;
}
#so-booking-widget[data-template="wide"] .so-bw-sumpanel__stafname {
    font-size: 13px;
    font-weight: 600;
    color: var(--so-bw-ds-n900, var(--so-bw-text));
}
#so-booking-widget[data-template="wide"] .so-bw-sumpanel__stafrole {
    font-size: 11.5px;
    color: var(--so-bw-ds-n500, var(--so-bw-text-secondary));
}
/* Green, not the accent: this pill reports a completed state, and the accent is
   already carrying "this is actionable" everywhere else on the step. */
#so-booking-widget[data-template="wide"] .so-bw-sumpanel__stafpill {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    white-space: nowrap;
    font-size: 11.5px;
    font-weight: 600;
    color: #067647;
}

/* The three notes. Same shape, different jobs: `__next` says what is coming,
   `__almost` says the flow is nearly done, `__assure` carries the one
   reassurance line that has a setting behind it. */
#so-booking-widget[data-template="wide"] .so-bw-sumpanel__next,
#so-booking-widget[data-template="wide"] .so-bw-sumpanel__assure,
#so-booking-widget[data-template="wide"] .so-bw-sumpanel__almost {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    column-gap: 10px;
    align-items: start;
    margin: 0;
    padding: 11px 12px;
    border-radius: var(--so-bw-r-10);
    font-size: 12px;
    line-height: 1.5;
}
#so-booking-widget[data-template="wide"] .so-bw-sumpanel__next {
    background: color-mix(in srgb, var(--so-bw-primary) 7%, transparent);
    color: var(--so-bw-ds-n700, var(--so-bw-text-secondary));
}
#so-booking-widget[data-template="wide"] .so-bw-sumpanel__nexticon {
    display: inline-flex;
    padding-top: 1px;
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}
#so-booking-widget[data-template="wide"] .so-bw-sumpanel__almost {
    background: rgba(6, 118, 71, .07);
    color: var(--so-bw-ds-n700, var(--so-bw-text-secondary));
}
#so-booking-widget[data-template="wide"] .so-bw-sumpanel__almosticon {
    display: inline-flex;
    padding-top: 1px;
    color: #067647;
}
#so-booking-widget[data-template="wide"] .so-bw-sumpanel__almosttext,
#so-booking-widget[data-template="wide"] .so-bw-sumpanel__assuretext {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
}
#so-booking-widget[data-template="wide"] .so-bw-sumpanel__almosttext strong {
    font-size: 12.5px;
    color: #067647;
}
/* A hairline rather than a filled card: it is the quietest thing in the panel
   and should not compete with the totals above it. */
#so-booking-widget[data-template="wide"] .so-bw-sumpanel__assure {
    padding: 11px 0 0;
    border-top: 1px solid var(--so-bw-ds-n100);
    border-radius: 0;
    color: var(--so-bw-ds-n500, var(--so-bw-text-secondary));
}
#so-booking-widget[data-template="wide"] .so-bw-sumpanel__assureicon {
    display: inline-flex;
    padding-top: 1px;
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}
#so-booking-widget[data-template="wide"] .so-bw-sumpanel__assuretext strong {
    font-size: 12.5px;
    color: var(--so-bw-ds-n900, var(--so-bw-text));
}

/* ── S7 · The docked commit bar ──────────────────────────────────────────
   C9 already docks `.so-bw-nav` with a tinted fill and a hairline over it.
   This puts the selection's three facts on the same bar, between Back and
   Continue, so the customer commits from the same place they read the total.

   `.so-bw-multi-summary` is switched off for this template: it reports the same
   two figures but only from two services up, and two running totals on one
   screen would disagree the moment their gates differ. This bar shows from one. */
#so-booking-widget[data-template="wide"] .so-bw-multi-summary {
    display: none;
}
#so-booking-widget[data-template="wide"] .so-bw-navstats {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 22px;
    /* Takes the space between the two buttons, so Back stays hard left and
       Continue hard right whatever the stats say. */
    flex: 1 1 auto;
    justify-content: center;
    min-width: 0;
}
#so-booking-widget[data-template="wide"] .so-bw-navstat {
    display: inline-flex;
    align-items: center;
    gap: 9px;
    min-width: 0;
}
#so-booking-widget[data-template="wide"] .so-bw-navstat__icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
    width: 30px;
    height: 30px;
    border-radius: var(--so-bw-r-9);
    background: color-mix(in srgb, var(--so-bw-primary) 10%, transparent);
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}
#so-booking-widget[data-template="wide"] .so-bw-navstat__text {
    display: flex;
    flex-direction: column;
    line-height: 1.25;
    min-width: 0;
}
#so-booking-widget[data-template="wide"] .so-bw-navstat__text strong {
    font-size: 13.5px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    color: var(--so-bw-ds-n900, var(--so-bw-text));
}
#so-booking-widget[data-template="wide"] .so-bw-navstat__text small {
    font-size: 11px;
    color: var(--so-bw-ds-n500, var(--so-bw-text-secondary));
}
/* Below the stack width the bar is Back + Continue again. Three stat pairs and
   two buttons do not fit a phone, and the same figures are in the summary
   panel, which sits directly above the bar once the columns stack. */
    #so-booking-widget:where([data-cw~="700"])[data-template="wide"] .so-bw-navstats {
        display: none;
    }

/* ── S8 · Review: the facts pane, two facts per line ────────────────────
   Section R6 already gives this step its two panes — what you booked on the
   left, what you are about to pay on the right. This narrows the left pane's
   rows from one-per-line to TWO, which is what makes the whole booking legible
   without scrolling: six facts (service, staff, date, time, timezone,
   duration) in three lines rather than six.

   Scoped to `.so-bw-summary-main`, so the money pane is untouched. Its rows
   are label-and-figure pairs that have to stay on one line and align on their
   decimal — a two-column grid would put "GST (18%)" beside "Subtotal" and
   break the column of prices they are read down. */
#so-booking-widget[data-template="wide"] .so-bw-summary-main .so-bw-summary-section {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    column-gap: 24px;
    row-gap: 0;
    align-items: start;
}
/* The section's own heading ("Your Details") is not a fact and spans the pair. */
#so-booking-widget[data-template="wide"] .so-bw-summary-main .so-bw-summary-section-title {
    grid-column: 1 / -1;
}
/* Label ABOVE value, not beside it. In a full-width row the label can sit left
   and the value right because there is a whole line to separate them; in a
   half-width cell that same rule puts a right-aligned value hard against the
   next column's label, so the pair reads as one wrong sentence ("Timezone
   Asia/Kolkata Duration"). Stacking removes the ambiguity and gives the value
   the emphasis it should have had anyway. */
#so-booking-widget[data-template="wide"] .so-bw-summary-main .so-bw-summary-row {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 2px;
    padding: 11px 0;
    border-bottom: 1px solid var(--so-bw-ds-n100);
}
/* The hairline is a divider BETWEEN lines, so the bottom line of each column
   drops it. `:nth-last-child(-n+2)` is the last row of each of the two columns
   for an even count; for an odd count the final row is alone on its line and
   the same selector still reaches it. */
#so-booking-widget[data-template="wide"] .so-bw-summary-main .so-bw-summary-row:nth-last-child(-n+2) {
    border-bottom: 0;
}
#so-booking-widget[data-template="wide"] .so-bw-summary-main .so-bw-summary-label {
    flex: 0 0 auto;
    font-size: 11px;
    font-weight: 500;
    color: var(--so-bw-ds-n500, var(--so-bw-text-secondary));
}
#so-booking-widget[data-template="wide"] .so-bw-summary-main .so-bw-summary-value {
    text-align: left;
    font-size: 13.5px;
    font-weight: 600;
    color: var(--so-bw-ds-n900, var(--so-bw-text));
}
/* One fact per line again once the pane is too narrow to halve. This is the
   SAME 900px container-width gate R6 uses to stack the two panes, so the
   halving and the stacking happen together rather than leaving a stage where
   the pane is full width but its facts are still crammed two-up. */
    #so-booking-widget:where([data-cw~="900"])[data-template="wide"] .so-bw-summary-main .so-bw-summary-section {
        grid-template-columns: minmax(0, 1fr);
    }
    /* Every row is now the last of its line, so the divider rule above would
       strip the wrong two. Restate it for one column. */
    #so-booking-widget:where([data-cw~="900"])[data-template="wide"] .so-bw-summary-main .so-bw-summary-row {
        border-bottom: 1px solid var(--so-bw-ds-n100);
    }
    #so-booking-widget:where([data-cw~="900"])[data-template="wide"] .so-bw-summary-main .so-bw-summary-row:last-child {
        border-bottom: 0;
    }


/* ==========================================================================
   W. WEEK BOARD  —  THE WHOLE FLOW
   --------------------------------------------------------------------------
   Week Board arrived as a DATE treatment and nothing else. Section H gave it a
   timetable, the block above gave that timetable a look, and the other four
   steps were left as whatever the base stylesheet happened to render — so a
   customer walked from a generic service list, through a generic staff list,
   into a board with a strong identity, and back out into a generic form. One
   step in five was the template; the rest were the default in amber.

   This section makes the other four match. It does NOT invent primitives:
   every element it styles is already in the markup, sitting behind the
   `display: none` gate in section 12b that Wide Canvas opens for itself. What
   changes here is which template opens which gate, and what the opened element
   looks like once it does.

   Where a rule is close to Wide Canvas's, that is deliberate rather than
   copied by accident: both templates answer the same brief (a roomy,
   two-column, explicitly-committed flow), and a second visual vocabulary for
   the same job would be a third thing to maintain. Where they diverge, the
   comment says why.

   Scoped entirely under [data-template="weekboard"]. The other eighteen
   architectures are untouched by every rule below.
   ========================================================================== */

/* ── W1 · The step bar, given its words back ─────────────────────────────
   Section H reduced this to seven unlabelled 7px dots. That reads as a
   progress BAR — "you are somewhere in a sequence" — and the base stepbar it
   overrode already renders the thing this template actually wants: numbered
   circles joined by a rule, each named, completed ones ticked.

   The reduction made sense when Week Board's date step was the only screen
   with any identity: a quiet indicator kept attention on the board. Now that
   all five steps carry the template, the indicator is the one element telling
   a customer mid-flow how much is left, and seven anonymous dots cannot.

   These four rules restore the BASE treatment rather than declaring a new one,
   so the stepbar stays one implementation with one set of states. */
#so-booking-widget[data-template="weekboard"] .so-bw-stepbar {
    gap: 0;
}
#so-booking-widget[data-template="weekboard"] .so-bw-stepbar-label {
    display: block;
}
#so-booking-widget[data-template="weekboard"] .so-bw-stepbar-dot {
    width: 30px;
    height: 30px;
    font-size: 13px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    border-radius: 999px;
}
#so-booking-widget[data-template="weekboard"] .so-bw-stepbar-dot svg {
    display: block;
}

/* ── W2 · The service step header and filters ────────────────────────────
   A titled header, a search field, a row of category chips, then the cards.
   All four already exist; three of them are behind the 12b gate. */
#so-booking-widget[data-template="weekboard"] .so-bw-svchead {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    column-gap: 14px;
    row-gap: 3px;
    align-items: center;
    margin: 0 0 18px;
}
/* The tile spans both rows so it is optically centred against the PAIR of
   lines, not against the heading alone. */
#so-booking-widget[data-template="weekboard"] .so-bw-svchead__icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    grid-column: 1;
    grid-row: 1 / span 2;
    width: 44px;
    height: 44px;
    border-radius: var(--so-bw-ds-radius-card);
    background: color-mix(in srgb, var(--so-bw-primary) 10%, transparent);
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}
#so-booking-widget[data-template="weekboard"] .so-bw-svchead__title {
    display: block;
    grid-column: 2;
    margin: 0;
    font-size: 20px;
    font-weight: 700;
    line-height: 1.2;
    letter-spacing: -0.015em;
    color: var(--so-bw-ds-n900, var(--so-bw-text));
}
#so-booking-widget[data-template="weekboard"] .so-bw-svchead .so-bw-step-desc {
    grid-column: 2;
    margin: 0;
    font-size: 13.5px;
    line-height: 1.5;
    color: var(--so-bw-ds-n500, var(--so-bw-text-secondary));
}

/* Category chips. Filter toggles over one list, not a tablist — see the
   markup note above `.so-bw-catbar`. */
#so-booking-widget[data-template="weekboard"] .so-bw-catbar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px;
    margin: 0 0 16px;
}
#so-booking-widget[data-template="weekboard"] .so-bw-catchip {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    /* 38px, above Wide Canvas's 32px and still below the 44px floor this file
       applies to PRIMARY controls. That floor is a rule about controls which
       commit something; a filter chip commits nothing, is reachable by simply
       scrolling the list it filters, and at 44px a row of four would outweigh
       the cards underneath it. */
    min-height: 38px;
    padding: 0 14px;
    border: 1px solid var(--so-bw-border, var(--so-bw-ds-n200));
    border-radius: var(--so-bw-ds-radius-ctl);
    background: var(--so-bw-ds-n0);
    color: var(--so-bw-ds-n700, var(--so-bw-text-secondary));
    font-family: inherit;
    font-size: 13px;
    font-weight: 500;
    line-height: 1;
    cursor: pointer;
    transition: background .15s, border-color .15s, color .15s;
}
#so-booking-widget[data-template="weekboard"] .so-bw-catchip__icon {
    flex: 0 0 auto;
    color: var(--so-bw-ds-n500);
}
#so-booking-widget[data-template="weekboard"] .so-bw-catchip:hover {
    background: var(--so-bw-ds-n25);
    border-color: var(--so-bw-ds-n300);
}
/* The ON chip is carried by fill AND weight, never by hue alone — the same
   greyscale test this repo applies to its template gallery applies to a filter
   bar, where otherwise a colour-blind customer cannot tell which filter is on. */
#so-booking-widget[data-template="weekboard"] .so-bw-catchip--on {
    background: color-mix(in srgb, var(--so-bw-primary) 9%, transparent);
    border-color: color-mix(in srgb, var(--so-bw-primary) 45%, transparent);
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
    font-weight: 600;
}
#so-booking-widget[data-template="weekboard"] .so-bw-catchip--on .so-bw-catchip__icon {
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}

/* ── W3 · The service card ───────────────────────────────────────────────
   Two up, and each card is a row: circular tile at the left, name and
   description beside it, then duration, price and the commit on one baseline
   underneath.

   The tile is `.so-bw-card-media` — the fixed-height header band the base card
   uses to keep photo-cards and initials-cards the same height. That band is
   the right call for a gallery of photographed services and the wrong one
   here, where the design leads each row with a 56px disc. Collapsing it to a
   disc rather than hiding it and styling the avatar directly keeps the photo
   case working: a service WITH an image gets the same disc, cropped. */
#so-booking-widget[data-template="weekboard"] .so-bw-step--service .so-bw-card-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
    gap: 16px;
}
#so-booking-widget[data-template="weekboard"] .so-bw-step--service .so-bw-card:not(.so-bw-sp__panel) {
    position: relative;
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    column-gap: 16px;
    align-items: start;
    padding: 18px;
    border: 1px solid var(--so-bw-border, var(--so-bw-ds-n200));
    border-radius: var(--so-bw-ds-radius-card);
    background: var(--so-bw-ds-n0);
    transition: border-color .15s, background .15s, box-shadow .15s;
}
#so-booking-widget[data-template="weekboard"] .so-bw-step--service .so-bw-card:not(.so-bw-sp__panel):hover {
    border-color: var(--so-bw-ds-n300);
    box-shadow: var(--so-bw-ds-shadow-sm);
}
/* Selected states its case three ways — border, tint, and the tick badge below
   — because one of the three is hue, and hue alone is not a state. */
#so-booking-widget[data-template="weekboard"] .so-bw-step--service .so-bw-card--selected {
    border-color: var(--so-bw-primary);
    box-shadow: inset 0 0 0 1px var(--so-bw-primary);
    background: color-mix(in srgb, var(--so-bw-primary) 5%, var(--so-bw-ds-n0));
}
#so-booking-widget[data-template="weekboard"] .so-bw-step--service .so-bw-card-media {
    grid-column: 1;
    grid-row: 1;
    width: 56px;
    height: 56px;
    min-height: 0;
    margin: 0;
    border-radius: 999px;
    overflow: hidden;
}
#so-booking-widget[data-template="weekboard"] .so-bw-step--service .so-bw-card-media-avatar {
    width: 100%;
    height: 100%;
    border-radius: 999px;
    font-size: 17px;
    font-weight: 700;
}
#so-booking-widget[data-template="weekboard"] .so-bw-step--service .so-bw-card-media-img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}
/* A 56px disc has no room for arrows or dots, and the full gallery is reachable
   from the service's own description modal. Hiding them rather than leaving
   them to overflow keeps the disc a disc. */
#so-booking-widget[data-template="weekboard"] .so-bw-step--service .so-bw-card-media-nav,
#so-booking-widget[data-template="weekboard"] .so-bw-step--service .so-bw-card-media-dots {
    display: none;
}
/* `.so-bw-card-top` is flattened so its two children become grid items of the
   CARD — without this the tick badge is a grandchild and cannot be placed
   against the card's own corner. */
#so-booking-widget[data-template="weekboard"] .so-bw-step--service .so-bw-card-top {
    display: contents;
}
#so-booking-widget[data-template="weekboard"] .so-bw-step--service .so-bw-card-body {
    grid-column: 2;
    grid-row: 1;
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 8px;
    min-width: 0;
}
#so-booking-widget[data-template="weekboard"] .so-bw-step--service .so-bw-card-title {
    font-size: 15.5px;
    font-weight: 700;
    line-height: 1.3;
    color: var(--so-bw-ds-n900, var(--so-bw-text));
}
#so-booking-widget[data-template="weekboard"] .so-bw-step--service .so-bw-card-meta {
    font-size: 13px;
    line-height: 1.5;
    color: var(--so-bw-ds-n500, var(--so-bw-text-secondary));
}
/* The tick badge, against the card's top-right corner. Absolute rather than a
   grid cell so it can sit ON the border radius the way the design draws it,
   and so a long service name never has to leave a column free for it. */
#so-booking-widget[data-template="weekboard"] .so-bw-step--service .so-bw-card-check {
    position: absolute;
    top: 12px;
    right: 12px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 24px;
    height: 24px;
    border-radius: 999px;
    background: var(--so-bw-primary);
    color: var(--so-bw-on-primary, #fff);
    /* Fades rather than appears, so the card does not reflow as it is picked. */
    opacity: 0;
    transition: opacity .15s;
}
#so-booking-widget[data-template="weekboard"] .so-bw-step--service .so-bw-card-check--visible {
    opacity: 1;
}
#so-booking-widget[data-template="weekboard"] .so-bw-step--service .so-bw-card-check svg {
    width: 13px;
    height: 13px;
}
#so-booking-widget[data-template="weekboard"] .so-bw-step--service .so-bw-card-check-order {
    font-family: var(--so-bw-mono);
    font-size: 11px;
    font-weight: 700;
    color: var(--so-bw-on-primary, #fff);
}

/* The bottom rail: duration, price, and the commit. `.so-bw-card-badges` and
   `.so-bw-card-pick` are both children of the body, so the rail is made by
   pushing the badges to the bottom and letting the button take the end. */
#so-booking-widget[data-template="weekboard"] .so-bw-step--service .so-bw-card-badges {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px;
    width: 100%;
    /* Pushes this row to the bottom of the body so two cards with unequal
       description lengths still line their badges and buttons up. */
    margin-top: auto;
    padding-top: 4px;
}
#so-booking-widget[data-template="weekboard"] .so-bw-step--service .so-bw-badge {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    min-height: 30px;
    padding: 0 11px;
    border: 0;
    border-radius: var(--so-bw-ds-radius-chip);
    background: var(--so-bw-ds-n50, var(--so-bw-ds-n25));
    color: var(--so-bw-ds-n700, var(--so-bw-text-secondary));
    font-size: 12.5px;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
}
/* On a picked card the duration badge takes the accent tint, so the row the
   customer just committed to reads as one object rather than as an amber frame
   around three grey chips. */
#so-booking-widget[data-template="weekboard"] .so-bw-step--service .so-bw-card--selected .so-bw-badge--duration {
    background: color-mix(in srgb, var(--so-bw-primary) 12%, transparent);
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}
#so-booking-widget[data-template="weekboard"] .so-bw-step--service .so-bw-card-pick {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    margin-left: auto;
    /* 38px: this DOES commit, but it is a second route into a card that is
       itself the target and is ~90px tall. The 44px floor is held by the card. */
    min-height: 38px;
    padding: 0 16px;
    border: 1px solid var(--so-bw-border, var(--so-bw-ds-n300));
    border-radius: var(--so-bw-ds-radius-ctl);
    background: var(--so-bw-ds-n0);
    color: var(--so-bw-ds-n900, var(--so-bw-text));
    font-family: inherit;
    font-size: 13px;
    font-weight: 600;
    line-height: 1;
    cursor: pointer;
    transition: background .15s, border-color .15s, color .15s;
}
#so-booking-widget[data-template="weekboard"] .so-bw-step--service .so-bw-card-pick:hover {
    border-color: var(--so-bw-primary);
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}
#so-booking-widget[data-template="weekboard"] .so-bw-step--service .so-bw-card-pick--on {
    background: var(--so-bw-primary);
    border-color: var(--so-bw-primary);
    color: var(--so-bw-on-primary, #fff);
}
#so-booking-widget[data-template="weekboard"] .so-bw-step--service .so-bw-card-pick--on:hover {
    color: var(--so-bw-on-primary, #fff);
}
/* `.so-bw-card-add` — Wide Canvas's old +/- glyph — was hidden here so the two
   commit vocabularies could not both appear. The class no longer exists: the
   glyph and the checkbox were consolidated onto `.so-bw-card-pick`, which this
   section styles above, so there is nothing left to suppress. */

/* ── W4 · The docked selection facts ─────────────────────────────────────
   "1 service selected · Total duration · Total price", docked on the same bar
   as Continue so the customer commits from the same place they read the total.
   Gated in the markup to the service step: "1 service selected" is not a fact
   about the date picker.

   `.so-bw-multi-summary` is switched off with it. That block reports the same
   two figures but only from TWO services up, and two running totals on one
   screen would disagree the moment their gates differ. This bar shows from one. */
#so-booking-widget[data-template="weekboard"] .so-bw-multi-summary {
    display: none;
}
#so-booking-widget[data-template="weekboard"] .so-bw-navstats {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 22px;
    /* Takes the space between the two buttons, so Back stays hard left and
       Continue hard right whatever the stats say. */
    flex: 1 1 auto;
    justify-content: center;
    min-width: 0;
}
#so-booking-widget[data-template="weekboard"] .so-bw-navstat {
    display: inline-flex;
    align-items: center;
    gap: 9px;
    min-width: 0;
}
#so-booking-widget[data-template="weekboard"] .so-bw-navstat__icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
    width: 32px;
    height: 32px;
    border-radius: 999px;
    background: color-mix(in srgb, var(--so-bw-primary) 10%, transparent);
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}
#so-booking-widget[data-template="weekboard"] .so-bw-navstat__text {
    display: flex;
    flex-direction: column;
    line-height: 1.25;
    min-width: 0;
}
#so-booking-widget[data-template="weekboard"] .so-bw-navstat__text strong {
    font-size: 13.5px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    color: var(--so-bw-ds-n900, var(--so-bw-text));
}
#so-booking-widget[data-template="weekboard"] .so-bw-navstat__text small {
    font-size: 11px;
    color: var(--so-bw-ds-n500, var(--so-bw-text-secondary));
}
/* Below the stack width the bar is Back + Continue again. Three stat pairs and
   two buttons do not fit a phone, and the same figures are in the running
   summary that reappears once the bar drops them. */
    #so-booking-widget:where([data-cw~="700"])[data-template="weekboard"] .so-bw-navstats {
        display: none;
    }
    #so-booking-widget:where([data-cw~="700"])[data-template="weekboard"] .so-bw-multi-summary {
        display: block;
    }

/* ── W5 · The staff step ─────────────────────────────────────────────────
   The one step that is NOT a table anywhere: a staff member is a person with a
   face, a role and a bio, and a row strips all three to a line of text.

   Two things the service cards do not need:

     • `.so-bw-card-role` — the job title on its OWN line, above the bio. The
       base markup renders `member.bio || member.title`, so a member with both
       has their title thrown away and reads as prose with no idea what they
       do. The role line is behind the 12b gate for exactly this reason.
     • `.so-bw-card-profile` — "View profile". A card carrying a face, a role
       and a bio has more to say than fits, and the popup is where it says it. */
#so-booking-widget[data-template="weekboard"] .so-bw-step--staff .so-bw-card-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
    gap: 16px;
}
#so-booking-widget[data-template="weekboard"] .so-bw-step--staff .so-bw-card:not(.so-bw-sp__panel) {
    position: relative;
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    column-gap: 16px;
    align-items: start;
    padding: 18px;
    border: 1px solid var(--so-bw-border, var(--so-bw-ds-n200));
    border-radius: var(--so-bw-ds-radius-card);
    background: var(--so-bw-ds-n0);
    transition: border-color .15s, background .15s, box-shadow .15s;
}
#so-booking-widget[data-template="weekboard"] .so-bw-step--staff .so-bw-card:not(.so-bw-sp__panel):hover {
    border-color: var(--so-bw-ds-n300);
    box-shadow: var(--so-bw-ds-shadow-sm);
}
#so-booking-widget[data-template="weekboard"] .so-bw-step--staff .so-bw-card--selected {
    border-color: var(--so-bw-primary);
    box-shadow: inset 0 0 0 1px var(--so-bw-primary);
    background: color-mix(in srgb, var(--so-bw-primary) 5%, var(--so-bw-ds-n0));
}
#so-booking-widget[data-template="weekboard"] .so-bw-step--staff .so-bw-card-icon--avatar {
    width: 56px;
    height: 56px;
    border-radius: 999px;
    font-size: 17px;
    font-weight: 700;
}
#so-booking-widget[data-template="weekboard"] .so-bw-step--staff .so-bw-card-body {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 6px;
    min-width: 0;
}
#so-booking-widget[data-template="weekboard"] .so-bw-step--staff .so-bw-card-title {
    font-size: 15.5px;
    font-weight: 700;
    line-height: 1.3;
    color: var(--so-bw-ds-n900, var(--so-bw-text));
}
/* The role carries the accent because it is the fact a customer is scanning
   FOR — "who does the thing I came for" — and it is the only line on the card
   that is neither a name nor prose. */
#so-booking-widget[data-template="weekboard"] .so-bw-step--staff .so-bw-card-role {
    display: block;
    font-size: 12.5px;
    font-weight: 600;
    line-height: 1.3;
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}
#so-booking-widget[data-template="weekboard"] .so-bw-step--staff .so-bw-card-meta {
    font-size: 13px;
    line-height: 1.5;
    color: var(--so-bw-ds-n500, var(--so-bw-text-secondary));
}
/* "View profile" and the commit share the bottom rail. The profile link is a
   quiet text affordance so the filled button is unambiguously the primary. */
#so-booking-widget[data-template="weekboard"] .so-bw-step--staff .so-bw-card-profile {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    margin-top: auto;
    padding: 6px 0;
    border: 0;
    background: transparent;
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
    font-family: inherit;
    font-size: 12.5px;
    font-weight: 600;
    cursor: pointer;
}
#so-booking-widget[data-template="weekboard"] .so-bw-step--staff .so-bw-card-profile svg {
    width: 14px;
    height: 14px;
}
#so-booking-widget[data-template="weekboard"] .so-bw-step--staff .so-bw-card-profile:hover {
    text-decoration: underline;
}
#so-booking-widget[data-template="weekboard"] .so-bw-step--staff .so-bw-card-pick {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    min-height: 38px;
    padding: 0 16px;
    border: 1px solid var(--so-bw-border, var(--so-bw-ds-n300));
    border-radius: var(--so-bw-ds-radius-ctl);
    background: var(--so-bw-ds-n0);
    color: var(--so-bw-ds-n900, var(--so-bw-text));
    font-family: inherit;
    font-size: 13px;
    font-weight: 600;
    line-height: 1;
    cursor: pointer;
    transition: background .15s, border-color .15s, color .15s;
}
#so-booking-widget[data-template="weekboard"] .so-bw-step--staff .so-bw-card-pick:hover {
    border-color: var(--so-bw-primary);
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}
#so-booking-widget[data-template="weekboard"] .so-bw-step--staff .so-bw-card-pick--on {
    background: var(--so-bw-primary);
    border-color: var(--so-bw-primary);
    color: var(--so-bw-on-primary, #fff);
}
#so-booking-widget[data-template="weekboard"] .so-bw-step--staff .so-bw-card-pick--on:hover {
    color: var(--so-bw-on-primary, #fff);
}
#so-booking-widget[data-template="weekboard"] .so-bw-step--staff .so-bw-card-check {
    position: absolute;
    top: 12px;
    right: 12px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 24px;
    height: 24px;
    border-radius: 999px;
    background: var(--so-bw-primary);
    color: var(--so-bw-on-primary, #fff);
    opacity: 0;
    transition: opacity .15s;
}
#so-booking-widget[data-template="weekboard"] .so-bw-step--staff .so-bw-card-check--visible {
    opacity: 1;
}
#so-booking-widget[data-template="weekboard"] .so-bw-step--staff .so-bw-card-check svg {
    width: 13px;
    height: 13px;
}

/* "Any Available" is the fastest route through this step, so it leads the list
   full width with a tinted field, rather than sitting as the first of N equal
   cards where it reads as one more person. It is `.so-bw-card--wide` in the
   markup order (first child of the grid); spanning every column is what makes
   it a banner instead of a tile. */
#so-booking-widget[data-template="weekboard"] .so-bw-step--staff .so-bw-card-grid > .so-bw-card:first-child {
    grid-column: 1 / -1;
    align-items: center;
    background: color-mix(in srgb, var(--so-bw-primary) 6%, var(--so-bw-ds-n0));
    border-color: color-mix(in srgb, var(--so-bw-primary) 35%, transparent);
}
#so-booking-widget[data-template="weekboard"] .so-bw-step--staff .so-bw-card-grid > .so-bw-card:first-child .so-bw-card-body {
    /* The banner puts its commit at the RIGHT end of one line rather than under
       the text, which is the shape that distinguishes it from the cards below. */
    flex-direction: row;
    align-items: center;
    gap: 16px;
    flex-wrap: wrap;
}
#so-booking-widget[data-template="weekboard"] .so-bw-step--staff .so-bw-card-grid > .so-bw-card:first-child .so-bw-card-title {
    flex: 0 0 auto;
}
#so-booking-widget[data-template="weekboard"] .so-bw-step--staff .so-bw-card-grid > .so-bw-card:first-child .so-bw-card-meta {
    flex: 1 1 240px;
    min-width: 0;
}
#so-booking-widget[data-template="weekboard"] .so-bw-step--staff .so-bw-card-grid > .so-bw-card:first-child .so-bw-card-pick {
    flex: 0 0 auto;
    margin-left: auto;
    background: var(--so-bw-primary);
    border-color: var(--so-bw-primary);
    color: var(--so-bw-on-primary, #fff);
}

/* ── W6 · The details step ───────────────────────────────────────────────
   The form on the left, a read-only summary of the booking on the right, so
   the customer can check what they are about to confirm without going back.

   FLEX, NOT GRID, and that is load-bearing: `.so-bw-sumpanel` is absent on the
   date step and the review step (`summaryPanelSteps`). With two explicit grid
   tracks the second keeps its width AND its gap while its only item is
   `display: none`, stranding ~320px of dead canvas on the busiest screen in
   the flow. A flex container adds no gap for an item that is not rendered. */
#so-booking-widget[data-template="weekboard"] .so-bw-body {
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
    align-items: flex-start;
    gap: 20px;
}
/* Same as Wide Canvas: the full-bleed `.so-bw-recap` row is gone with the
   recap markup itself. */
/* `min-width: 0` is not optional: a flex item's automatic minimum size is its
   content, so a long service name or an unbroken URL would push this column
   past its share and shove the panel off the right edge instead of wrapping. */
#so-booking-widget[data-template="weekboard"] .so-bw-step-host {
    flex: 1 1 0;
    min-width: 0;
}
#so-booking-widget[data-template="weekboard"] .so-bw-sumpanel {
    display: flex;
    flex-direction: column;
    gap: 14px;
    flex: 0 0 320px;
    box-sizing: border-box;
    padding: 18px;
    border: 1px solid var(--so-bw-border, var(--so-bw-ds-n200));
    border-radius: var(--so-bw-ds-radius-card);
    background: var(--so-bw-ds-n0);
    /* `max-height` plus its own `overflow-y` guard a failure this repo has
       already been bitten by: a sticky box TALLER than its scrollport pins at
       `top` and never releases, stranding whatever sits below it.

       No scrollbar styling here on purpose — `.so-bw-body` already declares
       `scrollbar-width`/`scrollbar-color`, and setting either on a descendant
       silently kills every `::-webkit-scrollbar` rule in Chromium. */
    position: sticky;
    top: 0;
    max-height: 100%;
    overflow-y: auto;
    overscroll-behavior: contain;
}
/* Below this container width the two columns stop being two columns: at 900px
   a 320px panel leaves the form under 560px, which is narrower than a
   two-column form needs. Gated on `data-cw` (the container width the widget
   measures for itself in _wireContainerWidth) rather than a viewport media
   query, because this widget is embedded and the viewport says nothing about
   the box it was given. */
    #so-booking-widget:where([data-cw~="900"])[data-template="weekboard"] .so-bw-sumpanel {
        flex: 0 0 100%;
        position: static;
        max-height: none;
        overflow-y: visible;
    }

/* ── W7 · The card's bottom rail, on ONE line ────────────────────────────
   `.so-bw-card-badges` and `.so-bw-card-pick` are SIBLINGS inside the body
   with no row wrapper between them, so a column flex puts them on two lines:
   duration and price, then Select underneath. The rendered walk showed it
   immediately — every card an extra 38px tall, and the commit detached from
   the facts it commits to.

   A grid fixes it without a wrapper the other twenty-one templates would also
   have to carry. The name and description span both tracks; the badges take
   the first and the button the second, on one row, both aligned to the bottom
   so cards of unequal description length still rule their buttons together. */
#so-booking-widget[data-template="weekboard"] .so-bw-step--service .so-bw-card-body,
#so-booking-widget[data-template="weekboard"] .so-bw-step--staff .so-bw-card-body {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    align-content: start;
    row-gap: 7px;
    column-gap: 12px;
}
#so-booking-widget[data-template="weekboard"] .so-bw-step--service .so-bw-card-title,
#so-booking-widget[data-template="weekboard"] .so-bw-step--service .so-bw-card-meta,
#so-booking-widget[data-template="weekboard"] .so-bw-step--service .so-bw-card-more,
#so-booking-widget[data-template="weekboard"] .so-bw-step--staff .so-bw-card-title,
#so-booking-widget[data-template="weekboard"] .so-bw-step--staff .so-bw-card-role,
#so-booking-widget[data-template="weekboard"] .so-bw-step--staff .so-bw-card-meta {
    grid-column: 1 / -1;
}
#so-booking-widget[data-template="weekboard"] .so-bw-step--service .so-bw-card-badges {
    grid-column: 1;
    width: auto;
    align-self: end;
    /* The row is placed by the grid now; `margin-top: auto` was the column-flex
       way of saying the same thing and would fight `align-content: start`. */
    margin-top: 0;
    padding-top: 2px;
}
#so-booking-widget[data-template="weekboard"] .so-bw-step--service .so-bw-card-pick,
#so-booking-widget[data-template="weekboard"] .so-bw-step--staff .so-bw-card-pick {
    grid-column: 2;
    align-self: end;
    margin-left: 0;
}
/* Staff has "View profile" where services have their badges — same slot, same
   baseline, so the two steps rule their bottom rails identically. */
#so-booking-widget[data-template="weekboard"] .so-bw-step--staff .so-bw-card-profile {
    grid-column: 1;
    justify-self: start;
    align-self: end;
    margin-top: 0;
}

/* ── W8 · Where the summary rail belongs ─────────────────────────────────
   Not on the service or staff steps. Those two carry the running total on the
   nav bar instead (W4), and the rail would state the same figures a second
   time three inches away — the exact duplication the `.so-bw-multi-summary`
   note above refuses for the same reason. They also want the full width: two
   columns of cards at 320px do not fit beside a 320px rail.

   It earns its place from the details step on, where the customer is typing
   into a form and the booking they are about to confirm has scrolled out of
   reach. That is the one screen where restating it is not repetition.

   Keyed on `data-step-kind` rather than a step number because Studio can
   switch steps on and off, so the numbering differs per site. */
#so-booking-widget[data-template="weekboard"] .so-bw-body[data-step-kind="service"] .so-bw-sumpanel,
#so-booking-widget[data-template="weekboard"] .so-bw-body[data-step-kind="staff"] .so-bw-sumpanel {
    display: none;
}

/* ── W9 · The summary rail itself ────────────────────────────────────────
   W6 revealed the panel and framed it; these are its contents. Without them
   the list renders as a browser-default `<ul>` — a bullet, a raw trash glyph
   and unspaced facts — which is what the first rendered walk caught. */
#so-booking-widget[data-template="weekboard"] .so-bw-sumpanel__head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
}
#so-booking-widget[data-template="weekboard"] .so-bw-sumpanel__title {
    margin: 0;
    font-size: 15px;
    font-weight: 700;
    letter-spacing: -0.01em;
    color: var(--so-bw-ds-n900, var(--so-bw-text));
}
#so-booking-widget[data-template="weekboard"] .so-bw-sumpanel__clear {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
    width: 30px;
    height: 30px;
    padding: 0;
    border: 1px solid var(--so-bw-border, var(--so-bw-ds-n200));
    border-radius: var(--so-bw-ds-radius-chip);
    background: var(--so-bw-ds-n0);
    color: var(--so-bw-ds-n500);
    cursor: pointer;
    transition: border-color .15s, color .15s;
}
#so-booking-widget[data-template="weekboard"] .so-bw-sumpanel__clear svg {
    width: 14px;
    height: 14px;
}
/* Destructive, so it declares itself on hover rather than staying neutral —
   but in the danger token, never in the template accent, which everywhere else
   in this flow means "chosen". */
#so-booking-widget[data-template="weekboard"] .so-bw-sumpanel__clear:hover {
    border-color: var(--so-bw-danger, #b42318);
    color: var(--so-bw-danger, #b42318);
}
#so-booking-widget[data-template="weekboard"] .so-bw-sumpanel__empty {
    margin: 0;
    font-size: 13px;
    line-height: 1.5;
    color: var(--so-bw-ds-n500, var(--so-bw-text-secondary));
}
#so-booking-widget[data-template="weekboard"] .so-bw-sumpanel__list {
    display: flex;
    flex-direction: column;
    gap: 12px;
    /* The browser default is a bulleted, indented list. This is a set of rows. */
    margin: 0;
    padding: 0;
    list-style: none;
}
#so-booking-widget[data-template="weekboard"] .so-bw-sumpanel__item {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    column-gap: 10px;
    align-items: start;
}
#so-booking-widget[data-template="weekboard"] .so-bw-sumpanel__itemicon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 30px;
    height: 30px;
    border-radius: 999px;
    font-size: 11px;
    font-weight: 700;
    line-height: 1;
}
#so-booking-widget[data-template="weekboard"] .so-bw-sumpanel__itemtext {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
}
#so-booking-widget[data-template="weekboard"] .so-bw-sumpanel__itemname {
    font-size: 13px;
    font-weight: 600;
    line-height: 1.35;
    color: var(--so-bw-ds-n900, var(--so-bw-text));
}
#so-booking-widget[data-template="weekboard"] .so-bw-sumpanel__itemdesc {
    /* Two lines, then an ellipsis: a verbose service must not push the totals
       below the fold of a sticky panel. */
    display: -webkit-box;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    font-size: 12px;
    line-height: 1.45;
    color: var(--so-bw-ds-n500, var(--so-bw-text-secondary));
}
#so-booking-widget[data-template="weekboard"] .so-bw-sumpanel__itemmeta {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin-top: 2px;
    font-size: 12px;
    font-variant-numeric: tabular-nums;
}
#so-booking-widget[data-template="weekboard"] .so-bw-sumpanel__itemdur {
    color: var(--so-bw-ds-n500, var(--so-bw-text-secondary));
}
#so-booking-widget[data-template="weekboard"] .so-bw-sumpanel__itemprice {
    font-weight: 600;
    color: var(--so-bw-ds-n900, var(--so-bw-text));
}
/* The fact rows — service, staff, date, time, duration — each an icon, a
   label and a value, ruled apart. This is the shape the details step shows.

   The row is a `<dl>` of `<div>`s holding a span, a `<dt>` and a `<dd>`, with
   no class on the two text elements, so they are addressed by element. A `dl`
   carries a browser default margin and a `dd` a 40px inline start — both have
   to go or every value sits indented under a phantom hanging indent. */
#so-booking-widget[data-template="weekboard"] .so-bw-sumpanel__facts {
    display: flex;
    flex-direction: column;
    margin: 0;
}
#so-booking-widget[data-template="weekboard"] .so-bw-sumpanel__fact {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    column-gap: 10px;
    align-items: center;
    padding: 11px 0;
    border-bottom: 1px solid var(--so-bw-ds-n100);
}
#so-booking-widget[data-template="weekboard"] .so-bw-sumpanel__fact:last-child {
    border-bottom: 0;
}
/* The icon spans both text rows so it is centred against the PAIR, the same
   call the step header makes for its tile. */
#so-booking-widget[data-template="weekboard"] .so-bw-sumpanel__facticon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    grid-column: 1;
    grid-row: 1 / span 2;
    width: 32px;
    height: 32px;
    border-radius: 999px;
    background: color-mix(in srgb, var(--so-bw-primary) 10%, transparent);
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}
#so-booking-widget[data-template="weekboard"] .so-bw-sumpanel__facticon svg {
    width: 15px;
    height: 15px;
}
#so-booking-widget[data-template="weekboard"] .so-bw-sumpanel__fact dt {
    grid-column: 2;
    grid-row: 1;
    margin: 0;
    font-size: 11.5px;
    line-height: 1.35;
    color: var(--so-bw-ds-n500, var(--so-bw-text-secondary));
}
#so-booking-widget[data-template="weekboard"] .so-bw-sumpanel__fact dd {
    grid-column: 2;
    grid-row: 2;
    /* `dd` has a 40px margin-inline-start by default. */
    margin: 0;
    font-size: 13.5px;
    font-weight: 700;
    line-height: 1.3;
    color: var(--so-bw-ds-n900, var(--so-bw-text));
}
/* The running totals, set apart from the facts by a rule rather than by a
   heading — they are the same kind of thing, read last. Same `dl`/`dd` reset. */
#so-booking-widget[data-template="weekboard"] .so-bw-sumpanel__totals {
    display: flex;
    flex-direction: column;
    gap: 7px;
    margin: 0;
    padding-top: 14px;
    border-top: 1px solid var(--so-bw-ds-n100);
}
#so-booking-widget[data-template="weekboard"] .so-bw-sumpanel__total {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 12px;
    font-size: 13px;
    color: var(--so-bw-ds-n500, var(--so-bw-text-secondary));
    font-variant-numeric: tabular-nums;
}
#so-booking-widget[data-template="weekboard"] .so-bw-sumpanel__total dt,
#so-booking-widget[data-template="weekboard"] .so-bw-sumpanel__total dd {
    margin: 0;
}
#so-booking-widget[data-template="weekboard"] .so-bw-sumpanel__total:last-child {
    font-size: 15px;
    font-weight: 700;
    color: var(--so-bw-ds-n900, var(--so-bw-text));
}

/* "Almost there!" — the reassurance card under the rail on the details step.
   A panel, not a banner: it sits outside the summary box in the markup, so it
   needs its own frame or it reads as loose text under a card. */
#so-booking-widget[data-template="weekboard"] .so-bw-sumpanel__almost,
#so-booking-widget[data-template="weekboard"] .so-bw-sumpanel__assure {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    column-gap: 12px;
    align-items: start;
    padding: 14px;
    border: 1px solid color-mix(in srgb, var(--so-bw-primary) 25%, transparent);
    border-radius: var(--so-bw-ds-radius-card);
    background: color-mix(in srgb, var(--so-bw-primary) 6%, var(--so-bw-ds-n0));
}
#so-booking-widget[data-template="weekboard"] .so-bw-sumpanel__almosticon,
#so-booking-widget[data-template="weekboard"] .so-bw-sumpanel__assureicon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 34px;
    height: 34px;
    border-radius: 999px;
    background: color-mix(in srgb, var(--so-bw-primary) 14%, transparent);
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}
#so-booking-widget[data-template="weekboard"] .so-bw-sumpanel__almosttext,
#so-booking-widget[data-template="weekboard"] .so-bw-sumpanel__assuretext {
    font-size: 12.5px;
    line-height: 1.5;
    color: var(--so-bw-ds-n700, var(--so-bw-text-secondary));
}
#so-booking-widget[data-template="weekboard"] .so-bw-sumpanel__almosttext strong,
#so-booking-widget[data-template="weekboard"] .so-bw-sumpanel__assuretext strong {
    display: block;
    margin-bottom: 2px;
    font-size: 13.5px;
    font-weight: 700;
    color: var(--so-bw-ds-n900, var(--so-bw-text));
}

/* ── W10 · Two up, and ruled along the bottom ────────────────────────────
   Two corrections the rendered walk caught.

   TWO COLUMNS, not `auto-fill`. Auto-fill with a 320px floor gives three
   across a 1240px embed, and a third column is not a free win here: these
   cards carry a 56px disc, a name, a three-line description, two badges and a
   worded button, and at ~390px the description wraps to four lines while the
   button crowds the price. The design is a two-column comparison and the card
   is built for that width. It drops to one below 640px of container.

   ALIGNED BOTTOM RAILS. `align-content: start` packs the body's rows at the
   top, so a card with a two-line description ruled its badges 20px above its
   neighbour's — the one alignment a comparison grid cannot afford, since the
   prices are read ACROSS. Stretching the body to the card's height and
   spacing its rows apart pins the rail to the bottom on every card in the row. */
#so-booking-widget[data-template="weekboard"] .so-bw-step--service .so-bw-card-grid,
#so-booking-widget[data-template="weekboard"] .so-bw-step--staff .so-bw-card-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
}
    #so-booking-widget:where([data-cw~="640"])[data-template="weekboard"] .so-bw-step--service .so-bw-card-grid,
    #so-booking-widget:where([data-cw~="640"])[data-template="weekboard"] .so-bw-step--staff .so-bw-card-grid {
        grid-template-columns: minmax(0, 1fr);
    }
/* `stretch` (the default) rather than `start`, so the body is as tall as the
   card and has room to push its last row down. The disc is unaffected: it is
   a fixed 56px box, and a fixed height wins over a stretched track. */
#so-booking-widget[data-template="weekboard"] .so-bw-step--service .so-bw-card:not(.so-bw-sp__panel),
#so-booking-widget[data-template="weekboard"] .so-bw-step--staff .so-bw-card:not(.so-bw-sp__panel) {
    align-items: stretch;
}
#so-booking-widget[data-template="weekboard"] .so-bw-step--service .so-bw-card-media {
    align-self: start;
}
#so-booking-widget[data-template="weekboard"] .so-bw-step--staff .so-bw-card-icon--avatar {
    align-self: start;
}
#so-booking-widget[data-template="weekboard"] .so-bw-step--service .so-bw-card-body,
#so-booking-widget[data-template="weekboard"] .so-bw-step--staff .so-bw-card-body {
    align-content: space-between;
}

/* ── W11 · The "Any Available" banner ────────────────────────────────────
   W5 made it span the grid and lay its body out as a row; W7 then turned every
   card body into a grid for the bottom rail, which put this one back into a
   stack. It is the one card in the step that is NOT a comparison tile — it is a
   single offer with a single button — so it opts back out explicitly rather
   than relying on source order.

   The button is centred against the text block rather than pinned to its
   bottom: the banner is one line of offer, so there is no rail to rule to. */
#so-booking-widget[data-template="weekboard"] .so-bw-step--staff .so-bw-card-grid > .so-bw-card:first-child .so-bw-card-body {
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
    align-content: center;
    gap: 6px 16px;
}
#so-booking-widget[data-template="weekboard"] .so-bw-step--staff .so-bw-card-grid > .so-bw-card:first-child .so-bw-card-title {
    flex: 0 0 auto;
}
#so-booking-widget[data-template="weekboard"] .so-bw-step--staff .so-bw-card-grid > .so-bw-card:first-child .so-bw-card-meta {
    flex: 1 1 240px;
    min-width: 0;
}
#so-booking-widget[data-template="weekboard"] .so-bw-step--staff .so-bw-card-grid > .so-bw-card:first-child .so-bw-card-pick {
    flex: 0 0 auto;
    align-self: center;
    margin-left: auto;
}
#so-booking-widget[data-template="weekboard"] .so-bw-step--staff .so-bw-card-grid > .so-bw-card:first-child .so-bw-card-icon--avatar {
    align-self: center;
}

/* ── S9 · Corrections made against the live render ───────────────────────
   Everything below was written after loading the widget at 1600px on the test
   install and measuring it, rather than from the markup alone. Each rule names
   what it was measured against. */

/* A CENTRED, FIXED-MEASURE CARD — the width the whole redesign hangs off.

   This position was reached by trying the opposite first. An earlier pass made
   the card 80vw and broke it OUT of the host page's content column with a
   negative inline margin, because `min(100%, …)` in section 23.2 otherwise
   clamps to the theme wrapper (1100px on the test install) and a viewport-
   relative width does nothing inside a narrower column.

   It worked, and it was wrong. At 1600px the card rendered 1280px, and a
   two-column booking surface that wide stops reading as a widget: the summary
   sits so far from the list that it no longer looks attached to it, and the
   thing behaves like a full-bleed page section in a page it is supposed to be
   embedded in. `80vw` also means the design changes shape on every monitor,
   so there is no width at which it can actually be tuned.

   So: a measure, centred, inside the flow. No negative margins — the host
   page's own padding becomes the page margin, which is what gives it the room
   around it. The size radio still scales it, and `min(100%, …)` still keeps it
   inside a narrow column, so nothing needs a breakpoint. */
#so-booking-widget[data-template="wide"][data-template-id] .so-bw-container {
    /* ── THE ONE NUMBER ──────────────────────────────────────────────────
       920px, the tighter of the two measures the brief named, and the one
       kept. It was raised to 1040px to give the step column more room and
       then reverted by request: the wider card read as a page section rather
       than an embedded widget, and the extra room bought less than it looked
       like it would — the service description is clamped to one line with a
       Read more link either way, so widening moves where the ellipsis falls
       rather than removing it.

       For the record, the two states measured (Widget Size = Medium):
          920px  -> step column 572, summary 290
         1040px  -> step column 653, summary 329
       Large and Full are unaffected either way: Large is capped by the host
       page's content column before the ramp reaches it, and Full is 90vw.
       Everything downstream reads this one value, so nothing else needs
       touching to change it back.

       This REPLACED a 80vw breakout that pulled the card out of the host
       page's content column with a negative inline margin. That made the
       widget behave like a full-bleed page section, which is the opposite of
       what an embeddable booking widget should do: at 1600px it rendered
       1280px wide, and the two columns drifted so far apart that the summary
       had nothing to do with the list beside it.

       Back inside the flow, therefore: no negative margins, `margin-inline:
       auto` to centre, and the host page's own padding becomes the generous
       page margin. `min(100%, …)` keeps it inside a narrower column without a
       breakpoint — the cap is a ceiling, never a floor. */
    --so-bw-wide-w: 920px;
    width: auto;
    max-width: min(100%, calc(var(--so-bw-wide-w) * var(--so-bw-size-scale, 1)));
    margin-inline: auto;
}
/* "Full" means edge to edge, and section 23.2 already says so at higher
   specificity. Only the width token needs releasing — the centring margin is
   `auto`, which is a no-op once the card fills its column. */
#so-booking-widget[data-size="full"][data-template="wide"][data-template-id] .so-bw-container {
    --so-bw-wide-w: 100%;
}

/* THREE headings stacked, measured on the first render: the brand H1 ("Book an
   Appointment"), then `.so-bw-step-label` ("Choose a Service") beside its step
   count, then the section header inside the card ("Choose your service"). Two
   of those name the same step in almost the same words, one directly above the
   other.

   The STEP LABEL is the one that goes. It is the redundant copy — the section
   header sits with the icon and the description that belong to it, and the
   stepbar underneath already names every step including this one. The step
   COUNT stays: "Step 1 of 6" is the only thing on that line that is not
   repeated anywhere else, and it becomes the pill on the right. */
#so-booking-widget[data-template="wide"] .so-bw-step-label {
    display: none;
}
#so-booking-widget[data-template="wide"] .so-bw-step-indicator {
    justify-content: flex-end;
    margin-bottom: 2px;
}
/* A pill, so it reads as a status rather than as a stray line of text once the
   label beside it is gone. */
#so-booking-widget[data-template="wide"] .so-bw-step-count {
    padding: 5px 11px;
    border-radius: 999px;
    background: var(--so-bw-ds-n50);
    color: var(--so-bw-ds-n700, var(--so-bw-text-secondary));
    font-size: 11.5px;
    font-weight: 600;
}

/* The stepbar dots measured 20px, which is what section K set them to when this
   template's header was denser. They carry a numeral and a tick, and at 20px
   the numeral is 10px — smaller than the label under it. 28px gives the glyph
   room and makes the row the primary progress indicator it is meant to be. */
#so-booking-widget[data-template="wide"] .so-bw-stepbar-dot {
    width: 28px;
    height: 28px;
    font-size: 12px;
    font-weight: 700;
}
#so-booking-widget[data-template="wide"] .so-bw-stepbar-label {
    font-size: 12px;
    font-weight: 600;
}

/* Duration and price were rendering as the shared badges: two filled pills in
   IBM Plex Mono, the price on a 15% accent wash. On a comparison table that is
   two more boxes per row competing with the row itself, and the measurement
   showed them reading as controls rather than as facts.

   Plain text instead — the duration quiet, the price the accent and the heaviest
   thing on the row after the name. Tabular figures so a column of prices aligns
   on its digits, which is the entire reason to put them in a table. */
#so-booking-widget[data-template="wide"] .so-bw-step--service .so-bw-card-badges {
    gap: 14px;
    align-items: center;
}
#so-booking-widget[data-template="wide"] .so-bw-step--service .so-bw-badge {
    padding: 0;
    border: 0;
    border-radius: 0;
    background: none;
    font-family: inherit;
    font-variant-numeric: tabular-nums;
}
#so-booking-widget[data-template="wide"] .so-bw-step--service .so-bw-badge--duration {
    font-size: 12.5px;
    font-weight: 500;
    color: var(--so-bw-ds-n500, var(--so-bw-text-secondary));
}
#so-booking-widget[data-template="wide"] .so-bw-step--service .so-bw-badge--price {
    font-size: 13.5px;
    font-weight: 700;
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}
#so-booking-widget[data-template="wide"] .so-bw-step--service .so-bw-badge--free {
    font-size: 12.5px;
    font-weight: 600;
    color: #067647;
}

/* == W12 · The details step as a panel ================================== */
/* The form and the summary rail read as one pair only if both are framed.
   The rail got its frame in W6; without this the form is loose fields on the
   widget field beside a bordered box, and the pair looks unfinished.

   Scoped to `.so-bw-step--details` (added to the markup alongside this) rather
   than to `.so-bw-form`, because the same form primitive is used by the
   custom-fields step, which renders its own submit and is not a two-pane
   screen. */
#so-booking-widget[data-template="weekboard"] .so-bw-step--details {
    padding: 20px;
    border: 1px solid var(--so-bw-border, var(--so-bw-ds-n200));
    border-radius: var(--so-bw-ds-radius-card);
    background: var(--so-bw-ds-n0);
}

/* ── S10 · The progress bar is narrower than the card ────────────────────
   The step bar used to span the container's full width. At 80vw that is 1280px
   of track for five or six dots — each connector ran ~205px — so the row read
   as a rule drawn across the top of the card with numbers sitting on it, rather
   than as a compact progress indicator.

   Capped and centred instead. `min(100%, 760px)` so it still fits a narrow
   embed without needing a second breakpoint: the cap is a ceiling, never a
   floor, which is the same shape the container width uses.

   The leading-connector fix that used to live here is GONE, deliberately. It
   turned out to be a bug in the shared base rule (~7688) — Alpine's `x-for`
   leaves its `<template>` as the real first child, so `:not(:first-child)`
   matched the first ITEM and drew a connector back to a dot that does not
   exist. Corrected at that rule with `:first-of-type` instead, so all
   twenty-two architectures lose the stray line rather than just this one. */
#so-booking-widget[data-template="wide"] .so-bw-stepbar {
    max-width: min(100%, 760px);
    margin-inline: auto;
}

/* ==========================================================================
   S11 · THE COMPACT PASS
   ==========================================================================
   Everything here exists to make the widget feel EMBEDDED. The redesign above
   got the structure right and the density wrong: at 80vw with page-sized
   padding it read as a landing page that happened to contain a booking form.
   This section tightens the measure (§S1), the column split, the
   vertical rhythm and the type scale, and it replaces the footer with a real
   action bar.

   Numbers, so they can be argued with rather than guessed at again:
     column split      66 / 34, gap 18px
     card radius       13px  (12-14 band)
     panel radius      14px
     borders           #ECEAE6 / #DCD8D2 — warm neutrals, not blue-grey
     main heading      21px / 600
     section title     17px / 600
     service title     15.5px / 600
     supporting text   13px, muted
     price             15px / 700
   ========================================================================== */

/* ── S11.1 · The two columns, 66 / 34 ───────────────────────────────────
   Percentages rather than a fixed 320px panel. At a fixed measure the panel
   was 35% at 920px and would have been 25% at 1280 — the balance drifted with
   the width, so it could never be tuned. Basis percentages hold the ratio at
   any width the card is allowed to take.

   `calc(… - gap)` on both so the two bases plus the gap sum to exactly 100%.
   Without it the pair overflows and wraps, which is the classic two-column
   flex bug: 66% + 34% + 18px is more than the container. */
#so-booking-widget[data-template="wide"] .so-bw-body {
    gap: 16px;
}
/* ONE number states the split: the panel's basis. The step takes whatever is
   left, so the two can never fail to sum to the container.

   The first version gave BOTH a percentage basis minus half the gap —
   `calc(66% - 8px)` and `calc(34% - 8px)`. That is arithmetic that has to stay
   correct as the gap changes, and it was already slightly wrong in practice:
   `.so-bw-body` is a scroll container, so its scrollbar takes a few pixels off
   the box the percentages resolve against, and the pair measured 64 / 32.5
   instead of 66 / 34 — outside the 65-68 the brief asks for.
   `flex: 1 1 0` on the step is self-correcting: whatever the panel and the gap
   actually take, the step gets the remainder. Measured after: 65 / 33. */
#so-booking-widget[data-template="wide"] .so-bw-step-host {
    flex: 1 1 0;
    min-width: 0;
}
#so-booking-widget[data-template="wide"] .so-bw-sumpanel {
    flex: 0 0 33%;
    padding: 16px;
    border: 1px solid #ECEAE6;
    border-radius: var(--so-bw-r-14);
    box-shadow: 0 1px 2px rgba(16, 18, 26, .04);
    gap: 12px;
}

/* ══════════════════════════════════════════════════════════════════════════
   S12 · TWO FRAMED COLUMNS, 70 / REMAINDER
   ══════════════════════════════════════════════════════════════════════════
   The split becomes explicit and both columns get a frame, so the step and its
   summary read as a matched pair rather than a list next to a floating card.

   THE BASIS IS ON THE LIST, NOT ON BOTH. 70% on the step column with the
   summary left flexible (`flex: 1 1 0`) means the pair can never fail to sum to
   the container: whatever the gap and the scrollbar actually take, the summary
   absorbs it. The earlier version of this split gave BOTH a percentage
   (`calc(66% - 8px)` / `calc(34% - 8px)`) and drifted — measured 64 / 32.5,
   because a scroll container's bar comes out of the box the percentages resolve
   against. One basis and one flexible sibling cannot drift.

   BOTH BASES ARE FIXED, AND THE WIDTH IS THE REASON. 70% on the list and
   `30% - gap` on the summary sum to exactly the content box, so the columns hold
   the same width on every step and through every selection.

   The list was briefly FLEXIBLE (`flex: 1 1 0`) so that it could reclaim the
   summary's share on the steps that render no summary (`summaryPanelSteps`
   decides which do). That reclaim was the bug, not the feature: the summary
   appears when a service is selected and disappears again between steps, so the
   list re-sized under the customer's cursor as they picked and advanced — the
   layout moving on its own. A constant measure is worth more here than the
   recovered space, and the width now changes for exactly one reason: the admin
   changing Widget Size in Customize.

   The cost, stated rather than hidden: on the steps with no summary the right
   30% is empty. The alternative is to render the panel on every step, which is a
   markup and content decision (what would it say on the Details step?), not a
   CSS one.

   `justify-content: space-between` is declared because the design calls for it,
   and it is INERT here — the two bases plus the gap already fill the row, so
   there is no free space left to distribute. Recorded so nobody later "fixes" a
   rule that is doing nothing. */
/* No outer frame on the card. The two columns below carry their own borders, so
   an outer one makes three nested frames — the box-in-box-in-box the design
   system calls out as visual noise. The shadow stays: it still separates the card
   from the page, which is what the border was doing, without drawing a line
   around a thing that already contains two bordered boxes.
   `editorial`, `accordion`, `frost`, `hero` and `dialog` opt out the same way,
   so this follows an established pattern rather than inventing one. */
#so-booking-widget[data-template="wide"] .so-bw-container {
    border: none;
}

#so-booking-widget[data-template="wide"] .so-bw-body {
    justify-content: space-between;
    /* One token for the column gap, because the panel's basis has to subtract
       exactly this to leave the list a true 70%. Two literals would drift. */
    --so-bw-wide-colgap: 16px;
    gap: var(--so-bw-wide-colgap);
}
#so-booking-widget[data-template="wide"] .so-bw-step-host {
    /* `0 0`, so the column neither grows into a missing summary nor shrinks to
       fit long content. An earlier `1 1 auto` here did both and was wrong twice
       over: `auto` took the base size from the column's own content, which on the
       service step exceeded the row and made `flex-wrap` drop the summary onto a
       second line (measured list 99.1% AND panel 27.9%, on step 1 but not step 4,
       because the content differs per step); and growing is what made the width
       move between steps. */
    flex: 0 0 70%;
    /* Not optional: a flex item's automatic minimum size is its content, so
       without this a long service name refuses to shrink and shoves the summary
       off the right edge instead of wrapping. */
    min-width: 0;
    /* The frame, in the same vocabulary the summary panel above already uses —
       same hairline, same radius, same one-pixel shadow. Matching it is the
       point: two different frames beside each other read as two unrelated
       widgets. */
    box-sizing: border-box;
    /* 12px at the top, 16px elsewhere: the heading sits directly under the
       stepper, so the frame needs less air there than beside a card's edge.
       Together with the body's trim above this takes the header-to-heading gap
       from 33px to 21px. */
    padding: 12px 16px 16px;
    border: 1px solid #ECEAE6;
    border-radius: var(--so-bw-r-14);
    /* Fallback chain, not a bare token. An undefined custom property makes the
       whole declaration invalid at computed-value time and the panel renders
       TRANSPARENT — which looks exactly like a stacking bug and has already
       cost this file a long diagnosis once. */
    background: var(--so-bw-ds-n0, var(--so-bw-bg, #fff));
    box-shadow: 0 1px 2px rgba(16, 18, 26, .04);
}
/* The summary carries the basis: 30% less the gap, so the list's 70% and this
   sum to exactly the content box. `0 0` so neither grows nor shrinks. */
#so-booking-widget[data-template="wide"] .so-bw-sumpanel {
    flex: 0 0 calc(30% - var(--so-bw-wide-colgap));
    /* THE PANEL MUST NOT BE WIDENED BY ITS OWN CONTENT. A flex item's automatic
       minimum size is its content, so `min-width: auto` lets a long service name
       push the panel past the basis above — and with both columns set not to
       shrink, the pair then exceeds the row and `flex-wrap` drops the summary onto
       a second line. It does not happen with this catalogue, which is precisely
       why it needs pinning: it would arrive with the first long service name a
       customer happens to have. */
    min-width: 0;
    /* Reserve this panel's own scrollbar track, always.
       The panel is a scroll port (`max-height: 100%; overflow-y: auto`, set at its
       base rule as the guard against a sticky box taller than its viewport). Its
       content GROWS the moment a service is selected — a service row, totals, the
       next-steps note — so on any platform with classic scrollbars the bar appears
       at that moment and takes ~15px out of the panel's content box, re-wrapping
       everything inside it as you click. That is the "width changes while
       selecting" report: the columns hold (measured constant), the content inside
       the panel does not.
       Not reproducible in headless Chromium, which uses overlay scrollbars that
       take no space — hence a fix reasoned from the mechanism rather than from a
       screenshot. `stable` reserves the track whether or not the bar is showing,
       which is the same technique the body already uses for the same reason. */
    scrollbar-gutter: stable;
}
/* ══ The layout follows the CUSTOMIZE WIDTH, declaratively ══════════════
   Widget Size in Customize IS the width control — there is no separate width
   option — and it scales the card: Small 736, Medium 920, Large 1100, Full 90vw
   (all measured). Two columns are right for the last three. They are not right
   for Small: 736px leaves the summary a 229px column, too narrow for the service
   name, duration, price and totals it has to hold, so Small stacks instead.

   GATED ON `[data-size]`, NOT ON A MEASUREMENT, and that distinction is the
   whole point. The same behaviour was first built by measuring the card in
   _wireContainerWidth() so `data-cw` would follow the size. That closed a loop:
   the card's width is itself a product of the stamped attributes (the phone tier
   changes its cap, padding and radius), so one transient narrow reading latched
   and a 1728px card rendered the stacked phone layout — in the field only, never
   headless, which is what a timing-dependent latch looks like. `data-size` is
   written by PHP from the saved setting; nothing here can feed back into it.

   These beat the two ungated bases above on specificity (1,3,0 against 1,2,0)
   as well as on order, so they do not depend on staying below them.

   Genuine narrow viewports are a SEPARATE axis and still handled by the
   `[data-cw~="768"]` tiers further down: a Medium card in a 700px column stacks
   too, because there the box really is narrow. Size answers "how wide did the
   admin ask for", `data-cw` answers "how much room did the page actually give
   it", and the widget needs both. */
#so-booking-widget[data-size="small"][data-template="wide"] .so-bw-step-host,
#so-booking-widget[data-size="small"][data-template="wide"] .so-bw-sumpanel {
    flex: 0 0 100%;
}
/* The panel stops being a column: it releases the sticky pin and its own scroll
   port (both only make sense beside a scrolling list) and reads AFTER the list it
   summarises. Mirrors what the `[data-cw]` stacking tier does, for the same
   reason. */
#so-booking-widget[data-size="small"][data-template="wide"] .so-bw-sumpanel {
    position: static;
    max-height: none;
    overflow-y: visible;
    order: 2;
}

/* ── Full Width is FULL SCREEN: 100vw by 100vh ────────────────────────────
   Paired with the `max-width: 100vw` on the alignfull root, Full Width now means
   the whole viewport on both axes rather than the widest of four cards.

   `calc(100dvh - 112px)`, not `100dvh`, and the 112 is NOT a new number — it is
   the shell's own constant, documented at its rule: the embed route puts a 16px
   body gutter and a ~56px identity banner (plus its margin) above the card, ~102px
   the card never gets, with 10px of slack. Asking for a literal 100dvh would
   overflow by exactly that chrome and put a page scrollbar beside a widget that is
   already scrolling internally — which is the bug that constant was introduced to
   fix. Measured before this change: page scrollHeight 914 against a 900px
   viewport, and that was with the 760px cap still in force.

   `dvh` not `vh`: on a phone a `vh` bound measures the URL-bar-collapsed
   viewport, so the card overhangs the screen until you scroll.

   `height` as well as `max-height`, because a cap alone does not fill: the
   container is a flex column whose body is `flex: 1 1 auto`, so without a
   definite height the card stays content-tall and simply stops growing at the
   ceiling. With one, the body takes the slack and the LIST gets the extra room —
   which is the point of asking for full height.

   The floor is 180px, DOWN from 360px, and that is a bug fix rather than a taste
   change. A floor above the viewport forces the page to scroll — at a reported
   1496x312 viewport the 360px card made a 482px page — and that page scrollbar was
   what exposed the `100vw` mismatch above, so the card's width moved every time
   the bar toggled. At Full Width the card IS the viewport height; a floor that
   overrides that defeats the point. 180px still keeps it a card rather than a
   sliver, and the body scrolls internally so nothing becomes unreachable.

   `--so-bw-vchrome` lets a SURFACE correct the guess. 112px is the general
   case and stays the fallback, because a customer's page has a theme header
   and footer this file cannot measure. The embed route knows exactly what it
   put above the card, so it publishes the real number (88px — the identity
   banner plus slack, with its body gutter dropped at this size) and the card
   reclaims the ~42px the conservative default was leaving empty.
   Over-subtracting costs a band of unused space; under-subtracting costs a
   second scrollbar, so the fallback errs high on purpose.

   `!important` on `max-height` only: the shell's bound is itself `!important`, so
   nothing weaker can raise the 760px ceiling. `height` has no important rule
   competing for it. */
#so-booking-widget[data-size="full"][data-template="wide"] .so-bw-container {
    height: max(180px, calc(100dvh - var(--so-bw-vchrome, 112px)));
    max-height: max(180px, calc(100dvh - var(--so-bw-vchrome, 112px))) !important;
    /* Release the shell's `min-height: clamp(360px, 52vh, 560px)`. That floor is
       right for a card sitting IN a page - a two-card step should still read as a
       card - but at Full Width the height IS the viewport, and a 360px floor
       simply overrides that on any short viewport: at 1496x312 it made a 360px
       card in a 312px window, so the page scrolled, and that scrollbar is what
       exposed the `100vw` mismatch fixed above. Measured `height: 360px` computed
       while this rule was asking for 224.
       Safe at `0` because `height` above carries its own 180px floor, so the card
       can never actually collapse. */
    min-height: 0;
}


/* ── S11.2 · Vertical air comes out ─────────────────────────────────────
   The header carried page-sized padding and the step bar sat in a band of its
   own with the content a long way below it. The brief asks for the stepper
   close to the content, and every pixel removed here is a pixel of scrolling
   removed on a phone. */
/* ONE horizontal inset for the whole card, declared once as the gutter token
   and consumed by the header, the body and the nav.

   These three carried literal `20px` x-padding at first, and
   widget-css-cascade.test.ts rejects that by design: an architecture may vary
   VERTICAL padding freely, but a literal horizontal value re-opens the
   "stair-step", where the header, the step content and the action bar each sit
   on a slightly different left edge. Routing all three through the token means
   they cannot drift, and this one line is the only place the number lives. */
#so-booking-widget[data-template="wide"] .so-bw-container {
    --so-bw-gutter: 20px;
    /* The width of the body's scrollbar. ONE value, because that is what the
       file actually resolves to: there are three `.so-bw-body::-webkit-scrollbar`
       width rules — 8px gated at the 768 tier, then 5px, then an UNGATED 8px —
       and the last one wins everywhere. The first version of this token mirrored
       the ramp those rules look like they describe (5px, 8px from 768 up), and it
       was wrong at Small: the token said 5 while the bar drew 8, so the
       correction below under-compensated by 3px and the stacked summary panel
       missed the Continue button by that much (measured).

       The token exists because the scrollbar comes out of the body's CONTENT
       box, so the scrolling column and the static header/nav ended up on two
       different right edges — R-29 against R-21. Anything cancelling the bar
       reads it here rather than restating the number. If those pseudo-element
       rules are ever untangled into a real ramp, this is the one place that has
       to learn about it. */
    --so-bw-sbw: 8px;
    /* ── The lead: what sits left of a line of text ──────────────────────
       The section heading and the service cards both put a rounded tile, then a
       gap, then their text — and both stated their own numbers. The heading ran
       38px icon + 12px gap from the rail (text at L+71); the cards ran 16px
       padding + 42px avatar + 14px gap (text at L+93). So the column had two
       text rails 23px apart, purely because the same measurement was written
       twice.

       One set of numbers, read by both, which is why this is a fix and not a
       tuning: they cannot drift apart again. 16 + 42 + 14 = 72 either way, so
       the text lands on L+93 and — because the tile now matches the avatar —
       the tile's own left edge lands on the avatars' L+37 too. Both rails, not
       just the text one.

       The icon grows 38 -> 42 and that row gains ~4px. That is the price of the
       alignment; the alternative keeps the smaller tile and aligns the text only
       (padding 20 + icon 38 + gap 14), leaving the tile 6px off its avatars. */
    --so-bw-wide-leadpad: 16px;
    --so-bw-wide-leadsize: 42px;
    --so-bw-wide-leadgap: 14px;
}
#so-booking-widget[data-template="wide"] .so-bw-header {
    padding: 16px var(--so-bw-gutter) 12px;
}
#so-booking-widget[data-template="wide"] .so-bw-brand {
    margin-bottom: 10px;
}
/* Main heading — 21px semibold. */
#so-booking-widget[data-template="wide"] .so-bw-brand__title {
    font-size: 21px;
    font-weight: 600;
    line-height: 1.25;
    letter-spacing: -0.015em;
}
#so-booking-widget[data-template="wide"] .so-bw-brand__subtitle {
    font-size: 13.5px;
    margin-top: 2px;
    color: var(--so-bw-ds-n500, var(--so-bw-text-secondary));
}
/* The step bar loses its own vertical band and tucks under the heading. 560px
   because at 920px the bar was 760px of track for six dots — the connectors
   were longer than the labels they joined. */
#so-booking-widget[data-template="wide"] .so-bw-stepbar {
    max-width: min(100%, 560px);
    margin-inline: auto;
    margin-top: 4px;
    margin-bottom: 0;
}
#so-booking-widget[data-template="wide"] .so-bw-stepbar-dot {
    width: 24px;
    height: 24px;
    font-size: 11.5px;
    font-weight: 700;
}
#so-booking-widget[data-template="wide"] .so-bw-stepbar-label {
    font-size: 11.5px;
    font-weight: 500;
}
/* The step count sits on the heading's line rather than on a line of its own. */
#so-booking-widget[data-template="wide"] .so-bw-step-indicator {
    margin-bottom: 0;
}
#so-booking-widget[data-template="wide"] .so-bw-step-count {
    padding: 4px 10px;
    font-size: 11px;
}
#so-booking-widget[data-template="wide"] .so-bw-body {
    /* Top inset trimmed to 8px, bottom left at 16px. The gap between the stepper
       and the section heading was 33px of stacked padding - this 16px plus the
       list frame's own 16px plus its border - which read as a band of empty
       space between the two things that orient the customer. Asymmetric on
       purpose: the space ABOVE the framed columns is shared with the header's own
       padding and its rule, while the space below them has nothing else in it.
       The x-inset stays on the gutter token, which is what the cascade suite
       pins. */
    padding: 8px var(--so-bw-gutter) 16px;
}
/* ── The scrolling column and the static chrome share one right edge ─────
   `.so-bw-body` is the scroll container, and a classic scrollbar is taken out of
   its CONTENT box — so the list, the cards and the summary panel all ended at
   R-29 while the header's step pill and the Continue button sat at R-21. The
   visible tell was the panel's right edge missing the button's by 8px, at every
   width and on every step.

   Two halves, and both are needed:

   `scrollbar-gutter: stable` reserves the track whether or not it is in use.
   Without it the correction below is right only while the list is long enough to
   scroll, and a short catalogue overshoots to R-13 — the same error mirrored. It
   also stops the list reflowing at the moment a search filters it down past the
   scrolling threshold.

   `padding-right` then gives back exactly what the bar took, so the content box
   lands on the same rail as the chrome and the bar itself sits inside the
   gutter rather than eating into content. Longhand on purpose: the cascade suite
   rejects a literal x-slot in a `padding` shorthand here, and the gutter stays
   the single source of the inset either way. */
#so-booking-widget[data-template="wide"] .so-bw-body {
    scrollbar-gutter: stable;
    padding-right: calc(var(--so-bw-gutter) - var(--so-bw-sbw));
}
#so-booking-widget[data-template="wide"] .so-bw-step {
    padding: 0;
}

/* ── S11.3 · Section header, with the search beside it ──────────────────
   The search was a full-width bar on its own line under the header — a 575px
   input to filter a handful of services, and a whole line of height for it.
   It moves onto the header's own line, capped at 220px.

   Third column in the header grid, spanning both its rows, so it is centred
   against the title/description pair rather than sitting on one of them. */
/* The heading keeps its OWN rail, by request.

   It was briefly brought onto the cards' text rail — tile sized to the avatar
   and a left inset matching the card's padding, which put both tiles on L+38 and
   both text runs on L+94. Reverted: the smaller tile and the tighter gap read
   better here, and a section heading sitting outside the cards is entitled to a
   different rail from the rows beneath it.

   The lead tokens stay declared on the container and the CARDS still read them,
   so the card side has one source of truth. This header simply does not
   participate. If it is ever aligned again, the numbers to match are
   `--so-bw-wide-leadpad + 1px` of left padding (the +1 is the card's border,
   which a card measures its padding from and this box has none of),
   `--so-bw-wide-leadsize` for the tile and `--so-bw-wide-leadgap` for the gap. */
#so-booking-widget[data-template="wide"] .so-bw-svchead {
    grid-template-columns: auto minmax(0, 1fr) auto;
    column-gap: 12px;
    row-gap: 2px;
    margin: 0 0 12px;
}
#so-booking-widget[data-template="wide"] .so-bw-svchead__icon {
    width: 38px;
    height: 38px;
    border-radius: var(--so-bw-r-11);
}
/* Section title — 17px semibold. */
#so-booking-widget[data-template="wide"] .so-bw-svchead__title {
    font-size: 17px;
    font-weight: 600;
    letter-spacing: -0.01em;
}
/* The step description, walked down on request: 13px originally, then 12.5px,
   then 10px, now 9px. Far below the 13-14px band the original brief named, and
   that is the decision — the heading beside it carries the meaning and this
   line is deliberately fine print.

   `line-height` goes UP as the size comes down, which looks backwards and is
   not. Small type needs proportionally MORE leading: holding the old 1.4 ratio
   at 9px would set two lines 12.6px apart and fuse the sentence into a grey
   smear. 1.6 keeps the gap between lines near where the eye expects it, and
   matters at narrow container widths where this copy still wraps even though it
   fits one line on desktop.

   NOTE FOR WHOEVER TOUCHES THIS NEXT: 9px is below every accessible-minimum
   this file otherwise respects, and the copy is not decorative — it is the only
   place the widget explains that multiple services get booked back-to-back in
   the order they are picked. If that sentence matters, the fix is to SHORTEN it
   and restore ~12px, not to keep shrinking. Do not treat this value as a
   precedent for other supporting text. */
/* `!important` IS REQUIRED, and it is not specificity laziness — measured with
   CDP `CSS.getMatchedStylesForNode` against a real page. Block themes ship

     .entry-content.alignfull.wp-block-post-content.has-global-padding
     .is-layout-constrained.wp-block-post-content-is-layout-constrained p
     { font-size: 18px !important }

   which lands on EVERY `<p>` the widget renders inside post content. An author
   `!important` beats any non-important author declaration regardless of
   specificity, so an ID-anchored rule at (1,3,0) still loses. The symptom is
   deceptive: `line-height` here applied fine (nothing competes for it) while
   four successive `font-size` values were silently discarded, so the element
   looked stuck rather than overridden.

   It only fires when the content wrapper carries `.alignfull` — i.e. at Widget
   Size = Full Width — which is why the same build renders correctly at
   `medium` and wrong at `full` on the same theme.

   `bubble`, `compact` and `minimal` already declare this element `!important`
   for the same reason; this is the established idiom in this file, not a new
   escape hatch. */
#so-booking-widget[data-template="wide"] .so-bw-svchead .so-bw-step-desc {
    font-size: 10px !important;
    line-height: 1.55;
}
/* The same size, restated WITHOUT the `.so-bw-svchead` ancestor.
   The rule above only matches when the paragraph sits inside the section
   header, which is where the service, staff and details steps put it — but
   several steps (add-ons, date & time, review) emit `.so-bw-step-desc` as a
   direct child of the step instead, and those kept the shared 14px base while
   the three header steps shrank. That inconsistency is not a design decision,
   it is a selector that assumed one markup shape.
   Lower specificity than the rule above (1,2,0 vs 1,3,0) and placed after it,
   so the header variant still wins on ties and both land on 10px anyway. */
#so-booking-widget[data-template="wide"] .so-bw-step-desc {
    font-size: 10px !important;
    line-height: 1.55;
}
/* The search sits on the header's line, and it is PLACED there rather than
   dragged there. `order` cannot do it — the search is a SIBLING of the header,
   not a child of it — so the step itself becomes the grid the two share: the
   header takes column 1, the search column 2, and everything else spans both.

   The first version of this used `margin-top: -50px` to lift the search onto
   the row above. That is a guess about the header's rendered height dressed up
   as a rule: change the title's size or let the description wrap to two lines
   and the input drifts off its row or lands on top of the text. Grid placement
   cannot drift, because it never had a number in it. */
#so-booking-widget[data-template="wide"] .so-bw-step--service {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    align-content: start;
    /* The search column's width. Read by the search itself and by the sticky
       band that must cover its column — see S11.4. */
    --so-bw-wide-searchw: 220px;
    /* The reserved width of every card's Select button. Sized for the LONGER
       state ("✓ Selected" measures 106px) so the shorter one cannot shrink the
       track — see the card-body grid note. */
    --so-bw-wide-pickw: 108px;
}
/* Everything that is not the header or the search is full width. Stated as a
   default so a block added to this step later inherits the right behaviour
   instead of being auto-placed into the search's column. */
#so-booking-widget[data-template="wide"] .so-bw-step--service > * {
    grid-column: 1 / -1;
}
#so-booking-widget[data-template="wide"] .so-bw-step--service > .so-bw-svchead {
    grid-column: 1;
    grid-row: 1;
}
#so-booking-widget[data-template="wide"] .so-bw-step--service > .so-bw-search {
    grid-column: 2;
    grid-row: 1;
    align-self: center;
    max-width: var(--so-bw-wide-searchw);
    width: var(--so-bw-wide-searchw);
    height: 34px;
    margin: 0 0 12px;
    border-radius: var(--so-bw-r-9);
    border-color: #DCD8D2;
    /* IN FLOW, against the shell's default of `position: sticky` for this
       element (set for container widths >=768, and asserted by a test).

       The shell's rule assumes the search spans the scroll region's width, which
       it does in the single-column architectures: pinned, it covers the list and
       cards pass cleanly beneath it. Here it is a 220px control sharing a grid
       row with the section heading, so pinning it left a floating pill over the
       list with the cards' prices and Select buttons showing AROUND it, and the
       heading it belongs to scrolling away without it.
       The header row now behaves as one thing: it scrolls with the list. The
       test's invariant was amended to permit a per-template exception rather
       than deleted — it still holds for every architecture that does not say
       otherwise.

       `relative`, NOT `static`. `.so-bw-search-icon` is absolutely positioned
       inside this box, so `static` takes away its containing block and it
       reparents to the nearest positioned ancestor — the magnifier escaped to
       the top-left corner of the card (seen in the render). `relative` is in
       flow exactly like `static` while still anchoring the icon. */
    position: relative;
    /* The shell's sticky rule also gives this a `z-index: 4`, which it needed to
       sit above the list it was pinned over. In flow it has nothing to rise
       above, and 4 ties with the sticky page header — a tie the later DOM node
       wins, so the search floated over the pinned stepper (seen in the render).
       `auto` puts it back in the normal paint order, under the header. */
    z-index: auto;
}
#so-booking-widget[data-template="wide"] .so-bw-step--service .so-bw-search-input {
    font-size: 13px;
}

/* ── S11.4 · The header row scrolls WITH the list ──────────────────
   This section used to pin the heading and the search to the top of the scroll
   region, with an opaque band behind them, a pseudo-element extending that band
   across the search's column, and a stacking context on the card grid to stop
   selected cards painting through it. All of it is GONE by request: the header
   scrolls away with the list and `.so-bw-body` is the only scroll region again.

   Kept as a note because the removal is not free, and whoever restores it
   should know what it cost to get right the first time:
     - sticky offsets resolve against the scrollport's PADDING box, so a pinned
       band needs a NEGATIVE `top` equal to that padding or content scrolls
       visibly above it;
     - `.so-bw-search` is a border-box with an explicit height, so it cannot
       carry the band's padding — its input ends up taller than its own pill;
     - a card is `position: relative` with `z-index: auto`, which paints in the
       same pass as its positioned siblings, so `z-index: 3` on the band is NOT
       enough — the list needs its own stacking context;
     - the grid's missing `column-gap` is load-bearing: with one, a transparent
       strip opens between heading and search for cards to show through.

   `.so-bw-search` keeps whatever the shared rule gives it (sticky at container
   widths ≥768, which predates this work and is asserted by a test), so nothing
   here restates its position. */
/* Chips below the search, above the list. */
#so-booking-widget[data-template="wide"] .so-bw-catbar {
    margin: 0 0 12px;
    gap: 6px;
}
#so-booking-widget[data-template="wide"] .so-bw-catchip {
    min-height: 30px;
    padding: 0 10px;
    border-color: #DCD8D2;
    border-radius: var(--so-bw-r-8);
    font-size: 12px;
}
/* The wrapper's own cap fights the panel beside it: the list should run as long
   as it needs and let the body scroll, so the two columns end together instead
   of the list scrolling inside a short window.

   The inline padding and margin go with it, and that is an ALIGNMENT fix. The
   shared rule pairs `padding: 4px 12px 6px 8px` with `margin-left/right: -8px`,
   so the left nets zero (-8 + 8) while the right nets +4 (-8 + 12): every card
   ended 4px short of the column holding it, flush on the left and inset on the
   right, and the wrapper itself hung 8px outside the rail (L+13 against L+21).
   The 12px was reserving room for a scrollbar — which this wrapper does not
   have, because the line above hands scrolling back to the body.

   BOTH class names, because the shared rule lists both and the steps use them
   interchangeably: the service step wraps its list in `.so-bw-scroll-wrap`,
   while the location and staff steps put `.so-bw-card-grid--scroll` on the grid
   itself. Fixing only the wrapper left those two steps still at L+13 / R-29
   (measured), which is the whole defect surviving under a second name.

   Widened from the service step to the whole template — the staff step showed
   the same overhang. Still scoped to Wide Canvas: on the other architectures
   these may genuinely scroll, where the reservation is doing its job and
   removing it needs its own visual pass. */
#so-booking-widget[data-template="wide"] .so-bw-scroll-wrap,
#so-booking-widget[data-template="wide"] .so-bw-card-grid--scroll {
    max-height: none;
    overflow: visible;
    padding-inline: 0;
    margin-inline: 0;
}

/* ── S11.4 · The summary panel, compact ─────────────────────────────────
   Type down a step, spacing down a step, and a real empty state — the panel
   used to open with one line of grey text, which reads as a placeholder rather
   than as a card that is waiting for you. */
#so-booking-widget[data-template="wide"] .so-bw-sumpanel__title {
    font-size: 15px;
    font-weight: 600;
}
#so-booking-widget[data-template="wide"] .so-bw-sumpanel__empty {
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    gap: 4px;
    padding: 18px 4px 14px;
    margin: 0;
}
#so-booking-widget[data-template="wide"] .so-bw-sumpanel__emptyicon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    margin-bottom: 4px;
    border-radius: 50%;
    background: var(--so-bw-ds-n50);
    color: var(--so-bw-ds-n500);
}

/* ── 5 · …and it is welded to the bottom of the shell ────────────────────
   `.so-bw-nav` was `position: sticky; bottom: 0` (REVIEW STEP, ~13290) and
   that rule's own comment states the intent plainly: nothing between the nav
   and the page establishes a scroll container, so it sticks to the PAGE.

   That was right when the widget WAS a page — a card that grew to its content
   and pushed Confirm below the fold. It is wrong now, and rule 4 above already
   says why: the nav sits outside the scroll region, so it has nothing left to
   rescue. What page-sticky does instead is take the bar off the shell. Sticky
   pins it to the bottom of the VIEWPORT, so for as long as the shell's bottom
   edge is below the fold — which is every page with anything above the widget,
   and every page until you have scrolled past the card — the bar floats UP
   inside the card, slices whatever row it lands on, and leaves the shell's
   real bottom edge stranded below it. Scroll the page and it slides back down,
   which is what "the border moves when I scroll" is.

   Accordion is where that reads as a broken card rather than a floating bar,
   because it is the one architecture that paints the shell on its children
   (section 14 C) and the NAV carries the bottom border, both bottom corners
   and the drop shadow. When the bar lifts, the card's bottom edge lifts with
   it and the body's two side borders run on underneath to nowhere.

   Welded to the shell the bar is permanently visible anyway — a bounded pane
   with an internal scroller gives you that for free. `relative`, not `static`,
   so the z-index above it (6) still resolves and the bar keeps painting over
   anything the body lets touch it. */
#so-booking-widget[data-template] .so-bw-nav,
#so-booking-widget .so-bw-nav {
    position: relative;
}

/* The one case the bound cannot cover, and the reason the sticky is kept
   rather than deleted. The container's floor is `min-height: clamp(360px,
   52vh, 560px)` and its ceiling `calc(100dvh - 112px)`; below ~472px of
   viewport height the floor is the taller of the two, the shell genuinely
   overflows the window, and the PAGE scrolls again. There the bar has
   something to rescue and the shell's bottom edge is off-screen regardless.
   Landscape phones, mostly. */
@media (max-height: 500px) {
    #so-booking-widget[data-template] .so-bw-nav,
    #so-booking-widget .so-bw-nav {
        position: sticky;
        bottom: 0;
    }
}

/* ==========================================================================
   24 · THE WIDGET INSIDE A SHELL — popup and floating
   --------------------------------------------------------------------------
   Everything above assumes the widget is the page's own block. Two of the
   three positions are not that: `popup` puts it in a dialog over a dimmed
   page, `floating` docks it in a corner over live content. The shell markup
   and the shell's own stylesheet live in PHP (BookingShortcode::renderShell /
   positionStyles) because they must work on themes that bring no modal CSS;
   what belongs HERE is everything about how the WIDGET behaves once it is
   inside one.

   The rule the whole section serves: a size scales the TEMPLATE, and a shell
   obeys that size rather than replacing it with a constant of its own.
   ========================================================================== */

/* ── 1 · A shell is the frame, so drop the page-embed chrome ──────────────
   `#so-booking-widget` carries `margin-bottom: 40px` to separate itself from
   whatever the page puts after it, plus top padding for the same reason.
   Inside a modal there is no next block — both render as dead white, and the
   40px was the ENTIRE cause of the popup's nested scrollbar (dialog
   scrollHeight 812 against clientHeight 810). It also took 48 of the floating
   panel's 88px of wasted height. */
.so-bw-popup__dialog #so-booking-widget,
.so-bw-floating__panel #so-booking-widget {
    margin-bottom: 0;
    padding-top: 0;
}

/* ── 2 · One height authority per position ───────────────────────────────
   The widget bounds itself to `min(760px, 100dvh - 112px)` — a page-embed
   rule, measured against the VIEWPORT. A shell has its own, smaller ceiling
   (80vh panel / 90vh dialog), so the widget kept sizing itself past the box it
   was in and the commit bar was sliced off the bottom edge: measured 31px
   below the panel on a 1440x900 laptop, up to 61px, with the Continue button
   down to 3% of its height on 17 of the 19 templates. Worse, it could not be
   scrolled back into view (see rule 3).

   `100dvh - 112px` stays as a term inside the min(): dropping it LOOSENS the
   cap on any viewport under ~1120px tall and introduces a fresh clip. The
   48px subtrahend is the shell chrome the widget no longer has to pay for,
   and it assumes rule 1 above has removed the 40px margin. */
#so-booking-widget[data-position="floating"] .so-bw-container {
    max-height: min(760px, calc(100dvh - 112px), calc(80vh - 48px)) !important;
}
#so-booking-widget[data-position="popup"] .so-bw-container {
    max-height: min(760px, calc(100dvh - 112px), calc(90vh - 48px)) !important;
}

/* ── 3 · Let the scroll chain reach the shell, and stop there ─────────────
   `.so-bw-body` carries `overscroll-behavior: contain` so a customer scrolling
   the slot list cannot scroll the host page. Inside a shell that is one
   containment too many: the gesture was swallowed by the body, so once the
   body hit its end the panel never scrolled and the clipped commit bar was
   reachable only by putting the pointer on a 16px strip of padding.

   Releasing the body to `auto` chains the gesture up to the shell; the shell
   itself is `contain` (positionStyles), which is what actually stops the page
   behind a modal from scrolling. Both halves are required — `auto` alone
   chains straight through to the document. Placed last: three rules declare
   overscroll-behavior on .so-bw-body at equal specificity, so source order
   decides. */
#so-booking-widget[data-position="floating"] .so-bw-body,
#so-booking-widget[data-position="popup"] .so-bw-body {
    overscroll-behavior: auto;
}

/* ── 4 · The restacked summary rail must not starve the body ─────────────
   Not a shell bug — it is worst there, but inline collapses identically. Below
   its collapse width each rail template lifts the summary out of its column
   and into an extra `auto` grid row. That rail is 240-336px tall, larger than
   the header, and inside a height-bounded pane it is taken out of the body's
   share: at 640px of height Accordion spends header 196 + nav 81 + rail 239 of
   528px and `.so-bw-body` computes to ZERO — the entire booking step vanishes
   while the chrome around it looks perfectly healthy.

   Capping the restacked rail returns ~131px to the body. The tokens mirror
   each template's own restack breakpoint. Do NOT instead floor the body with
   `min-height` — measured, that destroys the commit bar on split/agenda/dusk
   (nav 73px -> 0) and damages 47 configurations including inline.

   A constant cap is not enough on a short window: at 160px the rail still
   spent more than the pane had and the body reached 0 again below ~560px of
   viewport height. The cap is a SHARE of the viewport with a 160px ceiling, so
   the shorter the window the less the summary takes and the body keeps a
   positive share all the way down. */
#so-booking-widget:where([data-cw~="900"])[data-template="tabbed"] .so-bw-rail,
#so-booking-widget:where([data-cw~="820"])[data-template="agenda"] .so-bw-rail,
#so-booking-widget:where([data-cw~="640"])[data-template="dusk"] .so-bw-rail {
    min-height: 0;
    max-height: min(160px, 22vh);
    overflow-y: auto;
}

/* ── 4b · A short corner dock cannot afford a summary at all ─────────────
   A floating panel on a short window is the one place the cap above is not
   enough. It is bounded by 80vh rather than the popup's 90vh AND it is narrow
   enough to have restacked the rail in the first place, so on a 480px-tall
   window the pane is ~336px and the header and commit bar alone claim 236 of
   it — the rail then takes the rest and `.so-bw-body` computes to ZERO, which
   is the booking step disappearing while its chrome looks fine.

   Below 700px of viewport height the summary yields. It is the only element
   here that is a RECAP: every value in it was chosen by the customer two taps
   ago and is restated in full on the review step, whereas the body is the step
   itself. A height media query is right — this is a fact about the window, not
   about the container. */
@media (max-height: 700px) {
    #so-booking-widget[data-position="floating"] .so-bw-rail {
        display: none;
    }
}

/* ── 5 · Reserve the shell's close-button corner ─────────────────────────
   The shells' close button is absolutely positioned at top:8px/right:10px with
   z-index 2, so it sits ON the widget's own header: measured overlapping
   `.so-bw-step-count` on 15 of 19 templates at 390px — the only progress
   signal those templates give — and the rail title on split/agenda at 1440.
   The button wins, so the text is what disappears. */
.so-bw-popup__dialog #so-booking-widget .so-bw-header,
.so-bw-popup__dialog #so-booking-widget .so-bw-rail-head,
.so-bw-floating__panel #so-booking-widget .so-bw-header,
.so-bw-floating__panel #so-booking-widget .so-bw-rail-head {
    padding-right: 52px;
}


/* ==========================================================================
   34 · DESKTOP HEIGHT — ONE CARD SIZE PER TEMPLATE, ON EVERY STEP
   --------------------------------------------------------------------------

   The shell has had a CEILING since section 23 — `max-height: min(760px,
   calc(100dvh - 112px))`, raised by three templates that declare their own —
   and a FLOOR since the RESPONSIVE AUTO-HEIGHT block, `min-height:
   clamp(360px, 52vh, 560px)`. Between those two the card is as tall as
   whatever step happens to be showing, and on a desktop the gap between them
   is most of a card.

   MEASURED, twenty-three templates x seven steps at 1440x900, driven through
   the plugin's own CSS and JS by tests/visual/flow-walk/build-pages.mjs.
   Shortest step, tallest step, and how far the commit bar travels between
   them:

     frost      610 … 836   226      tabbed     559 … 760   201
     bundle     568 … 760   192      weekboard  575 … 760   185
     workspace  596 … 760   164      agenda     602 … 760   158
     wide       607 … 760   153      dusk       636 … 788   152 (nav 196)
     dualmonth  613 … 760   147      daypart    617 … 760   143
     dialog     650 … 760   110      deck       653 … 760   107
     counter    682 … 788   106      split      656 … 760   104
     roster     659 … 760   101      chat       661 … 760    99
     soft       675 … 760    85      inline     693 … 760    67 (nav 86)
     hero       695 … 760    65      editorial  700 … 760    60
     timeline   703 … 760    57      centred    729 … 760    31
     accordion  760 … 760     0

   Twenty-one of the twenty-three moved by more than 55px between two steps of
   the same booking, and the commit bar moved with them: on Tabbed Rail the
   Continue button sits 201px further up the page on Your Details than it does
   on Review. The customer's eye has to re-find the one control the step exists
   for, on every step, and anything below the widget on the host page jumps
   with it. Your Details is the low point on thirteen of the twenty-three and
   the not-yet-picked Date & Time on six more — a six-field form and an empty
   slot column are simply shorter than a service catalogue — so the collapse
   lands in the middle of the flow rather than at either end of it.

   Editorial was the only template that had already been settled (27.11, which
   raised its floor to `min(700px, calc(100dvh - 112px))` against the same
   ceiling). This generalises that fix to the other twenty-two.

   ── A DESKTOP BAND: BOTH BOUNDS, STATED HERE ─────────────────────────────

   This section owns BOTH ends of the card on a desktop — a floor of 800px and
   a ceiling of 860px — rather than only raising the floor to meet section 23's
   shared 760.

   860 is the shared ceiling plus 100px, and the 100px is not decoration: it is
   about two more service cards, three more staff rows or two more rows of the
   slot grid visible before the customer scrolls, on a card that is one pane on
   someone's page rather than the page itself. 860 also happens to be where the
   catalogue's own tall templates already sit — Dusk had taken 840 for exactly
   this reason (26.4c) and is folded back into the shared value here, because
   the argument it made for itself is now the argument for everyone.

   Both bounds are `min(<px>, calc(100dvh - 112px))`, which is what keeps a
   taller card honest:

     • On a tall window the PIXEL terms win, so the card breathes within a
       60px band and never jumps. Measured at 1920x1080 after this section:
       every one of the twenty-three sits at 800…860, swing 60 (Counter Sand
       860…900 and Frost 940…980, swing 40, on their own ceilings).
     • On a short window `calc(100dvh - 112px)` wins in BOTH, floor and
       ceiling resolve to the same number, and the card is pinned to exactly
       what the screen can show with nothing overhanging. A 1440x900 laptop
       renders 788px on every step and a 1366x768 one renders 656px: under the
       800px this section asks for, and the honest answer, because the
       alternative is a card taller than the window with the page scrolling to
       reach its own commit bar. This is also why the increase is felt on a
       1080p desktop and not on a 900px-tall laptop — there is no room on the
       latter, and inventing it would mean overflowing the viewport.
     • A template that re-tunes either bound re-tunes the matching token; the
       two cannot drift into crossing, because the only comparison that can
       ever matter is 800 < 860.

   ── THE VIEWPORT TERM IS 72px, NOT SECTION 23's 112 ──────────────────────

   Section 23 subtracts 112px: a 16px body gutter top and bottom, plus the
   identity banner and its 14px margin, plus slack. That reserves the BANNER as
   if it had to be on screen at the same time as the whole card. It does not —
   the banner sits above the widget in page flow and scrolls away, and on a
   customer's own page there is a theme header above it doing the same. What
   genuinely has to be reserved is the space the WIDGET ITSELF occupies outside
   the card: its root's 12px top padding, its 40px bottom margin (section 2,
   which separates it from whatever follows), and the page gutter.

   REPORTED against Hero, and measured at 1920x860 with the card at Full Width:
   the card resolved to 748px and sat with NINETY PIXELS of empty page below it
   — 40px of root margin, 10px of gutter and 40px of viewport nothing — while
   `.so-bw-body` still had 1176px left to scroll. The customer was shown a
   clipped row of service cards above a band of white.

   72 is that footprint stated honestly: 12 + 40 + ~20 of gutter. At an 860px
   viewport the card resolves to 788px, the widget's margin box ends exactly at
   the bottom of the screen, and the whole 40px goes to `.so-bw-body` — which
   is the only child that can take it, so it is the content area that grows and
   not the header or the commit bar. Nothing overflows: the term is still a
   `min()` against the screen, so the card can never be taller than what is
   left after the widget's own chrome.

   Three templates need more than 72 because their ROOT draws an inset outside
   the card that this number cannot see: 34.3 for Dialog and Card Deck, 34.2
   for Frost. Each adds its own inset to the same 72.

   NOT FULL-SCREEN, DELIBERATELY. 860 leaves ~108px of page visible above and
   below the card on a 1080p desktop, so the widget still reads as an embed.
   The bound is a pixel value first and a viewport fraction only as a fallback
   precisely so that a very tall monitor does not turn the booking form into a
   1200px column.

   WHY `max-height` IS RESTATED HERE RATHER THAN EDITED IN SECTION 23. That
   rule is the shell's invariant at EVERY width — it is what stops a phone
   handing its scrolling to the page — and 860 is a desktop answer. Overriding
   it from inside the `[data-cwmin~="960"]` gate leaves the narrow tiers on the
   760 they were tuned against, which is what "mobile and tablet keep
   responsive auto-height" has to mean if it means anything. `!important` only
   because the rule it answers carries one.

   A single `height` would have been simpler and wrong: it would stop a short
   step growing when a long service name wraps or a validation message needs
   the room, which is the one case where the card SHOULD move.

   ── WHY THE BODY ABSORBS IT AND THE CHROME DOES NOT ──────────────────────

   Nothing here touches the header, the progress bar or the commit bar. Every
   architecture already routes spare height to `.so-bw-body` and to nothing
   else — the flex column at ~12346 makes it the one `flex: 1 1 auto` child
   with every sibling at `flex-shrink: 0`, and the grid architectures (Split,
   Accordion, Tabbed, Agenda, Dusk) spend it on their one `1fr` / `minmax(0,
   1fr)` row. So raising the floor moves the commit bar DOWN to a fixed line
   and leaves the header, the stepper and the progress bar exactly where they
   were. Section 23 keeps `.so-bw-body` the only scroller in the widget, so a
   step with more content than the band allows still scrolls its own list
   rather than the card or the page.

   ── DESKTOP AND LAPTOP ONLY ──────────────────────────────────────────────

   Gated on `[data-cwmin~="960"]`: the widget's own measured column width is
   at least 960px. Not a media query, because the widget does not live in the
   window — it lives in whatever box the embed gives it, which is the whole
   reason this file moved off `@media` (see _wireContainerWidth in
   booking-widget.js). 960 is the file's existing desktop tier (section 21),
   and asking the column rather than the screen settles three cases for free:

     • A phone or a portrait tablet never stamps 960, so the responsive
       auto-height floor at ~13423 keeps the narrow tiers exactly as they are.
     • The popup shell is 560px wide and the floating panel 420px, so neither
       can match — which matters, because both cap this same container at 90vh
       / 80vh (26.9, 26.10) and a floor pushed past that cap is a second
       scrollbar in the shell.
     • The Studio's device-frame preview renders inside a 460px iframe and is
       likewise unaffected; its own `min-height: 0 !important` from
       BookingShortcode::expandedPreviewStyles() would win regardless.

   The cost of asking the column: a host theme whose content column is under
   960px keeps today's behaviour on a desktop. That is the conservative side
   of the trade — the alternative is a 720px floor inside an 800px column,
   which is a tall narrow card, not a stable one.

   ── SPECIFICITY ──────────────────────────────────────────────────────────

   (1,4,0), with the id OUTSIDE any `:where()`. The floor has to clear four
   things: the RESPONSIVE AUTO-HEIGHT floor (1,2,0), its phone tier (1,2,0),
   the five architectures that set a taller floor of their own — Split 560,
   Agenda 520, Dusk 520, Roster 520, Workspace 560 — at (1,2,0), and
   Editorial's 27.11 at (1,3,0). The ceiling has to clear section 23's shared
   bound at (1,2,0) !important and the two templates that raise it at (1,3,0)
   !important, which is what `!important` here is answering rather than
   competing with; 34.2 hands those two their own number back.

   The Studio's two preview routes are unaffected in the ways that matter. The
   device frame renders in a 460px iframe, which never stamps `data-cwmin~=
   "960"`; the expanded tab preview does match, and its own
   `max-height: max(360px, 100%) !important` gives way to this — which resolves
   to within a few pixels of the window height it was reaching for, so the
   preview keeps showing what a real embed shows.
   ========================================================================== */

/* ── 34.1 · The desktop band, for all twenty-three ───────────────────────
   Read through tokens rather than written inline, so 34.2 and 34.3 restate one
   value per template instead of repeating the whole rule — and so the
   relationship between a floor and its ceiling stays visible at each site.

   The viewport allowance is TWO tokens, not one, and the split is what lets
   34.5 drop the separator without knowing any template's inset:

     --so-bw-desk-inset   the widget's own box outside the card: its root's
                          12px top padding plus the page gutter. 32px shared;
                          the three templates that draw more restate this one.
     --so-bw-desk-sep     the root's 40px bottom margin (section 2), which
                          exists to separate the widget from what follows it
                          on the page. 34.5 zeroes it when nothing does. */
#so-booking-widget[data-cwmin~="960"][data-template][data-template-id] .so-bw-container {
    --so-bw-desk-inset: 32px;
    --so-bw-desk-sep: 40px;
    --so-bw-desk-floor: min(800px, calc(100dvh - var(--so-bw-desk-inset) - var(--so-bw-desk-sep)));
    --so-bw-desk-ceil: min(860px, calc(100dvh - var(--so-bw-desk-inset) - var(--so-bw-desk-sep)));
    min-height: var(--so-bw-desk-floor);
    max-height: var(--so-bw-desk-ceil) !important;
}

/* ── 34.2 · The two templates that still stand above the shared ceiling ───
   Counter Sand (24.3) and Frost (28.1) declare taller ceilings than 860
   because their choice step is a scrolling index rather than a calendar or a
   form, and every extra pixel is another fraction of the catalogue on screen.
   Both are restated here rather than left to their own rules, because 34.1's
   `!important` at (1,4,0) outranks the (1,3,0) `!important` those carry — the
   token is the only way their numbers survive.

   CORAL POP IS A THIRD, and it restates in section 39 (`1b-ii`) rather than
   here. Its ceiling is not one number but a ramp the Studio "Widget Size"
   radio sets, so its entry has to sit beside that ramp or the two drift; it
   pins floor to ceiling as well, because its pane hands every spare pixel to a
   scrolling list and so has no natural height to settle at. Listed here so
   this enumeration stays the complete answer to "who overrides the band".

   DUSK IS DELIBERATELY ABSENT. Its 840 (26.4c) was the same argument this
   section now makes for the whole catalogue, and 860 is more than the 840 it
   asked for, so it takes the shared band and its own rule becomes redundant
   rather than contradicted.

   ONLY THE PIXEL TERM IS EACH TEMPLATE'S OWN. Counter Sand's viewport term is
   the shared `calc(100dvh - 112px)`, which is what its own ceiling already
   uses. Frost's is 164px, and that number is a CORRECTION rather than a
   preference — 28.1 gives it 64px on the reasoning that its sheets already
   stand 12px inside the container edge, and claims "on anything under ~1044px
   tall the screen decides and the card cannot overflow it". Measured at
   1440x900, the card resolved to 836px and the page scrolled 60px, on three of
   its seven steps.

   The arithmetic 64 misses: Frost's ROOT carries 32px of gradient field above
   and below the card, against 12px on Centred, so it needs 52px MORE clearance
   than the shared allowance, not 48px less — a 100px error, of which 60 showed
   up as page scroll. Its inset is therefore 32 + 52 = 84, which with the
   shared 40px separator is the 124 the card is actually held back by. The
   980px pixel term is untouched: Frost stays the tallest card in the catalogue
   on a screen with the room for it, and stops overhanging the ones without it.

   Only the DESKTOP tier is corrected here. 28.1 still governs the narrow tiers
   as it does today, where the same 64px understates the clearance on a phone
   by roughly the same amount — worth a look, but it is not a desktop height
   question and this section does not reach it. */
#so-booking-widget[data-cwmin~="960"][data-template="counter"][data-template-id] .so-bw-container {
    --so-bw-desk-floor: min(860px, calc(100dvh - var(--so-bw-desk-inset) - var(--so-bw-desk-sep)));
    --so-bw-desk-ceil: min(900px, calc(100dvh - var(--so-bw-desk-inset) - var(--so-bw-desk-sep)));
}
#so-booking-widget[data-cwmin~="960"][data-template="frost"][data-template-id] .so-bw-container {
    --so-bw-desk-inset: 84px;
    --so-bw-desk-floor: min(940px, calc(100dvh - var(--so-bw-desk-inset) - var(--so-bw-desk-sep)));
    --so-bw-desk-ceil: min(980px, calc(100dvh - var(--so-bw-desk-inset) - var(--so-bw-desk-sep)));
}

/* ── 34.3 · Two templates pay rent outside the card ──────────────────────
   The shared `calc(100dvh - 72px)` accounts for the page gutter and the
   widget root's own 12px top padding and 40px bottom margin, and assumes the
   root adds nothing else around the card. On twenty-one templates it adds
   nothing else.

   Two do not. Dialog draws a 40px inset all round, which is what sells "a
   modal floating over the page" rather than a card sitting on it, and Card
   Deck draws 28px above and 40px below PLUS a 16px top margin on the card, to
   make room for the two peeking cards of its stack. All of that is OUTSIDE
   `.so-bw-container`, so the shared expression cannot see it, and the card is
   sized as though the root were flush.

   MEASURED, not assumed. All twenty-three were swept for the distance between
   the root's edges and the card's: twenty sit at 12px, Frost at 64, Dialog at
   80, Card Deck at 84. Card Deck's 16px card margin is exactly why guessing
   from its padding alone left it 16px short and still page-scrolling — the
   sweep is the check to repeat if a template ever grows an inset.

   It never showed before because neither template reached its ceiling often
   enough to matter — measured at 1440x900 before section 34, Dialog ran
   650…760 and page-scrolled 0, Card Deck 653…760 and scrolled 4px. Raising
   the floor is what makes the ceiling the normal case, and at 788px both
   overflowed the window: Dialog by 28px, Card Deck by 32.

   Each restates `--so-bw-desk-inset` alone, adding what its root draws OVER
   the 12px the shared 32 already counts: 68px for Dialog (80 - 12) gives 100,
   72px for Card Deck (84 - 12) gives 104. The separator token is left to 34.1
   and 34.5, and the pixel terms are untouched — so on a tall desktop these two
   sit at the same 800…860 as everyone else, and only the viewport term, the
   one that decides a laptop, knows about the inset. */
#so-booking-widget[data-cwmin~="960"][data-template="deck"][data-template-id] .so-bw-container {
    --so-bw-desk-inset: 104px;
}

/* ── 34.4 · An empty step is a designed state, not a stranded sentence ────
   The floor's whole purpose is that a short step keeps the card's presence,
   and the shortest step of all is an empty one: "No staff available for this
   service", one line of tertiary grey pinned to the top of an 800px card with
   nothing under it. Filling the room the floor creates is what makes that
   read as an answer rather than as a failure to render.

   `min-height` plus centring on the empty block itself, NOT a display change
   on `.so-bw-step` — twenty-three architectures lay that element out and none
   of them expects it to become a flex column. This touches one leaf box that
   is already `text-align: center` and owns nothing but its own message.

   THE CHILD COMBINATOR IS LOAD-BEARING. `.so-bw-empty` is reused twice inside
   smaller boxes: once in the slot column beside the month (booking-widget.js
   ~2694) and once inside the time-range policy panel (~3046), which is a
   bordered panel with a heading and a paragraph of its own. A 240px floor
   there would inflate a panel, not a step. Only the seven that are DIRECT
   children of `.so-bw-step` — the empty and no-search-results states for
   locations, services and staff, plus no add-ons — are the step's whole
   content. Verified against the rendered DOM, not against the indentation.

   240px is two service cards and their gutter, so an empty result occupies
   about what a thin result would have. */
#so-booking-widget[data-cwmin~="960"][data-template] .so-bw-step > .so-bw-empty,
#so-booking-widget[data-cwmin~="960"] .so-bw-step > .so-bw-empty {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    min-height: 240px;
}


/* ── 34.5 · No separator when there is nothing to separate from ──────────
   REPORTED: white space under the commit bar on the live Manage Booking page,
   still there after the allowance came down to 72px.

   MEASURED on that page at a 900px viewport, walking the layout downward from
   the card:

     card bottom / widget root bottom   1553   (the card fills the root exactly)
     + the root's 40px bottom margin    1593
     + the theme's own group padding    1663   (70px, `wp-block-group`)
     + the theme's footer               2108   (445px)

   So the widget's own contribution to that band is exactly one thing: the 40px
   `margin-bottom` section 2 puts on the root "to separate the appointment
   booking widget from whatever follows it on the page". The other 70px belongs
   to the theme and is not ours to take.

   When nothing follows, that margin is separating the widget from the end of
   the document — 40px of nothing under the commit bar, and 40px the card was
   asked to leave room for. `:last-child` is the whole condition, and it is
   self-limiting by construction: a page that stacks the events or tickets
   widget under this one does not match, and keeps its separator.

   BOTH HALVES ARE NEEDED. Dropping the margin alone just moves the page up and
   leaves the same gap between the card and the bottom of the screen, because
   the card was still sized as though the margin were there. Zeroing
   `--so-bw-desk-sep` is what hands those 40px to `.so-bw-body`.

   (1,5,0), one attribute above 34.1, and it sets ONLY the separator token —
   which is why 34.2 and 34.3 can restate `--so-bw-desk-inset` for Frost,
   Dialog and Card Deck at (1,4,0) and still have it honoured here. A rule that
   restated the whole expression would have silently flattened their insets.

   NOTE FOR THE PAGE ITSELF: a trailing EMPTY paragraph block in the editor is
   still an element, so it defeats `:last-child` while contributing no height
   of its own — the Manage Booking page has three of them after the shortcode,
   which is why this rule does not fire there until they are deleted. */
#so-booking-widget:last-child {
    margin-bottom: 0;
}
#so-booking-widget:last-child[data-cwmin~="960"][data-template][data-template-id] .so-bw-container {
    --so-bw-desk-sep: 0px;
}


/* ── 34.6 · HERO — the ceiling tracks the screen, so the grid gets rows ───
   REPORTED: Hero still does not use the available viewport height; the service
   grid should show 1.5–2 rows on a desktop without scrolling.

   MEASURED on the live page at 1920 wide, four columns: a service card is
   355px tall and the row gap is 14px, so a row costs 369px. Above the grid the
   step spends ~114px on its own padding, the step description and the search
   field. So the CARD height a given number of rows needs is:

     1 row      369 + 114 + 180 header + 80 commit bar   =  743px
     1.5 rows   554 + 114 + 260                          =  928px
     2 rows     738 + 114 + 260                          = 1112px

   Against that, 34.1's shared ceiling of 860px was giving the grid 442px — one
   row and a fifth of the next — and on a 1080p screen it was leaving 158px of
   viewport unused to do it. The pixel cap, not the screen, was the binding
   constraint. That is the whole complaint.

   SO THE PIXEL TERM STOPS BINDING ON THIS TEMPLATE. 1040/980 sits above what
   any ordinary desktop resolves `100dvh` to, which makes `calc()` the operative
   half of both bounds and the card a function of the screen rather than of a
   number in this file — the "no fixed heights" this was asked for. The cap is
   not removed, because a bound that only ever reads the viewport turns a 1440px
   monitor into a 1368px booking form; it is raised until it stops deciding.

   WHAT THAT BUYS, measured after:

     1080p desktop   card 1008, grid 634px   1.7 rows
     1440x900        card  828, grid 454px   1.2 rows — the screen's own limit
     1366x768        card  696, grid 322px   0.9 rows

   1.5 rows is reachable on a 1080p screen and NOT on a 900px-tall laptop, and
   no arithmetic here changes that: 928px of card needs 1000px of window, and
   pretending otherwise means a card taller than the screen with the commit bar
   below the fold. The two remaining levers are Hero's own design — its 180px
   header band, the tallest chrome in the catalogue, and its 355px card, most
   of which is the image — and both are the template's identity rather than its
   height, so neither is touched here.

   HERO ONLY. The other twenty-two keep the 800…860 band, which is the
   "spacious but not full-screen" the catalogue was tuned to; Hero is the one
   template whose content is a photographic grid rather than a list or a form,
   and the one this was reported against.

   The floor moves with the ceiling, so the band stays 60px and the card is as
   steady between steps as it is everywhere else. Both bounds keep the
   `--so-bw-desk-inset` / `--so-bw-desk-sep` pair, so 34.5 still hands Hero its
   separator back on a page where nothing follows the widget. */
#so-booking-widget[data-cwmin~="960"][data-template="hero"][data-template-id] .so-bw-container {
    --so-bw-desk-floor: min(980px, calc(100dvh - var(--so-bw-desk-inset) - var(--so-bw-desk-sep)));
    --so-bw-desk-ceil: min(1040px, calc(100dvh - var(--so-bw-desk-inset) - var(--so-bw-desk-sep)));
}


/* ── 34.7 · HERO — a shallower hero band, and the grid takes the difference ─
   34.6 gave Hero every pixel the SCREEN had. This gives it the pixels its own
   header was holding, which is the only place left: at 1440x900 the card is
   already at the viewport's limit, so the grid can only grow if the chrome
   above it shrinks.

   MEASURED, the 180px band broken down on the live page:

     padding-top                        24
     step indicator (title + "1 of 6")  31
       its margin-bottom                12
     progress segments  margin-top      14
       the segments themselves          20   (a 4px bar in 8px of padding)
       their margin-bottom              16
     padding-bottom                     52
                                       ---
     content                           169, held to 180 by `min-height`

   Two thirds of that is space, not ink. The 4px progress bar is carrying 58px
   of margin and padding around it, and the band under it is 52px deep.

   WHAT EACH NUMBER BECOMES, and why it is the number:

     min-height   180 -> 120   Stops binding; the content decides. 120 is the
                               phone tier's own floor (section 12b), so the
                               band cannot collapse under a short title.
     padding-top   24 -> 16    The phone tier's value. The title has never
                               needed 24 above it.
     padding-bot   52 -> 40    THE FLOOR HERE IS THE LIFT, not taste.
                               `.so-bw-body` is pulled up 28px so the white
                               card overlaps the hero — that overlap IS the
                               template. 40 leaves 12px of band visible under
                               the segments; anything under 28 would put them
                               behind the card.
     indicator mb  12 -> 8     }  the 58px of air around a 4px bar
     stepbar mt    14 -> 10    }  comes down to 32, which still reads as a
     stepbar pad    8 -> 4     }  deliberate gap rather than a collision
     stepbar mb    16 -> 10    }

   New content height: 16 + 31 + 8 + 10 + 12 + 10 + 40 = 127px, so the band
   goes 180 -> 127 and `.so-bw-body` gains 53px on every desktop screen.

   NOTHING ELSE MOVES. The card size, the grid, the lift, the commit bar and
   the type scale are all untouched — this is padding and margin on four
   elements. The hero photograph still carries the first screen, the segments
   still read as segments, and the white card still lifts off the band.

   DESKTOP ONLY, `[data-cwmin~="960"]`, like the rest of section 34. The phone
   tier already ships a tighter band of its own (120px / 16px / 30px, section
   12b) and tablets keep today's 180 — neither is a screen where the service
   grid was the complaint, and neither is re-tuned blind here. */
#so-booking-widget[data-cwmin~="960"][data-template="hero"][data-template-id] .so-bw-header {
    min-height: 120px;
    padding-top: 16px;
    padding-bottom: 40px;
}
#so-booking-widget[data-cwmin~="960"][data-template="hero"][data-template-id] .so-bw-step-indicator {
    margin-bottom: 8px;
}
#so-booking-widget[data-cwmin~="960"][data-template="hero"][data-template-id] .so-bw-stepbar {
    margin-top: 10px;
    margin-bottom: 10px;
    padding-top: 4px;
    padding-bottom: 4px;
}


/* ── 34.8 · HERO — the row itself, because the chrome has run out ─────────
   REPORTED: still under ONE full row, let alone one and a half.

   Confirmed. 34.6 and 34.7 were verified at 1920x1080, which is the BEST case
   in the catalogue — a 1920px column fits seven narrow cards, so the row is at
   its shortest. Swept across the resolutions people actually book on, with
   everything from 34.6 and 34.7 already applied:

     1920x1080   7 cols   row 355   grid 652   1.77 rows
     1600x900    5 cols   row 341   grid 472   1.33
     1440x900    5 cols   row 334   grid 472   1.36
     1536x864    5 cols   row 341   grid 436   1.23
     1366x768    4 cols   row 341   grid 340   0.96  <- under one full row

   At 1366x768 the grid gets 340px and a row costs 355. That is the report, and
   it is arithmetic rather than a bug: the row is simply longer than the space.

   WHERE THE 477px OF BODY GOES AT 1366x768:

     step padding-top                24
     step description + its margin    41
     the search field                 65
     ---------------------------------
     above the grid                  137
     the grid                        340   <- 15px short of one 355px row

   THE CHROME CANNOT PAY FOR IT. Every pixel above the grid is 137, and half of
   it is a search field nobody wants smaller. Even spending ALL of it leaves
   477px against the 526px that one and a half 341px rows need. So the row has
   to come down, and this is the section that says so out loud after two that
   avoided it.

   TWO TRIMS ABOVE THE GRID, worth 20px and costing nothing:
     step padding-top   24 -> 14   the search field below it already separates
                                   the grid from the panel edge
     desc margin-bottom 20 -> 10   the description and the field it introduces
                                   read as one block at 10px

   AND THE PHOTO BAND, 176 -> 144. This is the "keep the card unchanged" line,
   crossed deliberately and as narrowly as possible:

     • It is a CROP DEPTH, not a layout change. The band is `aspect-ratio:
       3 / 2` capped by a `max-height`, and at every desktop column width the
       CAP is what applies, never the ratio — a 340px column wants 227px and
       gets 176. So this moves one number that was already the operative one,
       and `object-fit: cover` means the photograph crops a little more rather
       than distorting or letterboxing.
     • 144px at a 340px column is a 2.36:1 band. The catalogue's shared default
       is 2:1. Hero stays a photo-first card; it stops being a photo-first card
       that cannot show its second row.
     • The card's width, padding, type, badges and grid are all untouched. The
       row goes 341 -> 309 and nothing else about it moves.

   MEASURED AFTER, and this is the whole point of the section:

     1920x1080   1.77 -> 1.98 rows
     1600x900    1.33 -> 1.51
     1440x900    1.36 -> 1.51
     1536x864    1.23 -> 1.40
     1366x768    0.96 -> 1.09   one FULL row and a peek

   1.5 rows is met from 900px of viewport upward. At 768 it is still not, and
   no arithmetic here reaches it: one and a half 315px rows plus 117px of step
   chrome plus a 127px header and an 80px commit bar is 796px of card against
   the 696px a 768px screen can give. The remaining lever is the card's TEXT
   block — 163px for a title, two clamped lines and a badge row — and that is a
   content decision rather than a spacing one, so it is left alone.

   Desktop only, like the rest of section 34. The phone tier caps the same band
   at 148px already (section 12b) and is not re-tuned here. */
#so-booking-widget[data-cwmin~="960"][data-template="hero"][data-template-id] .so-bw-step {
    padding-top: 8px;
}
#so-booking-widget[data-cwmin~="960"][data-template="hero"][data-template-id] .so-bw-step-desc {
    margin-bottom: 10px;
}
/* The sticky field keeps section 36's containment, one notch tighter: the pin
   inset drops 16 -> 10 and the painted band below it 12 -> 10. `margin-top`
   still mirrors `padding-top` exactly, which is the whole trick holding the
   resting layout still while the pinned inset changes.

   AND THE PULL CANNOT EXCEED THE GAP IT IS EATING. The negative margin rises
   into the step description's bottom margin, so `margin-top` and that
   `margin-bottom` are ONE number — both 10 here. Set the pull deeper than the
   gap and the field's opaque, z-index-4 background lands ON the description:
   caught at -12 against a 6px gap, which clipped the last 6px of "…in the
   order you pick them" behind the search bar. Change one and change the
   other. */
#so-booking-widget[data-cwmin~="960"][data-template="hero"][data-template-id] .so-bw-search {
    margin-top: -10px;
    padding-top: 10px;
    padding-bottom: 10px;
}
#so-booking-widget[data-cwmin~="960"][data-template="hero"][data-template-id] .so-bw-card-media {
    max-height: 144px;
}


/* ==========================================================================
   33 · FROST — THE FLOATING PANEL
   --------------------------------------------------------------------------
   Sections 28–32 tuned Frost as an INLINE widget. Rendered as the floating
   modal it is a different box with different problems, and none of them are
   the template's: the floating chrome is one shared block emitted by
   BookingShortcode::positionStyles(), written for a plain white panel with a
   plain white widget inside it.

   Every rule here is scoped with `:has(> #so-booking-widget[data-template-id=
   "glass-frost"])`, the pattern the Dusk and Counter Sand floating patches in
   section 23 already use. That guard is also what makes these rules BIND: the
   shared block is inline `<style>` in the page body, so it comes after this
   stylesheet in document order and wins every tie at equal specificity. `:has()`
   carries the specificity of its argument, so these score (1,2,0) against its
   (0,1,0) and source order never enters into it.

   Nothing here changes what the widget does. No markup, no handlers — the
   trigger still toggles `.is-open`, the × still removes it.
   ========================================================================== */

/* ── 33.1 · One scroll region, and the page holds still ──────────────────
   REPORTED: nested scrollbars, and the page scrolling behind the modal.

   Both are the same rule. The shared panel is `max-height: 80vh; overflow:
   auto` — a scroll container — and the widget inside it is bounded by its own
   `min(980px, 100dvh - 64px)` (28.1), which on a 900px screen is 836px against
   the panel's 752px. The widget cannot fit, so the panel scrolls it, and the
   body scrolls inside that: two bars, and a drag near the edge moving the
   wrong one.

   The panel stops being a scroller and becomes the BOUND instead. Its height
   is stated so it can never reach the top of the screen — `100dvh` less the
   wrapper's own 24px bottom offset, less the trigger gap it opens above, less
   28px of clearance, which is the 24–32px asked for and holds at all three
   trigger sizes because the gap is a variable. The widget then fills that box
   exactly, and `.so-bw-body` — already `flex: 1 1 auto; min-height: 0` from
   the bounded-shell block — is the one thing that scrolls. Header and commit
   bar are its flex siblings and stay put, which is what they were always for.

   The page lock is `:has()` on the root element: no class is added to <body>
   when the panel opens (the trigger toggles `.is-open` on its own wrapper), so
   the selector reads that state where it actually lives. `scrollbar-gutter`
   keeps the width the page had, or removing the bar would shift the layout
   under the modal — a lock that jolts the page is worse than no lock. A
   browser without `:has()` support keeps today's behaviour. */
html:has(.so-bw-floating.is-open .so-bw-floating__panel > #so-booking-widget[data-template-id="glass-frost"]) {
    overflow: hidden;
    scrollbar-gutter: stable;
}
.so-bw-floating__panel:has(> #so-booking-widget[data-template-id="glass-frost"]) {
    flex-direction: column;
    overflow: hidden;
    max-height: calc(100dvh - var(--so-bw-fl-gap, 64px) - 52px);
    /* The FALLBACK width only — the four size rules below are what normally
       decides this. Kept so an embed whose wrapper carries no `data-so-bw-size`
       (anything saved before that attribute existed) renders exactly as it did.
       560 rather than 420 because two columns of service card need it (33.5),
       and the viewport term keeps the phone case full-bleed as it is now. */
    width: min(560px, calc(100vw - 40px));
}
/* ── 33.1a · …and it still has to OPEN and CLOSE, and answer to the size ──
   REPORTED: on Frost the floating launcher does nothing, the × does nothing,
   and the Widget Size control does nothing. All three are this one block, and
   none of them is the JavaScript.

   The shell's own rules are `.so-bw-floating__panel{display:none}` and
   `.so-bw-floating.is-open .so-bw-floating__panel{display:block}`, both
   (0,3,0) at most, plus a four-step width ramp on
   `[data-so-bw-size]` that is also (0,3,0). The selector above scores (1,2,0)
   — `:has()` takes its argument's specificity, and that argument carries the
   widget's ID — so ONE id outranks all of them. `display: flex` therefore beat
   both `none` and `block`, and the hard `width` beat all four sizes.

   MEASURED on the live page at 1440 before this: the panel was `display:flex`,
   560px and on screen at page load with the shell closed; clicking the launcher
   flipped `.is-open` true, clicking the × flipped it back to false — the class
   machinery worked perfectly — and the panel never moved. Width read 560px at
   small, medium, large AND full.

   So the state is restated here at Frost's own specificity rather than the
   override being weakened: `display` now follows `.is-open` like every other
   template, and the size ramp is repeated with Frost's viewport term
   (`100vw - 40px`, which is what its phone full-bleed depends on) instead of
   the shared `100vw - 48px`. The numbers are the shell's own 420/480/560/960,
   so Frost's designed 560 is simply what LARGE now means, and the two-column
   grid it was chosen for still appears from there up. `flex-direction`,
   `overflow` and the height cap stay unconditional above — they cost nothing
   while the panel is hidden and are what the open state needs. */
.so-bw-floating:not(.is-open) .so-bw-floating__panel:has(> #so-booking-widget[data-template-id="glass-frost"]) {
    display: none;
}
.so-bw-floating.is-open .so-bw-floating__panel:has(> #so-booking-widget[data-template-id="glass-frost"]) {
    display: flex;
}
.so-bw-floating[data-so-bw-size="small"] .so-bw-floating__panel:has(> #so-booking-widget[data-template-id="glass-frost"]) {
    width: min(420px, calc(100vw - 40px));
}
.so-bw-floating[data-so-bw-size="medium"] .so-bw-floating__panel:has(> #so-booking-widget[data-template-id="glass-frost"]) {
    width: min(480px, calc(100vw - 40px));
}
.so-bw-floating[data-so-bw-size="large"] .so-bw-floating__panel:has(> #so-booking-widget[data-template-id="glass-frost"]) {
    width: min(560px, calc(100vw - 40px));
}
.so-bw-floating[data-so-bw-size="full"] .so-bw-floating__panel:has(> #so-booking-widget[data-template-id="glass-frost"]) {
    width: min(960px, calc(100vw - 40px));
}
/* The gradient field fills the panel and passes the height down. `min-height:
   0` on both, or a flex item's automatic minimum refuses to shrink and the
   overflow lands back on the panel — the same trap the body had. */
.so-bw-floating__panel:has(> #so-booking-widget[data-template-id="glass-frost"]) > #so-booking-widget {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-height: 0;
    /* 32px of gradient around a 560px modal is 64px the catalogue does not
       get. The field still reads as a field at 12px. */
    padding: 12px;
}
.so-bw-floating__panel:has(> #so-booking-widget[data-template-id="glass-frost"]) .so-bw-container {
    max-height: 100% !important;
    height: 100%;
    min-height: 0;
}

/* ── 33.2 · The panel stops being a white box ────────────────────────────
   The shared panel paints a flat white background, a 16px pad and a shadow —
   a white card wrapped around a template whose whole proposition is a tinted
   gradient with sheets floating on it. Inline, Frost's root IS the modal
   surface; floating, it was matted like a photograph.

   The lift moves to the root, where the corner already is, so the modal still
   reads as a modal against the page. */
.so-bw-floating__panel:has(> #so-booking-widget[data-template-id="glass-frost"]) {
    background: transparent;
    padding: 0;
    border-radius: var(--so-bw-r-20);
    box-shadow: none;
}
.so-bw-floating__panel:has(> #so-booking-widget[data-template-id="glass-frost"]) > #so-booking-widget {
    box-shadow: 0 24px 60px -20px rgba(20, 40, 80, 0.55),
                0 4px 14px -6px rgba(20, 40, 80, 0.3);
}

/* ── 33.3 · The close button, inside the header ──────────────────────────
   REPORTED: the × is not in the modal.

   It is `position: absolute; top: 8px; right: 10px` against the PANEL, so with
   the panel's 16px pad it landed in the gradient margin outside the header
   sheet, half over the rounded corner. 32px is also under the 44px floor every
   other control in this widget is held to.

   40×40 at the header sheet's own top-right, which for this template resolves
   to the root's 12px pad plus 10px of inset. It is a sibling of the widget
   rather than a child of the header, so it is placed rather than flowed — but
   the header is a fixed band at the top of a bounded column, so the two agree
   at every panel size.

   The step count ("Step 1 of 6") lives in that corner too. The header takes a
   padding-right wide enough for the button plus its gap, so the two sit beside
   each other instead of on top of each other. */
.so-bw-floating__panel:has(> #so-booking-widget[data-template-id="glass-frost"]) .so-bw-floating__close {
    top: 22px;
    right: 22px;
    width: 40px;
    height: 40px;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    font-size: 22px;
    line-height: 1;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.72);
    -webkit-backdrop-filter: blur(10px);
    backdrop-filter: blur(10px);
    border: 1px solid rgba(255, 255, 255, 0.85);
    color: var(--so-bw-text-secondary);
    box-shadow: 0 2px 8px -4px rgba(20, 40, 80, 0.45);
    transition: background var(--so-bw-transition), color var(--so-bw-transition),
                border-color var(--so-bw-transition);
}
.so-bw-floating__panel:has(> #so-booking-widget[data-template-id="glass-frost"]) .so-bw-floating__close:hover {
    background: rgba(255, 255, 255, 0.95);
    border-color: rgba(var(--so-bw-primary-rgb, 99, 102, 241), 0.45);
    color: var(--so-bw-text);
}
.so-bw-floating__panel:has(> #so-booking-widget[data-template-id="glass-frost"]) .so-bw-floating__close:focus-visible {
    outline: 2px solid var(--so-bw-primary);
    outline-offset: 2px;
}
/* 40px button + 10px inset + 12px of breathing room. */
.so-bw-floating__panel:has(> #so-booking-widget[data-template-id="glass-frost"]) .so-bw-header {
    padding-right: 62px;
}

/* ── 33.4 · One primary action at a time ─────────────────────────────────
   The trigger stayed lit under the open modal — two blue buttons on screen,
   one of which closes what the other opened.

   `visibility` rather than `display`: the wrapper is a flex column on a phone
   (the shared block's 560px media query) and the panel is positioned against
   it on desktop, so removing the button from layout moves the modal. Hidden in
   place, nothing shifts, and it comes back exactly where it was. */
.so-bw-floating.is-open:has(.so-bw-floating__panel > #so-booking-widget[data-template-id="glass-frost"]) .so-bw-floating__btn {
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
    transition: opacity var(--so-bw-transition), visibility 0s linear 0.15s;
}

/* ── 33.5 · Two up in the panel, one up on a phone ───────────────────────
   Section 30's 280px track floor is measured against the INLINE container.
   Inside a 560px panel the grid gets ~444px of content, which that floor
   resolves to a single column — one service on screen at a time in a modal
   whose whole job is to show a menu.

   200px puts two tracks in 444px and one in anything under ~414px, which is
   every phone: the panel is `100vw - 40px` there, so a 390px screen gives the
   grid ~242px and it collapses to one column on its own. Same mechanism as
   section 30, a floor measured against this box instead of that one. */
.so-bw-floating__panel:has(> #so-booking-widget[data-template-id="glass-frost"]) .so-bw-card-grid {
    grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
}

/* ── 33.6 · The service tile answers to the template ─────────────────────
   REPORTED: the live widget's colours do not match the template preview.

   Confirmed, and this is the one place it is true. Every service carries its
   own colour (`so_services.color` — the seeded catalogue runs violet, pink,
   teal, sky, amber, emerald), and `serviceIconTileStyle()` paints the card's
   header tile with it inline. On most templates that is a feature. On Frost it
   is six accents on a one-accent design: the tile is the largest single block
   of colour on a photo-less card, so a violet circle on blue glass reads as a
   different template from the one the preview showed.

   The widget anticipates exactly this — the tile also publishes its colour as
   `--so-bw-tile-bg` specifically so a template can force the background and a
   later rule can opt back in (see the note in booking-widget.js). This takes
   the first half of that contract. `!important` is required and expected:
   the value it overrides is an inline style.

   Photographs are untouched — a service with an image never renders a tile. */
#so-booking-widget[data-template="frost"] .so-bw-card-media-avatar {
    background: var(--so-bw-primary) !important;
    color: var(--so-bw-on-primary, rgba(255, 255, 255, 1)) !important;
}

/* ── 33.7 · Enough of the list to be a list ──────────────────────────────
   MEASURED at 1440×900 with the fixes above: 451px of scroll region, 165px of
   it spent before the first card, 276px cards — 0.99 rows. One row of a
   thirteen-service catalogue, with nothing below it to say there is more.

   The panel cannot grow (33.1 already gives it everything the viewport has),
   so the height comes out of the four things between the customer and the
   second row. None of them is content:

     · the header band, at 107px for a label and a progress bar;
     · the step's 24px top padding, above a helper line that already has its
       own margin;
     · the card's photo band, which at a 207px column is proportionally deeper
       than it is inline;
     · the description, unclamped by 28.3.

   The clamp is the one that needs stating. 28.3 released it deliberately —
   a 293px inline card had the room and a truncated service name is a service
   nobody picks. A 207px column in a modal does not have that room: the same
   description runs five lines there. Three lines plus the Read more button
   the markup already carries is the same information at a different measure,
   and it is scoped to the floating panel, so the inline widget keeps the full
   text exactly as it is.

   Result is ~1.6 rows: two cards, and the top of the next pair showing under
   them, which is what tells a customer to scroll. */
.so-bw-floating__panel:has(> #so-booking-widget[data-template-id="glass-frost"]) .so-bw-header {
    padding-top: 12px;
    padding-bottom: 12px;
}
.so-bw-floating__panel:has(> #so-booking-widget[data-template-id="glass-frost"]) .so-bw-step {
    padding-top: 12px;
}
.so-bw-floating__panel:has(> #so-booking-widget[data-template-id="glass-frost"]) .so-bw-card-media {
    aspect-ratio: 2.8 / 1;
}
.so-bw-floating__panel:has(> #so-booking-widget[data-template-id="glass-frost"]) .so-bw-card--service .so-bw-card-meta--clamp {
    display: -webkit-box !important;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 3 !important;
    line-clamp: 3;
    overflow: hidden !important;
}

/* ── 33.8 · The gap the hidden trigger no longer needs ───────────────────
   `--so-bw-fl-gap` (56/64/76px by trigger size) exists to open the panel
   ABOVE the button without covering it. 33.4 hides the button while the panel
   is open, so on a bottom corner that gap is 64px of empty page under a modal
   that is short of room — and it is the cheapest 64px in the layout, because
   it costs no content at all.

   8px instead, which still lifts the modal off the viewport floor, and the
   height cap re-derived from it so the 28px of top clearance is unchanged:
   100dvh less the wrapper's 24px bottom offset, less that 8px, less 28px.
   Together with 33.7 this is what takes the list from one row to ~1.5.

   Bottom corners only. A top-anchored panel is positioned with `top` and
   setting `bottom` as well would stretch it between the two. */
.so-bw-floating:not([data-corner^="top"]) .so-bw-floating__panel:has(> #so-booking-widget[data-template-id="glass-frost"]) {
    bottom: 8px;
    max-height: calc(100dvh - 60px);
}

/* The credit line is the last white rectangle in the modal — a flat strip
   under the commit bar, on a template that has no flat white anywhere else.
   It keeps its place and its text; it just stops being a panel. */
.so-bw-floating__panel:has(> #so-booking-widget[data-template-id="glass-frost"]) .so-bw-powered {
    background: transparent;
    border: 0;
}


/* ==========================================================================
   35 · THE FLOATING PANEL, ON ALL TWENTY-THREE
   --------------------------------------------------------------------------

   Section 33 fixed the floating modal for ONE template. Everything it fixed
   was a fault of the shared shell rather than of Frost, so the same three
   scroll regions were live on the other twenty-two. Measured at 1440x900 on a
   3000px host page, every template x three steps, with the real shell markup
   and the real inline shell CSS from BookingShortcode::positionStyles():

     the PAGE          scrollable behind the open modal, 2148px, on 23 of 23
     the PANEL         scrolling its own content, 88px, on 23 of 23
     the WIDGET BODY   scrolling inside that, up to 1581px (Accordion)
     the CARD          overhanging the panel's bottom edge by 32px, 23 of 23
     the widget's bar  hidden entirely on Hero and Card Deck

   Three nested scroll regions with one wheel between them: a drag near an edge
   moves whichever one the pointer happens to be over, and the page sliding
   behind a fixed modal is the one that reads as broken.

   ── ONE SCROLLER, AND IT IS THE ONE WITH THE CONTENT IN IT ───────────────

   The panel stops being a scroller and becomes the BOUND. It is already
   `max-height: 80vh`, so it already knows how tall it may be; it just handed
   that height to a scrollbar instead of to the widget. As a flex column with
   `overflow: hidden` it passes the height down instead, the card fills it
   exactly, and `.so-bw-body` — already `flex: 1 1 auto; min-height: 0` from
   the bounded-shell block — is the only thing left that can scroll. The header
   and the commit bar are its flex siblings and hold still, which is what the
   bounded shell was always for.

   `overflow: hidden` on the panel is what 26.9 and 26.10 explicitly declined
   to do, and their reason was right at the time: "the card can be taller than
   the panel on a phone, and clipping would put Continue out of reach". That is
   no longer reachable. The card cannot be taller than the panel now, because
   the panel is what sizes it — `max-height: 100%` against a definite parent,
   with `min-height: 0` so a flex item's automatic minimum cannot floor it back
   at its content height. Clipping is safe only in that order, so do not keep
   the `overflow` without the sizing.

   THE PANEL'S OWN GEOMETRY IS UNTOUCHED. No max-height, no width, no padding,
   no corner, no shadow, no box-sizing — it renders in exactly the box it
   renders in today (measured 752x452 at 1440x900, its top at 60px). 35.5 later
   adds a `height`, and that is not an exception: it resolves to the same
   `max-height` each template already declared, so the panel still renders at
   the size it renders at today — it just stops being SMALLER than that on a
   short step. Everything here changes what the box does, not where it is.

   ── THE PAGE HOLDS STILL ─────────────────────────────────────────────────

   `html:has(.so-bw-floating.is-open)`, the mechanism 33.1 introduced: no class
   is put on <body> when the panel opens — the trigger toggles `.is-open` on
   its own wrapper with an inline handler — so the selector reads that state
   where it actually lives. Which also means the lock cannot leak: there is no
   JS state to unwind, and closing the panel releases it by the same selector
   ceasing to match. Two widgets on one page, or a panel closed by the ×, by
   the trigger, or by a script, all behave identically.

   `scrollbar-gutter: stable` is not decoration. Removing the page's scrollbar
   widens the layout under the modal by its width, and a lock that jolts the
   page sideways is worse than no lock; the gutter keeps the width the page
   already had.

   Not applied to the POPUP shell. Its overlay is `position: fixed; inset: 0`
   and covers the page, so the page scrolling behind it is invisible, and
   26.9/26.10 already gave it a single scroll region. Every selector below
   names `.so-bw-floating`.

   A browser without `:has()` keeps today's behaviour exactly — the rules
   simply never match. Nothing here changes markup, handlers, or what the
   widget does.
   ========================================================================== */

/* ── 35.1 · The page stops scrolling behind the modal ────────────────────
   Deliberately weaker than 33.1's Frost-scoped twin — (0,2,1) against (1,4,1)
   — and setting identical values, so which of the two wins is immaterial. */
html:has(.so-bw-floating.is-open) {
    overflow: hidden;
    scrollbar-gutter: stable;
}

/* ── 35.2 · The panel bounds the widget instead of scrolling it ──────────
   `:has(> #so-booking-widget)` is doing two jobs. It scopes these rules to a
   panel that actually holds the BOOKING widget — the same shell wraps the
   events and tickets listings, which are long documents that are supposed to
   scroll — and it lifts the selector to (1,3,0), which is what makes it bind
   at all: the shell CSS is an inline <style> in the page body, so it comes
   after this stylesheet in document order and wins every tie at equal
   specificity. `display` in particular has to beat
   `.so-bw-floating.is-open .so-bw-floating__panel{display:block}` there,
   which is (0,3,0).

   The closed state is untouched: the shell's `.so-bw-floating__panel{display:
   none}` has no `.is-open` ancestor here to match against, so a closed panel
   is still not rendered. */
.so-bw-floating.is-open .so-bw-floating__panel:has(> #so-booking-widget) {
    display: flex;
    flex-direction: column;
    overflow: hidden;
}

/* The widget root passes the height through. `min-height: 0` on the root AND
   on the card below, or a flex item's automatic minimum refuses to shrink
   below its content and the overflow lands straight back on the panel.

   `margin-bottom` is the widget's separator from whatever follows it on a host
   PAGE (section 2). Inside a modal nothing follows it, so those 40px are a
   white strip along the bottom of the panel and 40px the catalogue does not
   get. (1,1,0) against the root rule's (1,0,0). */
.so-bw-floating__panel:has(> #so-booking-widget) > #so-booking-widget {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-height: 0;
    margin-bottom: 0;
}

/* ── 35.3 · The card is exactly the panel, and never more ────────────────
   (1,3,0) plus `!important`, and both are needed. Dusk and Counter Sand cap
   this same container at `80vh !important` from (1,3,0) selectors in 26.9 and
   26.10 — written when the panel could not bound the card and 80vh was the
   closest available proxy for the panel's height. It is a proxy no longer:
   80vh is the panel's CONTENT box, so against a 16px-padded panel it leaves
   the card 32px short with a strip of shell showing under it. `100%` of a
   parent that now states its own height is the number those rules were
   reaching for. Their popup halves are deliberately untouched.

   The `[data-template-id]` inside the `:has()` is what buys the third
   attribute of specificity; every widget BookingShortcode renders carries it. */
.so-bw-floating__panel:has(> #so-booking-widget[data-template-id]) .so-bw-container {
    height: 100%;
    max-height: 100% !important;
    min-height: 0 !important;
}

/* ── 35.4 · The one remaining scrollbar is visible, thin and quiet ───────
   Hero, Card Deck, Frost and Coral Pop hide `.so-bw-body`'s bar outright
   (`scrollbar-width: none` plus `::-webkit-scrollbar{display:none}` in section
   12b), because on a PHONE they are pinned to the viewport and scrolling is by
   touch — a bar there is chrome the device does not use. A 420px floating
   panel stamps that same `data-cw~="480"` tier on a desktop, where the pointer
   is a mouse and an invisible bar means a list that merely looks truncated.
   Measured: Hero and Card Deck rendered no bar at all inside the panel while
   their body had up to 1026px left to scroll.

   Restored to the widget's OWN bar rather than a new one — the 8px tinted
   track from the design system, whose colours are mixed from `--so-bw-text` so
   it stays quiet on the dark templates instead of lighting a grey strip down
   them. (1,2,0), and later in the file than the tier it releases, which is
   also (1,2,0). */
.so-bw-floating__panel #so-booking-widget .so-bw-body {
    scrollbar-width: thin;
    -ms-overflow-style: auto;
}
.so-bw-floating__panel #so-booking-widget .so-bw-body::-webkit-scrollbar {
    display: block;
    width: 8px;
}

/* ── 35.5 · The modal stops resizing as you step through it ──────────────
   REPORTED: the floating panel's auto-height is not user-friendly.

   Confirmed, and it is worse than a height change because the panel is
   anchored by its BOTTOM: a shorter step does not trim the modal, it slides
   the whole thing down the screen. Measured at 1440x900, stepping from a
   four-service catalogue to a one-row Add Extras:

     frost      840 -> 632   the header drops 208px down the viewport
     dusk       720 -> 600   120px
     hero       752 -> 716    36px
     centred    752 -> 719    33px

   `max-height` says how tall the panel MAY be; nothing said how tall it IS,
   so it took its content's height and every step re-laid out the modal.

   `height: 100dvh` is not a height, it is a REQUEST TO BE AS TALL AS ALLOWED.
   `max-height` always wins over `height`, and every panel already carries one
   — the shell's shared `80vh`, or Frost's `calc(100dvh - 60px)`, or the 80vh
   that 26.9 and 26.10 pin on the two dark templates. So each template resolves
   to exactly the ceiling it already had, on every step, and this rule needs to
   know none of those numbers. Nothing gets taller than it could get before;
   the short steps simply stop being shorter.

   THE CONTENT AREA TAKES THE DIFFERENCE, not the chrome. 35.2 and 35.3 made
   the card fill the panel and `.so-bw-body` the only flexible child, so the
   header band, the progress bar and the commit bar hold the positions they
   hold on the tallest step, and a short step shows more empty list rather than
   a smaller modal.

   ABOVE THE PHONE BREAKPOINT ONLY. 561px is the shell's own boundary
   (positionStyles() switches the panel to full-bleed at 560), and below it the
   panel is a bottom sheet on a screen it nearly fills — sizing itself to two
   fields is the right behaviour there, and pinning it to 80vh of a handset
   would put a mostly-empty sheet over the whole screen. This is one of the few
   places a real media query is correct rather than `data-cw`: the panel is
   `position: fixed` and sized against the VIEWPORT, not against a column. */
@media (min-width: 561px) {
    .so-bw-floating.is-open .so-bw-floating__panel:has(> #so-booking-widget) {
        height: 100dvh;
    }
}


/* ==========================================================================
   36 · HERO — THE SEARCH FIELD BELONGS TO THE PANEL
   --------------------------------------------------------------------------

   REPORTED: on Hero the search bar floats above the layout and overlaps the
   container instead of sitting inside the content panel.

   Confirmed, and it is the shared sticky pin meeting Hero's lifted card. The
   bounded shell (section 23) pins `.so-bw-search` with `top: 0` and an opaque
   `--so-bw-card-bg`, which is right on twenty-two templates whose body is a
   plain square region. Hero's body is not: it is a white card with a 1px
   border and a 14px radius, pulled up 28px so it lifts off the hero band. So
   when the catalogue scrolls, the pinned bar lands flush on that card's top
   edge — no inset above the input at all, and a square opaque rectangle
   painted across two rounded corners. Measured pinned at 1920x1080: the field
   sat at the panel's exact top with 0px between it and the border it was
   covering.

   TWO PROPERTIES FIX BOTH, AND NEITHER MOVES THE RESTING LAYOUT.

   1 · THE PIN CARRIES THE PANEL'S INSET. `margin-top: -16px` with
       `padding-top: 16px` grows the sticky BOX upward by 16px while leaving
       its CONTENT where it was — the 16px is taken out of the step
       description's 20px bottom margin, which is transparent, so nothing
       shifts at rest. Pinned, `top: 0` now parks the box's edge on the panel
       and the input sits 16px inside it. The inset is the same scrolled or
       not, which is the "consistent top padding" this was reported for.

   2 · THE CORNERS MATCH THE PANEL. The top radius is derived from the same
       `--so-bw-r-14` the body draws itself with, less the 1px border it sits
       inside, so the bar nests in the card instead of squaring it off — and it
       tracks the Studio's corner slider rather than pinning a literal.

   The 16px of gap between the field and the first row of cards is unchanged;
   it just moves from margin to padding, so it is PAINTED. That is what stops a
   card reading through the gap as it passes under — transparent margin is
   exactly where the "floating" look comes from.

   NOT gated on `[data-cwmin~="960"]`, unlike section 34. This is a layout
   defect rather than a height decision, and it is present at every width: the
   phone tier keeps its own 20px lift and 6px inset (section 12b) and the
   radius token adapts, so the same two rules land correctly there.

   (1,1,1) against the shell's (1,2,0) `#so-booking-widget[data-template]
   .so-bw-search` — so `top` and the background stay exactly as section 23 set
   them, and only the four properties named here are answered. Nothing about
   the field's height, its icon, its clear button or its behaviour changes. */
#so-booking-widget[data-template="hero"] .so-bw-search {
    margin: -16px 0 4px;
    padding: 16px 0 12px;
    border-top-left-radius: calc(var(--so-bw-r-14, 14px) - 1px);
    border-top-right-radius: calc(var(--so-bw-r-14, 14px) - 1px);
}

/* ═══════════════════════════════════════════════════════════════════════════
   36 · CHAT BOOKING — the message thread (chatbook-sky, layout `chatbook`)
   ═══════════════════════════════════════════════════════════════════════════

   THE PROPOSITION. The booking arrives as a message log. Each question is a
   bot turn, each answer stays on screen as the customer's own reply, and the
   step they are on renders INSIDE the newest bot bubble — so what they tap is
   the real service card, calendar, slot grid, Form Builder page, review
   summary and payment form, not chat-shaped copies of any of it.

   WHY THAT NEEDS ALMOST NO NEW MARKUP. `.so-bw-body` is already a column whose
   children are, in order, the recap and then `.so-bw-step-host` carrying every
   `.so-bw-step` (only the active one is shown). That is thread order already:
   history above, live question below. So this section does three things —

     1 · reveals `.so-bw-chatlog` (the answered turns) between them,
     2 · draws the ACTIVE `.so-bw-step` as the newest bot bubble,
     3 · turns the header into a chat bar and the nav into a reply bar,

   and then restyles the shared primitives the bubble now contains, so the
   cards, calendar, slots, fields, summary, payment list and confirmation read
   as parts of a conversation rather than pages of a form.

   The step engine, the validation, the slot holds, the waitlist and multi-day
   branches, the payment retry — none of it is touched, because none of it is
   presentation. See the D10 block in booking-widget.js.

   ── THE SYSTEM ─────────────────────────────────────────────────────────────
   Spacing is the base-4 ramp (4 / 6 / 8 / 10 / 12 / 16 / 20). Corners come off
   the shared `--so-bw-r-*` scale so the Studio's radius slider still moves
   them. Colour is `--so-bw-primary` plus the `--so-bw-ds-n*` neutral ramp —
   there are exactly two literals in the whole section and both are labelled
   where they appear. Type runs 11 / 11.5 / 12.5 / 13.5 / 15 / 17.

   Contrast: body copy is n700 on white (8.6:1) and n500 on white (5.9:1);
   timestamps and the bar subtitle are the smallest text here and both clear
   4.5:1. Selected states never rely on hue alone — every one of them carries a
   check mark or a border-weight change as well, so the choice survives being
   read without colour.

   ── SCOPING ────────────────────────────────────────────────────────────────
   Every rule below is either (a) inside `[data-template="chatbook"]`, or (b)
   one of the `display: none` defaults immediately following, which HIDE this
   layout's own markup everywhere else. The chat classes appear nowhere else in
   this file. So the other twenty-three architectures are unaffected by
   construction, not by inspection.

   THE ONLY CONVERSATIONAL BLOCK LEFT. Section 15 used to carry
   `[data-template="chat"]` (Conversation) — a stepper with a gentle voice
   and a transcript beside it — deliberately kept separate from this one.
   That template is withdrawn and `chat-emerald` now redirects here, so the
   thread is the whole of the register rather than half of it. */

/* The chat-only blocks, off for every layout. `x-show` already gates them on
   `usesChatFlow`, which is false everywhere else — this is the second of the
   two guards, and it is the one that holds during the pre-Alpine paint before
   `x-cloak` is lifted. */
#so-booking-widget .so-bw-chatbar,
#so-booking-widget .so-bw-chatlog,
#so-booking-widget .so-bw-chatalt,
#so-booking-widget .so-bw-chatmenu { display: none; }

/* ── 36.1 · The shell is a messaging window ──────────────────────────────
   Narrow, because a thread that runs the width of a desktop card puts the
   avatar and the reply at opposite ends of the screen and stops reading as a
   conversation. 560px is the widest a two-sided thread stays legible at.

   The WIDTH is not set here — it is `--so-bw-tpl-maxw` in section 23.1, with
   the rest of the catalogue's table. A bare `max-width` on this selector is
   (1,2,0) and 23.1's sizing rule is (1,3,0), so it would lose silently and the
   thread would render at the 800px default; going through the token also lets
   the Studio's size radio scale it rather than replace it.

   No `overflow` here either, deliberately. Section 1b already clips the
   container's corners with `overflow: clip` precisely BECAUSE `hidden` makes
   it a scroll container, which silently kills `position: sticky` on everything
   inside — that is the bug that stopped the review step's commit bar pinning.
   A `[data-template]` rule scores (1,2,0) and would beat that (1,1,0) fix, so
   restating `hidden` here would have re-broken it for this layout alone. */
#so-booking-widget[data-template="chatbook"] .so-bw-container {
    border-radius: var(--so-bw-r-20);
    box-shadow: 0 18px 40px -24px rgba(16, 24, 48, .28);
    /* The thread runs tighter than the catalogue's 24px page inset — a chat
       canvas is not a page margin. Set as the TOKEN rather than as literal
       padding on the header, body and nav, which is the stylesheet's standing
       rule (section 12b: "architectures override the TOKENS, never these
       declarations") and what keeps those three regions sharing one left edge.
       widget-css-cascade.test.ts fails a literal x-inset on any of them. */
    --so-bw-gutter: 14px;
}

/* ── 36.2 · The header is a chat bar ─────────────────────────────────────
   A step counter and a numbered progress bar answer "how far through a form am
   I?", which is not a question a thread poses — the log itself is the
   progress, and every answer given is still on screen above the fold. The
   brand block goes too: the bar already names the business, and printing it
   twice six pixels apart reads as a rendering fault. */
#so-booking-widget[data-template="chatbook"] .so-bw-header {
    padding: 0;
    background: var(--so-bw-primary);
    border-bottom: 0;
}
#so-booking-widget[data-template="chatbook"] .so-bw-brand,
#so-booking-widget[data-template="chatbook"] .so-bw-step-indicator,
#so-booking-widget[data-template="chatbook"] .so-bw-stepbar,
#so-booking-widget[data-template="chatbook"] .so-bw-progress { display: none; }

#so-booking-widget[data-template="chatbook"] .so-bw-chatbar {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 12px var(--so-bw-gutter);
    color: #fff;
}
/* White disc, brand-coloured mark — the inverse of the thread's own avatar, so
   the bar reads as the app and the bubbles read as messages from it. */
#so-booking-widget[data-template="chatbook"] .so-bw-chatbar-avatar {
    flex: 0 0 auto;
    width: 36px;
    height: 36px;
    border-radius: 999px;
    background: #fff;
    /* `--so-bw-primary-ink`, not `--so-bw-primary`. The mark is drawn with
       currentColor, which makes it TEXT as far as contrast is concerned, and
       the stylesheet routes brand-as-text through the ink token everywhere —
       a pale brand primary on a white disc is otherwise invisible.
       widget-ui-consistency.test.ts pins the convention. */
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
    display: flex;
    align-items: center;
    justify-content: center;
}
#so-booking-widget[data-template="chatbook"] .so-bw-chatbar-avatar svg { width: 22px; height: 22px; }
#so-booking-widget[data-template="chatbook"] .so-bw-chatbar-id {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 1px;
}
#so-booking-widget[data-template="chatbook"] .so-bw-chatbar-name {
    font-size: 15px;
    font-weight: 700;
    line-height: 1.25;
    color: #fff;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
/* Hierarchy here is carried by SIZE and WEIGHT, not by fading the text out.
   White on the brand blue is the one contrast this bar has to spend, and
   dropping the subtitle to 70% opacity would have taken it under 4.5:1 at
   11.5px — the smallest text in the layout failing first, which is the usual
   way a header like this goes wrong. */
#so-booking-widget[data-template="chatbook"] .so-bw-chatbar-status {
    display: flex;
    align-items: center;
    gap: 5px;
    font-size: 11.5px;
    font-weight: 400;
    line-height: 1.35;
    color: #fff;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
/* The one literal colour in the bar. A presence dot reports availability, and
   recolouring it per brand would make the same signal mean different things on
   different sites — green is the convention it is borrowing. */
#so-booking-widget[data-template="chatbook"] .so-bw-chatbar-dot {
    flex: 0 0 auto;
    width: 6px;
    height: 6px;
    border-radius: 999px;
    background: #4ade80;
}

/* ── 36.3 · The ⋮ menu ───────────────────────────────────────────────────
   40px of target on a 28px glyph — the visual size stays quiet in the bar
   while the tappable area clears the touch minimum. */
#so-booking-widget[data-template="chatbook"] .so-bw-chatmenu {
    display: block;
    position: relative;
    flex: 0 0 auto;
}
#so-booking-widget[data-template="chatbook"] .so-bw-chatmenu-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    padding: 0;
    border: 0;
    border-radius: 999px;
    background: transparent;
    color: #fff;
    cursor: pointer;
}
#so-booking-widget[data-template="wide"] .so-bw-sumpanel__emptytitle {
    font-size: 13.5px;
    font-weight: 600;
    color: var(--so-bw-ds-n900, var(--so-bw-text));
}
#so-booking-widget[data-template="wide"] .so-bw-sumpanel__emptyhint {
    font-size: 12.5px;
    line-height: 1.45;
    color: var(--so-bw-ds-n500, var(--so-bw-text-secondary));
}
#so-booking-widget[data-template="wide"] .so-bw-sumpanel__itemicon {
    width: 34px;
    height: 34px;
    border-radius: 50%;
}
#so-booking-widget[data-template="wide"] .so-bw-sumpanel__itemname {
    font-size: 13.5px;
}
#so-booking-widget[data-template="wide"] .so-bw-sumpanel__totals,
#so-booking-widget[data-template="wide"] .so-bw-sumpanel__facts,
#so-booking-widget[data-template="wide"] .so-bw-sumpanel__section {
    padding-top: 12px;
    border-top-color: #ECEAE6;
}
/* The selected count, so the panel states the same fact the action bar does. */
#so-booking-widget[data-template="wide"] .so-bw-sumpanel__count {
    font-size: 12px;
    font-weight: 500;
    color: var(--so-bw-ds-n500, var(--so-bw-text-secondary));
}
#so-booking-widget[data-template="wide"] .so-bw-sumpanel__next {
    padding: 10px 11px;
    font-size: 11.5px;
    border-radius: var(--so-bw-r-10);
}
/* ── The block-theme `<p>` override, neutralised for this panel ──────────
   These three are `<p>` elements, and block themes force

     .entry-content.alignfull.wp-block-post-content… p
     { font-size: 18px !important }

   on every paragraph inside post content. Author `!important` beats any
   non-important author rule whatever its specificity, so the sizes declared
   above were being discarded on the front end while the Studio preview — an
   iframe outside `.entry-content` — rendered them correctly. That mismatch is
   what makes this look like a caching or build problem when it is neither.
   Only triggers at Widget Size = Full Width, when the wrapper gets `.alignfull`.

   The values are RESTATED rather than the originals being marked `!important`,
   because marking a base declaration important would make it beat the
   per-template overrides that are supposed to win — the cascade in this file
   depends on later template rules staying stronger. Scoped to
   `[data-template="wide"]`, and `.so-bw-sumpanel` is `display: none` for the
   other twenty-one architectures anyway, so nothing else can be touched.

   NOT A COMPLETE FIX. 67 of the widget's 76 `<p>` elements are inflated the
   same way on such a theme — policy panels, empty states, confirmation text,
   the staff/service side panels. The rest are hidden on this step, so they are
   left for a deliberate pass: the sound general fix is a single important
   `#so-booking-widget p` rule whose font-size READS a custom property, with
   each class assigning that property — custom properties sidestep the
   importance war entirely, because the assignment never competes with the
   theme's declaration. (Not written as a code sample here: the cascade test
   scans this file for property reads and would count an illustrative one as a
   real undeclared token.) Adding `!important` to 28 scattered class rules would
   break the per-template cascade instead. */
#so-booking-widget[data-template="wide"] .so-bw-sumpanel__count {
    font-size: 12px !important;
}
#so-booking-widget[data-template="wide"] .so-bw-sumpanel__next {
    font-size: 11.5px !important;
}
#so-booking-widget[data-template="wide"] .so-bw-sumpanel__almost {
    font-size: 12px !important;
}

/* ── S11.5 · The action bar ─────────────────────────────────────────────
   The footer was a tall empty band with a lone Continue button floating in it.
   It becomes a compact bar stating the selection in one line and committing on
   the right — the two things the bottom of a booking step is for.

   The three icon+value+label stat groups that were here are gone. At this width
   they were three quarters of the bar's content for facts the summary panel
   already shows in full, so they are collapsed to a single middot line. */
#so-booking-widget[data-template="wide"] .so-bw-nav {
    padding: 12px var(--so-bw-gutter);
    gap: 12px;
    background: #FCFBFA;
    /* The horizontal rule of the count / divider / Continue arrangement. It
       closes the list above and opens the commit bar below. */
    border-top: 1px solid #ECEAE6;
}
/* ── The vertical divider, between the count and Continue ────────────────
   GENERATED on the button rather than typed into the markup, following the
   precedent of the middot between the nav stats: a real element would be left
   stranded whenever its neighbour is hidden by `x-show`, and this bar hides its
   stats routinely (prices off, nothing selected yet). A pseudo-element cannot
   outlive the thing it belongs to. It hangs off the BUTTON rather than the stats
   because the button is the one of the two that is always present.

   ABSOLUTELY POSITIONED, and that is the correction that matters. As a flex item
   the pseudo-element sat INSIDE the button's own box — a pale hairline drawn on
   top of the dark fill, reading as a rendering fault rather than a separator
   (seen at Medium). Taking it out of flow puts it in the gap between the two,
   where a divider belongs.

   `left` is half the nav's 12px `gap`, so the rule lands on the centre line
   between the stats and the button; `top`/`bottom` inset it slightly so it reads
   as a divider rather than a border on the button. */
#so-booking-widget[data-template="wide"] .so-bw-nav .so-bw-btn--primary {
    position: relative;
}
#so-booking-widget[data-template="wide"] .so-bw-nav .so-bw-btn--primary::before {
    content: '';
    position: absolute;
    left: -6px;
    top: 3px;
    bottom: 3px;
    width: 1px;
    background: #E4E1DC;
}
#so-booking-widget[data-template="wide"] .so-bw-navstats {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0 6px;
    flex: 1 1 auto;
    justify-content: flex-start;
    min-width: 0;
    font-size: 13px;
    color: var(--so-bw-ds-n700, var(--so-bw-text-secondary));
}
/* One line, middot-separated. The icon tiles and the small captions are
   dropped: at 13px on one line a label under each value is noise. */
#so-booking-widget[data-template="wide"] .so-bw-navstat {
    display: inline-flex;
    align-items: baseline;
    gap: 0;
}
#so-booking-widget[data-template="wide"] .so-bw-navstat__icon,
#so-booking-widget[data-template="wide"] .so-bw-navstat__text small {
    display: none;
}
#so-booking-widget[data-template="wide"] .so-bw-navstat__text {
    display: inline;
}
#so-booking-widget[data-template="wide"] .so-bw-navstat__text strong {
    font-size: 13px;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    color: var(--so-bw-ds-n900, var(--so-bw-text));
}
/* The separator is generated, not typed into the markup — so it can never be
   left dangling when a stat is hidden by its own `x-show` (prices off, for
   instance). `+` only puts one BETWEEN two visible stats. */
#so-booking-widget[data-template="wide"] .so-bw-navstat + .so-bw-navstat::before {
    content: '·';
    margin: 0 7px 0 1px;
    color: var(--so-bw-ds-n300);
    font-weight: 400;
}
/* ==========================================================================
   S11.7 · LESS CHROME, MORE LIST
   ==========================================================================
   The header and the commit bar were spending 211px of a 760px card on
   themselves — header 115.7, nav 63, the credit line 32.7 (all measured) —
   leaving 546 for the thing the customer actually came to read. Every number
   below is a trim of PADDING or of a decorative box, not of a control's
   readable size, and the list gains all of it.

     header   115.7 -> ~83   (page padding 16/12 -> 10/8; the step bar loses
                              its own 8/8 band and its 4px top margin; dots
                              24 -> 22, which is still a legible two digits)
     nav      63.0  -> ~52   (padding 12 -> 8, button 38 -> 36)
     credit   32.7  -> ~25   (padding 8/10 -> 4/6)

   The primary button stops at 36px rather than going smaller. It was already
   under the 44px pointer-target guideline at 38, and it is the one control the
   whole step exists to reach — shaving a further 4px off the card is not worth
   making it harder to hit. */
#so-booking-widget[data-template="wide"] .so-bw-header {
    padding: 10px var(--so-bw-gutter) 8px;
    /* ── FIXED AT THE TOP ────────────────────────────────────────────────
       Inside the card the header already never moved: it is a sibling ABOVE
       `.so-bw-body`, and the body is what scrolls. What it did not survive was
       the PAGE scrolling — measured at 700x900, where the card is taller than
       the viewport, the header travelled from y=99 to y=85 as the page moved and
       kept going, taking the progress stepper off screen mid-booking.

       Sticky, not fixed. `position: fixed` would take the header out of flow, so
       the body would slide up under it and the header would keep its place on
       screen even after the widget had scrolled past — a bar belonging to a card
       that is no longer visible. Sticky is bounded by its containing block, so
       it pins while the card is on screen and leaves with it.

       This works because `.so-bw-container` clips with `overflow: clip` rather
       than `hidden`: `clip` is not a scroll container, so the header's scrollport
       is the document and it can pin against the viewport. `hidden` there would
       silently kill this — which is exactly why the container's own rule chose
       `clip`, and there is a test guarding it.

       `top` clears the WordPress admin bar when there is one. The bar is
       position: fixed over the page for logged-in users, so `top: 0` would slide
       the stepper underneath it — invisible to precisely the people previewing
       the widget. The variable is absent for logged-out visitors and falls back
       to 0. */
    position: sticky;
    top: var(--wp-admin--admin-bar--height, 0px);
    /* Above the body's scrolling content, below the commit bar's 5 — the bar is
       the thing that must stay reachable if the two ever meet. */
    z-index: 4;
    /* The background is declared in section K, but restate it here: a pinned bar
       with a transparent background shows the list through itself, and that has
       already been a bug in this file once (the removed S11.4 band). */
    background: var(--so-bw-ds-n0);
    /* Only once pinned does the header need to separate itself from the content
       passing beneath it. Cheap enough to carry always, and it doubles as the
       edge between the stepper and the list. */
    border-bottom: 1px solid #ECEAE6;
}
#so-booking-widget[data-template="wide"] .so-bw-step-indicator {
    margin-bottom: 0;
}
#so-booking-widget[data-template="wide"] .so-bw-stepbar {
    margin-top: 0;
    padding-block: 0;
}
#so-booking-widget[data-template="wide"] .so-bw-stepbar-dot {
    width: 22px;
    height: 22px;
    font-size: 11px;
}
#so-booking-widget[data-template="wide"] .so-bw-nav {
    padding: 8px var(--so-bw-gutter);
}

/* ══════════════════════════════════════════════════════════════════════════
   S11.9 · SECOND TRIM: the chrome gives up another 40px
   ══════════════════════════════════════════════════════════════════════════
   S11.7 took the three chrome rows from 211px to 161px. This takes them to
   ~121px, and the whole of it goes to the list. Measured composition before:

     header  83.7 = 10 pad + 22.3 step pill + 42.4 stepbar + 8 pad + 1 border
     nav     53.0 =  8 pad + 36 button + 8 pad + 1 border
     credit  24.7 =  4 pad + 13 text + 6 pad

   The step pill is the big one and it is pure waste: "Step 1 of 5" is ~66px of
   text holding a 22px line of its own, while the row below it — a 560px stepbar
   centred in a 1728px card — has some 580px of empty space on either side. The
   pill moves into that space. Everything else here is padding.

   The button stays at 36px. It was already under the 44px pointer-target
   guideline and it is the control the step exists to reach; the rest of the bar
   can give up room, that cannot. */

/* The pill joins the stepbar's row instead of taking one of its own. Absolute
   against the header, which is already `position: sticky` and therefore a
   containing block — no extra positioning needed on the parent.

   Gated at 768 and up: the pill needs the slack beside the stepbar, and that
   slack is what a narrow container does not have. At the tightest gated width
   the stepbar leaves ~83px a side against a ~66px pill; below the gate the pill
   stays in flow and keeps its own line, which is the honest fallback. */
    #so-booking-widget:where([data-cwmin~="768"])[data-template="wide"] .so-bw-step-indicator {
        position: absolute;
        right: var(--so-bw-gutter);
        top: 50%;
        transform: translateY(-50%);
        width: auto;
        margin: 0;
    }
#so-booking-widget[data-template="wide"] .so-bw-header {
    padding: 8px var(--so-bw-gutter) 6px;
}
/* A smaller dot and a tighter dot-to-label gap: the row is a progress
   indicator, not a diagram, and 22px of circle for one digit was generous. */
#so-booking-widget[data-template="wide"] .so-bw-stepbar-dot {
    width: 20px;
    height: 20px;
    font-size: 10.5px;
}
#so-booking-widget[data-template="wide"] .so-bw-stepbar-item {
    gap: 4px;
}
#so-booking-widget[data-template="wide"] .so-bw-nav {
    padding: 4px var(--so-bw-gutter);
}
/* ── The action bar's text starts where the cards start ──────────────────
   `.so-bw-navstats` measured L+33 against the L+21 every card sits on, so the
   "1 service selected · 45 min · ₹1,200" line was indented 12px past the list
   it summarises — a mismatch between two things stacked directly on top of one
   another.

   The 12px is the nav's own `gap`, spent on something invisible. The bar's
   second child is `<div x-show="step <= 1"></div>` (booking-widget.js) — an
   empty spacer that reserves the Back button's slot so `space-between` keeps
   Continue on the right during step 1. It is zero-width but RENDERED, and a
   rendered flex item collects gap like any other.

   Switched off rather than deleted from the markup: the other twenty-one
   architectures may lean on it, and here it earns nothing — `space-between`
   with the stats and Continue already pushes the button right on its own.
   `:empty` is exact: the div has no children and no whitespace, and it is the
   only empty one in the bar (the others are the two buttons and the stats).
   From step 2 on, Alpine sets `display: none` on it anyway, so this only
   changes step 1. */
#so-booking-widget[data-template="wide"] .so-bw-nav > div:empty {
    display: none;
}
/* The credit line's trim is applied at its own rule further down, which sits
   later in the file and would otherwise override anything stated here. */

/* ── S11.8 · ONE scroller: the body ────────────────────────────
   This section briefly gave the step column its own `overflow-y` so the list
   and the summary scrolled independently. Removed by request: `.so-bw-body` is
   the single scroll region, which is also what every other architecture does
   and what the shell's own invariant describes.

   The two findings behind it are worth keeping, because both are easy to trip
   over again in this layout:
     - `max-height: 100%` on either column does NOT bound it. A percentage
       max-height needs the flex container's height to be definite to the child,
       and with `align-items: flex-start` the item is sized by its own content
       (measured: a 1149.6px column inside a 596.6px body).
     - `align-items: stretch` alone does not fix that either, because
       `.so-bw-body` is `flex-wrap: wrap` — a MULTI-line flex container, where
       `stretch` sizes an item to its own line's content-derived cross size. It
       would also need `flex-wrap: nowrap`, which then has to be undone at the
       stacking tier, since stacking IS wrapping here.

   The summary panel keeps the `max-height: 100%; overflow-y: auto` it has always
   had at its own rule — that is a guard against a sticky box taller than its
   port never releasing, not a second scroll region. */

/* Prominent, but a button rather than a slab: 15px semibold, 36px tall. */
/* ONE height for both buttons, and it is what sets the bar's own height.
   They were 36 and 38, so the bar stood 49px tall on step 1 (Continue alone) and
   51px from step 2 on (once Back appears) — the footer changed height as the
   customer moved through the flow, which is the kind of 2px nobody can name but
   everybody feels. Stating it once means the bar cannot do that again.

   34px, down from 36, at the user's request after the same trade-off was flagged
   twice. This is my floor: 34 is already well under the 44px pointer-target
   guideline, and these are the two controls the whole step exists to reach. If
   the footer needs to be shorter still, the honest move is to drop the credit
   line rather than shrink the buttons further. */
#so-booking-widget[data-template="wide"] .so-bw-nav .so-bw-btn--primary,
#so-booking-widget[data-template="wide"] .so-bw-nav .so-bw-btn--secondary {
    min-height: 34px;
}
#so-booking-widget[data-template="wide"] .so-bw-nav .so-bw-btn--primary {
    padding: 0 20px;
    border-radius: var(--so-bw-r-10);
    font-size: 14px;
    font-weight: 600;
}
#so-booking-widget[data-template="wide"] .so-bw-nav .so-bw-btn--secondary {
    padding: 0 16px;
    border-radius: var(--so-bw-r-10);
    font-size: 13.5px;
    border-color: #DCD8D2;
}
/* Secondary, and small: it is our badge, not the customer's business.
   Padding trimmed 8/10 -> 4/6 as part of S11.7. Changed HERE rather than
   restated in that section, because this rule sits later in the file and would
   simply have won — the override I first wrote there had no effect at all
   (measured: the credit line stayed 32.7px). */
#so-booking-widget[data-template="wide"] .so-bw-powered {
    /* 2/4 now, from 8/10 originally via 4/6 (S11.7). The text is 13px of line in
       a 24.7px band — the padding was most of it. Below this the credit would
       collide with the action bar's border above it. */
    padding: 1px var(--so-bw-gutter) 2px;
    font-size: 10.5px;
    opacity: .75;
}

/* ── S11.6 · Tablet and phone ───────────────────────────────────────────
   The summary stacks BELOW the list, per the brief. Gated on the container
   width (`data-cw`) rather than the viewport, because the widget is embedded
   and the viewport says nothing about the column it was given.

   768, NOT a round 760. `data-cw` is stamped from a FIXED list —
   SO_BW_BREAKPOINTS in booking-widget.js: 380, 420, 480, 520, 540, 560, 600,
   640, 680, 700, 767, 768, 820, 860, 900 — and a value outside it silently
   matches nothing. The first version of this rule said 760 and therefore never
   applied at all: measured at a 420px viewport, the summary was still sitting
   in a 103px column beside a 182px list instead of stacking. Any new gate here
   has to pick from that list.

   `order: 2` rather than source order: the panel already comes after the step
   in the DOM, but the step host is `flex: 0 1` and would otherwise share a line
   with it at awkward widths. Full basis on both is what forces the stack. */
    #so-booking-widget:where([data-cw~="768"])[data-template="wide"] .so-bw-step-host,
    #so-booking-widget:where([data-cw~="768"])[data-template="wide"] .so-bw-sumpanel {
        flex: 0 0 100%;
    }
    #so-booking-widget:where([data-cw~="768"])[data-template="wide"] .so-bw-sumpanel {
        position: static;
        max-height: none;
        overflow-y: visible;
        order: 2;
    }
    /* Narrow: the header needs the whole line, so the search drops beneath it
       and spans. One column for the step, and the search returns to flow. */
    #so-booking-widget:where([data-cw~="768"])[data-template="wide"] .so-bw-step--service {
        grid-template-columns: minmax(0, 1fr);
    }
    #so-booking-widget:where([data-cw~="768"])[data-template="wide"] .so-bw-step--service > .so-bw-search {
        grid-column: 1;
        grid-row: auto;
        width: auto;
        max-width: none;
        margin: 0 0 10px;
    }
    #so-booking-widget:where([data-cw~="768"])[data-template="wide"] .so-bw-svchead {
        grid-template-columns: auto minmax(0, 1fr);
    }

/* ==========================================================================
   W13 · WEEK BOARD — PROPORTION PASS
   --------------------------------------------------------------------------
   W1-W12 gave the flow the right ELEMENTS. This gives them the right SIZE.

   Nothing here changes the flow, the step order, or what any control does: it
   is spacing, type scale, card composition, and the two states (selected,
   disabled) that were carrying too little or too much weight. Every rule is
   still scoped to [data-template="weekboard"].

   Measured before and after rather than eyeballed — the target was a header
   25-30% shorter, and the numbers are recorded against each block.
   ========================================================================== */

/* ── W13.1 · Width ───────────────────────────────────────────────────────
   1040px, up from the 1000px token and inside the 980-1080 band the brief
   sets. The board wants every pixel of it (seven columns of times) and the
   service grid is two 500px cards, which is the width a description reads
   comfortably at without a third column crowding in.

   The cap is restated for `data-size="full"` as well. That is a DELIBERATE
   narrowing of the Studio "Widget Size" control for this one template, and the
   only rule in this section that overrides an admin choice: Full Width on a
   1920px monitor gives a 1772px card whose seven day columns drift 250px apart
   and whose two service cards become billboards. If you want Full Width to
   mean full width here again, delete this one rule — the token above still
   holds every other size. */
#so-booking-widget[data-template="weekboard"] .so-bw-container { --so-bw-tpl-maxw: 1040px; }
/* `[data-template-id]` is not decoration: the section-23 rule this has to beat
   is `[data-size="full"][data-template][data-template-id] .so-bw-container` at
   (1,3,0), so a two-attribute selector loses and the cap silently does nothing
   (measured: container still 1248px). Every path BookingShortcode renders emits
   both template attributes, so matching on both is safe as well as necessary. */
#so-booking-widget[data-size="full"][data-template="weekboard"][data-template-id] .so-bw-container {
    max-width: min(100%, 1040px);
    margin-inline: auto;
}


/* ── W13.2 · The header, 25-30% shorter ──────────────────────────────────
   Measured at 159px before this block: a 22px step title, a 66px step bar, and
   71px of padding and gaps between and around them. Over a fifth of a 720px
   viewport spent on chrome before the customer sees a single service.

   The savings are taken from the SPACING, not from the information — every
   element the header had, it still has:

     · the two rows sit 14px apart instead of 28px
     · the block is padded 16px/20px instead of 24px/28px
     · the step-bar dots come down from 30px to 24px, which pulls the labels
       up with them

   Measured after: ~112px. The progress bar underneath is switched off because
   it is a second, coarser reading of the same fact the numbered bar states
   exactly — the base file keeps it as a narrow-width fallback, and the step
   bar here compresses gracefully instead (W13.7), so the fallback is dead
   weight on this template. */
/* Horizontal inset via the shared gutter token, not a literal.
   widget-css-cascade.test.ts rejects a literal x-padding on header/nav/step by
   design: it re-opens the "stair-step", where each band sits on a slightly
   different left edge. These rules had already drifted into it — the header was
   inset 20px and the nav 18px, and at the narrow tier 15px against 14px — so
   normalising them onto one token per tier is what the invariant is for.
   Vertical padding is untouched; only the x-slot now reads the token. */
#so-booking-widget[data-template="weekboard"] .so-bw-container {
    --so-bw-gutter: 20px;
}
#so-booking-widget[data-template="weekboard"] .so-bw-header {
    padding: 16px var(--so-bw-gutter) 14px;
}
#so-booking-widget[data-template="weekboard"] .so-bw-step-indicator {
    margin-bottom: 14px;
}
#so-booking-widget[data-template="weekboard"] .so-bw-step-label {
    font-size: 21px;
    font-weight: 600;
    line-height: 1.2;
    letter-spacing: -0.02em;
}
#so-booking-widget[data-template="weekboard"] .so-bw-step-count {
    font-size: 12px;
    font-weight: 500;
    font-variant-numeric: tabular-nums;
    color: var(--so-bw-ds-n500, var(--so-bw-text-secondary));
    /* Top-right, on the heading's first line rather than centred against a
       two-line title. */
    align-self: start;
}
#so-booking-widget[data-template="weekboard"] .so-bw-progress {
    display: none;
}

/* ── W13.3 · The step bar ────────────────────────────────────────────────
   Smaller circles, tighter rail, and the three states told apart by more than
   hue: PENDING is a hairline ring on white, DONE is a filled disc with a tick,
   CURRENT is a filled disc carrying its number with a soft accent halo. Read in
   greyscale the current step is still the only one with a ring around it. */
#so-booking-widget[data-template="weekboard"] .so-bw-stepbar {
    gap: 0;
    margin: 0;
}
#so-booking-widget[data-template="weekboard"] .so-bw-stepbar-dot {
    width: 24px;
    height: 24px;
    font-size: 11.5px;
    font-weight: 700;
    border-width: 1px;
    /* No inner shadow or gradient — a 24px disc has no room to describe one and
       it only muddies the tick. */
    box-shadow: none;
}
#so-booking-widget[data-template="weekboard"] .so-bw-stepbar-dot svg {
    width: 11px;
    height: 11px;
    stroke-width: 3.5;
}
/* The halo marks the current step without growing the disc, so the rail's
   baseline never shifts as the customer advances. */
#so-booking-widget[data-template="weekboard"] .so-bw-stepbar-item--active .so-bw-stepbar-dot {
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--so-bw-primary) 18%, transparent);
}
#so-booking-widget[data-template="weekboard"] .so-bw-stepbar-label {
    display: block;
    margin-top: 5px;
    font-size: 11px;
    font-weight: 500;
    letter-spacing: 0;
    white-space: nowrap;
}
#so-booking-widget[data-template="weekboard"] .so-bw-stepbar-item--active .so-bw-stepbar-label {
    font-weight: 600;
}
/* The connecting rail, thinned to a hairline and pinned to the disc's centre
   line so it does not ride up as the dots shrink. */
#so-booking-widget[data-template="weekboard"] .so-bw-stepbar-item:not(:first-child)::before {
    height: 2px;
    top: 12px;
}

/* ── W13.4 · The service step: header, search, chips ─────────────────────
   The step header comes down a size (it is a section heading under the page
   heading in the bar above, and at 20px the two competed), and the search field
   stops stretching. A 1000px-wide input for a two-word query reads as a form
   field the step is built around rather than a tool beside the list. */
#so-booking-widget[data-template="weekboard"] .so-bw-svchead {
    column-gap: 12px;
    margin: 0 0 14px;
}
#so-booking-widget[data-template="weekboard"] .so-bw-svchead__icon {
    width: 38px;
    height: 38px;
    border-radius: var(--so-bw-r-11);
}
#so-booking-widget[data-template="weekboard"] .so-bw-svchead__icon svg {
    width: 17px;
    height: 17px;
}
#so-booking-widget[data-template="weekboard"] .so-bw-svchead__title {
    font-size: 18px;
    font-weight: 600;
    letter-spacing: -0.012em;
}
#so-booking-widget[data-template="weekboard"] .so-bw-svchead .so-bw-step-desc {
    font-size: 13px;
}
#so-booking-widget[data-template="weekboard"] .so-bw-step--service .so-bw-search,
#so-booking-widget[data-template="weekboard"] .so-bw-step--staff .so-bw-search {
    max-width: 420px;
    margin-bottom: 10px;
}
/* Chips sit UNDER the search rather than beside it — the brief's order, and the
   one that keeps the row of four from being squeezed by a growing field. */
#so-booking-widget[data-template="weekboard"] .so-bw-catbar {
    gap: 7px;
    margin: 0 0 14px;
}
#so-booking-widget[data-template="weekboard"] .so-bw-catchip {
    min-height: 32px;
    padding: 0 12px;
    border-radius: 999px;
    font-size: 12.5px;
}
#so-booking-widget[data-template="weekboard"] .so-bw-catchip__icon {
    width: 13px;
    height: 13px;
}
/* Only the SELECTED chip carries the accent. An unselected chip is neutral, so
   the row reads as one live filter rather than five competing buttons. */
#so-booking-widget[data-template="weekboard"] .so-bw-catchip--on {
    background: color-mix(in srgb, var(--so-bw-primary) 10%, transparent);
    border-color: color-mix(in srgb, var(--so-bw-primary) 40%, transparent);
}

/* ── W13.5 · The service and staff cards ─────────────────────────────────
   The composition was right; the proportions were loose. Four changes:

     · the disc comes down from 56px to 48px (-14%), which is the size at which
       two initials still read at a glance but the tile stops being the loudest
       thing on a row it only labels
     · 18px padding, 12px gaps between cards, 13px radius
     · the border softens to the n200 hairline and the hover lifts by
       border-colour alone, not by a shadow that made eight cards flicker as the
       pointer crossed the grid
     · the bottom rail tightens onto one baseline

   Card heights are already equal across a row (W10 stretched the body); this
   keeps them equal while removing the slack that made a short description sit
   in a tall box. */
#so-booking-widget[data-template="weekboard"] .so-bw-step--service .so-bw-card-grid,
#so-booking-widget[data-template="weekboard"] .so-bw-step--staff .so-bw-card-grid {
    gap: 12px;
}
#so-booking-widget[data-template="weekboard"] .so-bw-step--service .so-bw-card:not(.so-bw-sp__panel),
#so-booking-widget[data-template="weekboard"] .so-bw-step--staff .so-bw-card:not(.so-bw-sp__panel) {
    column-gap: 13px;
    padding: 18px;
    border-radius: var(--so-bw-r-13);
    border-color: var(--so-bw-ds-n200);
    box-shadow: none;
    transition: border-color .14s, background .14s;
}
#so-booking-widget[data-template="weekboard"] .so-bw-step--service .so-bw-card:not(.so-bw-sp__panel):hover,
#so-booking-widget[data-template="weekboard"] .so-bw-step--staff .so-bw-card:not(.so-bw-sp__panel):hover {
    border-color: var(--so-bw-ds-n300);
    box-shadow: none;
}
#so-booking-widget[data-template="weekboard"] .so-bw-step--service .so-bw-card-media,
#so-booking-widget[data-template="weekboard"] .so-bw-step--staff .so-bw-card-icon--avatar {
    width: 48px;
    height: 48px;
    flex: 0 0 48px;
}
#so-booking-widget[data-template="weekboard"] .so-bw-step--service .so-bw-card-media-avatar,
#so-booking-widget[data-template="weekboard"] .so-bw-step--staff .so-bw-card-icon--avatar {
    font-size: 15px;
}
#so-booking-widget[data-template="weekboard"] .so-bw-step--service .so-bw-card-body,
#so-booking-widget[data-template="weekboard"] .so-bw-step--staff .so-bw-card-body {
    row-gap: 6px;
}

/* Type scale, stated once. These are the brief's sizes; the point of writing
   them together is that the four steps then share one hierarchy instead of
   each card class drifting to its own. */
#so-booking-widget[data-template="weekboard"] .so-bw-step--service .so-bw-card-title,
#so-booking-widget[data-template="weekboard"] .so-bw-step--staff .so-bw-card-title {
    font-size: 15.5px;
    font-weight: 600;
    line-height: 1.32;
}
#so-booking-widget[data-template="weekboard"] .so-bw-step--service .so-bw-card-meta,
#so-booking-widget[data-template="weekboard"] .so-bw-step--staff .so-bw-card-meta {
    font-size: 13px;
    line-height: 1.45;
    color: var(--so-bw-ds-n500, var(--so-bw-text-secondary));
}
/* Duration is the quieter of the two facts; price is what the decision turns
   on, so it is the heavier. Both tabular, so a column of prices lines up. */
#so-booking-widget[data-template="weekboard"] .so-bw-step--service .so-bw-card-badges {
    gap: 7px;
    padding-top: 2px;
}
#so-booking-widget[data-template="weekboard"] .so-bw-step--service .so-bw-badge {
    min-height: 27px;
    padding: 0 9px;
    border-radius: var(--so-bw-r-7);
    font-size: 13px;
    font-weight: 500;
    background: var(--so-bw-ds-n25);
}
#so-booking-widget[data-template="weekboard"] .so-bw-step--service .so-bw-badge--price,
#so-booking-widget[data-template="weekboard"] .so-bw-step--service .so-bw-badge--free {
    font-size: 14.5px;
    font-weight: 600;
    color: var(--so-bw-ds-n900, var(--so-bw-text));
}
#so-booking-widget[data-template="weekboard"] .so-bw-step--service .so-bw-card-pick,
#so-booking-widget[data-template="weekboard"] .so-bw-step--staff .so-bw-card-pick {
    min-height: 34px;
    padding: 0 14px;
    border-radius: var(--so-bw-r-9);
    font-size: 12.5px;
}

/* ── W13.6 · The selected state, dialled back ────────────────────────────
   The 5% accent wash plus a 1px inset ring plus a 1px border came to three
   overlapping declarations of the same fact, and on amber the fill read as a
   tan block that dropped the description's contrast.

   One thin amber border, a 3% wash, and the badge. The tick and the button's
   fill are what carry it; the card stays a white-ish surface you can read. */
#so-booking-widget[data-template="weekboard"] .so-bw-step--service .so-bw-card--selected,
#so-booking-widget[data-template="weekboard"] .so-bw-step--staff .so-bw-card--selected {
    border-color: color-mix(in srgb, var(--so-bw-primary) 55%, transparent);
    background: color-mix(in srgb, var(--so-bw-primary) 3%, var(--so-bw-ds-n0));
    /* The inset ring is gone: it doubled the border into a 2px amber frame. */
    box-shadow: none;
}
/* The description must stay legible on the wash — the one thing a "selected"
   treatment must not cost. */
#so-booking-widget[data-template="weekboard"] .so-bw-step--service .so-bw-card--selected .so-bw-card-meta,
#so-booking-widget[data-template="weekboard"] .so-bw-step--staff .so-bw-card--selected .so-bw-card-meta {
    color: var(--so-bw-ds-n700, var(--so-bw-text-secondary));
}
#so-booking-widget[data-template="weekboard"] .so-bw-step--service .so-bw-card--selected .so-bw-badge {
    background: color-mix(in srgb, var(--so-bw-primary) 9%, transparent);
}
#so-booking-widget[data-template="weekboard"] .so-bw-step--service .so-bw-card--selected .so-bw-badge--duration {
    color: var(--so-bw-ds-n700, var(--so-bw-text-secondary));
}
#so-booking-widget[data-template="weekboard"] .so-bw-step--service .so-bw-card-check,
#so-booking-widget[data-template="weekboard"] .so-bw-step--staff .so-bw-card-check {
    top: 10px;
    right: 10px;
    width: 21px;
    height: 21px;
}
#so-booking-widget[data-template="weekboard"] .so-bw-step--service .so-bw-card-check svg,
#so-booking-widget[data-template="weekboard"] .so-bw-step--staff .so-bw-card-check svg {
    width: 12px;
    height: 12px;
}
/* A ticked card already carries a badge in the corner; the button repeating the
   tick made two ticks 40px apart. The word alone changes. */
#so-booking-widget[data-template="weekboard"] .so-bw-step--service .so-bw-card-pick--on svg {
    display: none;
}

/* ── W13.7 · The action bar ──────────────────────────────────────────────
   Was 80px of nav plus 40px of powered-by: 120px of footer under a step that
   wanted the room. Now one compact summary rail — facts on the left, Continue
   on the right — with the badge demoted to a line of small print beneath it.

   The stats lose their icon tiles and their captions and become one line of
   text separated by middots, because at this height a 30px tile per fact is
   what was making the bar tall in the first place. */
#so-booking-widget[data-template="weekboard"] .so-bw-nav {
    gap: 14px;
    padding: 11px var(--so-bw-gutter);
    background: var(--so-bw-ds-n25);
    border-top: 1px solid var(--so-bw-ds-n100);
}
#so-booking-widget[data-template="weekboard"] .so-bw-navstats {
    gap: 0;
    justify-content: flex-start;
    /* Middots between the facts, drawn by the gaps rather than by markup, so
       the separator can never outlive the fact it follows. */
    column-gap: 0;
}
#so-booking-widget[data-template="weekboard"] .so-bw-navstat {
    gap: 0;
}
#so-booking-widget[data-template="weekboard"] .so-bw-navstat + .so-bw-navstat::before {
    content: "·";
    margin: 0 8px;
    color: var(--so-bw-ds-n300);
}
#so-booking-widget[data-template="weekboard"] .so-bw-navstat__icon {
    display: none;
}
#so-booking-widget[data-template="weekboard"] .so-bw-navstat__text {
    flex-direction: row;
    align-items: baseline;
    gap: 5px;
}
#so-booking-widget[data-template="weekboard"] .so-bw-navstat__text strong {
    font-size: 13px;
    font-weight: 600;
}
/* The captions ("Total duration", "Total (INR)") go: in a middot-separated run
   of three facts they tripled the word count for nothing the figures did not
   already say. */
#so-booking-widget[data-template="weekboard"] .so-bw-navstat__text small {
    display: none;
}
/* Enabled amber, disabled a quiet grey — not a faded amber, which reads as a
   button mid-transition rather than one that is waiting for you. */
#so-booking-widget[data-template="weekboard"] .so-bw-nav .so-bw-btn--primary {
    min-height: 40px;
    padding: 0 18px;
    border-radius: var(--so-bw-r-10);
    font-size: 14px;
    font-weight: 600;
}
#so-booking-widget[data-template="weekboard"] .so-bw-nav .so-bw-btn--primary:disabled {
    background: var(--so-bw-ds-n100);
    border-color: var(--so-bw-ds-n200);
    color: var(--so-bw-ds-n500);
    box-shadow: none;
    opacity: 1;
    cursor: not-allowed;
}
#so-booking-widget[data-template="weekboard"] .so-bw-nav .so-bw-btn--secondary {
    min-height: 40px;
    padding: 0 14px;
    border-radius: var(--so-bw-r-10);
    font-size: 13.5px;
}
/* Below the bar, and small enough not to compete with it. */
#so-booking-widget[data-template="weekboard"] .so-bw-powered {
    padding: 8px 0 10px;
    font-size: 10.5px;
    letter-spacing: 0.01em;
    color: var(--so-bw-ds-n400, var(--so-bw-ds-n500));
}
#so-booking-widget[data-template="weekboard"] .so-bw-powered__name {
    font-weight: 600;
    color: var(--so-bw-ds-n500);
}

/* ── W13.8 · Narrow containers ───────────────────────────────────────────
   Two columns hold as long as each card clears ~330px, which is true down to
   ~700px of container — so tablet keeps two. Below 640px the cards go
   one-up (W10) and the chrome compresses rather than wrapping:

   the step bar keeps its dots and its rail and DROPS its labels. That is the
   opposite of the usual call, and deliberate: the labels are what make it wide
   (five words on a rail), while the numbered discs and the tick still say how
   far along the customer is. The step's own name is in the heading directly
   above, so nothing is lost — where a wrapped two-row rail would cost more
   height than the whole compaction above saved. */
    #so-booking-widget:where([data-cw~="640"])[data-template="weekboard"] .so-bw-container {
        --so-bw-gutter: 15px;
    }
    #so-booking-widget:where([data-cw~="640"])[data-template="weekboard"] .so-bw-header {
        padding: 13px var(--so-bw-gutter) 11px;
    }
    #so-booking-widget:where([data-cw~="640"])[data-template="weekboard"] .so-bw-step-label {
        font-size: 18px;
    }
    #so-booking-widget:where([data-cw~="640"])[data-template="weekboard"] .so-bw-step-indicator {
        margin-bottom: 11px;
    }
    #so-booking-widget:where([data-cw~="640"])[data-template="weekboard"] .so-bw-stepbar-label {
        display: none;
    }
    #so-booking-widget:where([data-cw~="640"])[data-template="weekboard"] .so-bw-stepbar-dot {
        width: 21px;
        height: 21px;
        font-size: 10.5px;
    }
    #so-booking-widget:where([data-cw~="640"])[data-template="weekboard"] .so-bw-stepbar-item:not(:first-child)::before {
        top: 10px;
    }
    #so-booking-widget:where([data-cw~="640"])[data-template="weekboard"] .so-bw-step--service .so-bw-search,
    #so-booking-widget:where([data-cw~="640"])[data-template="weekboard"] .so-bw-step--staff .so-bw-search {
        max-width: 100%;
    }
    /* The bar stays put and stays whole — it is the only route forward. The
       facts wrap above the buttons rather than being dropped, because on a
       phone the summary IS the confirmation that a tap registered. */
    #so-booking-widget:where([data-cw~="640"])[data-template="weekboard"] .so-bw-nav {
        flex-wrap: wrap;
        padding: 10px var(--so-bw-gutter);
    }
    #so-booking-widget:where([data-cw~="640"])[data-template="weekboard"] .so-bw-navstats {
        display: flex;
        order: -1;
        flex: 0 0 100%;
        justify-content: center;
    }

/* ==========================================================================
   W14 · WEEK BOARD — THE !IMPORTANT OPT-OUTS
   --------------------------------------------------------------------------
   W13 asked for 18px card padding and a 12px grid gap and got neither. Both
   were measured, not assumed: padding computed to 0px and the gap to 14px,
   with `border-radius: 13px` from the SAME rule block applying correctly —
   which is the signature of losing to `!important` rather than to specificity.

   Two base guards are responsible, and both are deliberate:

     · section 26 (`#so-booking-widget[data-template] .so-bw-card--service`)
       pins flex-direction / align-items / padding / gap with `!important` so
       the "+ Add extras" strip stays a full-width bottom row. Its own comment
       says a template replacing the layout must restate all four.
     · section "7 · Space between the cards" pins
       `.so-bw-card-grid { gap: 14px !important }` as a floor, so no template
       can put two selected cards' accent edges 4px apart.

   Wide Canvas already takes this opt-out (see its `.so-bw-card--service`
   block); this is the same move for Week Board. `flex-direction` is NOT
   restated because these cards are `display: grid`, where it has no effect —
   restating it would only imply the layout is flex.
   ========================================================================== */
#so-booking-widget[data-template="weekboard"] .so-bw-step--service .so-bw-card--service:not(.so-bw-sp__panel) {
    align-items: stretch !important;
    padding: 18px !important;
    /* Row gap 6px, column gap 13px: the column gap is the space between the
       disc and the text, and the row gap only matters for the extras strip
       below, which wants less. */
    gap: 6px 13px !important;
}
#so-booking-widget[data-template="weekboard"] .so-bw-step--service .so-bw-card-grid,
#so-booking-widget[data-template="weekboard"] .so-bw-step--staff .so-bw-card-grid {
    gap: 12px !important;
}

/* ── The add-ons strip, kept full width ──────────────────────────────────
   This is what section 26's guard exists to protect, and turning the card into
   a two-column grid would have broken it: the toggle auto-places into column 1
   of row 2 — a 48px-wide column, under the disc — so on a service WITH add-ons
   the strip becomes a squeezed stub beside empty space.

   Not caught by the rendered walk: every service in the test catalogue carries
   `so-bw-card--no-extras`, so the element was hidden on all 28 cards. Found by
   reading section 26's rationale rather than by seeing it fail, which is the
   only way this one could have been found before a customer with add-ons
   configured hit it.

   `1 / -1` spans both tracks, restoring the full-width bottom strip. */
#so-booking-widget[data-template="weekboard"] .so-bw-step--service .so-bw-card-extras-toggle,
#so-booking-widget[data-template="weekboard"] .so-bw-step--service .so-bw-card-extras {
    grid-column: 1 / -1;
}
/* The toggle reads as the quiet third option on the card — the two loud ones
   are Select and the price — so it is a text affordance, not a third button. */
#so-booking-widget[data-template="weekboard"] .so-bw-step--service .so-bw-card-extras-toggle {
    justify-self: start;
    min-height: 30px;
    padding: 4px 0;
    border: 0;
    background: transparent;
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
    font-size: 12.5px;
    font-weight: 600;
}
#so-booking-widget[data-template="weekboard"] .so-bw-step--service .so-bw-card-extras-toggle:hover {
    text-decoration: underline;
}

/* ── W14.1 · The last of the header height ───────────────────────────────
   W13.2 brought the header from 159px to 135px — 15%, short of the 25-30%
   asked for. The measurement said where the rest was hiding: `.so-bw-stepbar`
   carries 8px of its own padding top and bottom, which the base file gives it
   as breathing room for a 30px dot. At 24px it does not need it, and 16px of
   the remaining excess is exactly that.

   Header after this block: ~112px, a 30% reduction, with every element the
   159px version had. */
#so-booking-widget[data-template="weekboard"] .so-bw-stepbar {
    padding: 0;
}
#so-booking-widget[data-template="weekboard"] .so-bw-header {
    padding: 14px var(--so-bw-gutter) 12px;
}
#so-booking-widget[data-template="weekboard"] .so-bw-step-indicator {
    margin-bottom: 11px;
}

/* ==========================================================================
   W15 · WEEK BOARD — CLOSING THE VOIDS INSIDE THE CARDS
   --------------------------------------------------------------------------
   W10 pinned the bottom rail with `align-content: space-between`, which was the
   wrong tool. `space-between` spreads a grid's slack across EVERY gap, not just
   the one above the last row — so a card with spare height opened a void
   between its title and its description as well as above its price row. The
   rendered walk shows both.

   `start` is correct here, and the rails still align, because the base card
   already reserves the height of the variable parts on every card in the grid:
   `.so-bw-card-meta--clamp` reserves both description lines whether or not the
   text fills them, and `.so-bw-card-more--ghost` reserves the "Read more" row
   on cards whose text is short. That mechanism is documented as deliberate and
   not a per-template decision — which means content height is already equal
   across a row, and packing to the top lines the rails up without asking the
   grid to distribute anything.

   Net effect: the slack collects once, at the bottom of the card, instead of
   being sprayed between every pair of lines. */
#so-booking-widget[data-template="weekboard"] .so-bw-step--service .so-bw-card-body,
#so-booking-widget[data-template="weekboard"] .so-bw-step--staff .so-bw-card-body {
    align-content: start;
    row-gap: 5px;
}
/* The rail keeps a little air above it so it reads as a separate band from the
   description rather than as its next line. */
#so-booking-widget[data-template="weekboard"] .so-bw-step--service .so-bw-card-badges {
    padding-top: 6px;
}
/* The banner is a single row and never had a rail, so it keeps centring. */
#so-booking-widget[data-template="weekboard"] .so-bw-step--staff .so-bw-card-grid > .so-bw-card:first-child .so-bw-card-body {
    align-content: center;
}

/* ==========================================================================
   W16 · WEEK BOARD — THE SELECTED CARD, MEASURED
   --------------------------------------------------------------------------
   W13.6 asked for a 3% wash and a 55% border and got neither: the card measured
   a 15% wash and a fully-saturated border. Two base mechanisms own selection,
   and both are (1,3,0)-or-better with !important, so a per-template rule of the
   same specificity WITHOUT !important cannot reach either:

     · SELECTION FEEDBACK (line ~9682) paints
       `background: <accent> 15% !important` and `border-color: <accent>
       !important`, so selection shows on all nineteen templates whatever their
       per-template-id card rules say.
     · the `::after` overlay (line ~14571) draws a 2px accent border INSIDE the
       border box, so selection is visible over a photo or a header band and
       cannot be clipped by a scroll container.

   Both are deliberate and neither is being removed — the overlay in particular
   is what makes selection survive a photographed card. What changes is weight:
   15% of amber behind 13px grey text is a tan block that drops the
   description's contrast, which is the one thing a "selected" treatment must
   not cost. So the wash comes down to 5% and the overlay's border to 1.5px:
   still a real amber frame and a real tint, on a surface you can still read.

   `!important` here is not decoration — it is the documented way to opt out of
   these two guards, and the comment above the overlay names it as such. */
#so-booking-widget[data-template="weekboard"] .so-bw-step--service .so-bw-card.so-bw-card--selected,
#so-booking-widget[data-template="weekboard"] .so-bw-step--staff .so-bw-card.so-bw-card--selected {
    background: color-mix(in srgb, var(--so-bw-primary) 5%, var(--so-bw-ds-n0)) !important;
    border-color: color-mix(in srgb, var(--so-bw-primary) 45%, transparent) !important;
    /* The base rule's 2px outer ring doubled the overlay into a 4px amber
       frame. The overlay alone states it. */
    box-shadow: none !important;
}
#so-booking-widget[data-template="weekboard"] .so-bw-step--service .so-bw-card.so-bw-card--selected::after,
#so-booking-widget[data-template="weekboard"] .so-bw-step--staff .so-bw-card.so-bw-card--selected::after {
    border-width: 1.5px;
}
/* Now that the wash is light, the description takes the normal secondary ink
   rather than being darkened to fight a tint that is no longer there. */
#so-booking-widget[data-template="weekboard"] .so-bw-step--service .so-bw-card--selected .so-bw-card-meta,
#so-booking-widget[data-template="weekboard"] .so-bw-step--staff .so-bw-card--selected .so-bw-card-meta {
    color: var(--so-bw-ds-n500, var(--so-bw-text-secondary));
}

/* ==========================================================================
   30 · TABBED RAIL — THE SHELL IS AS TALL AS THE VIEWPORT
   --------------------------------------------------------------------------
   Every other architecture is a CARD on a page: it sizes to its step, and the
   shared bound near the end of section 27 caps it at `min(760px, 100dvh -
   112px)` so it cannot outgrow the screen. Tabbed Rail is the one that wants
   the opposite. Its proposition is a standing two-pane workspace — steps as
   tabs across the top, the running order pinned to a rail on the right — and
   a rail is only a rail if it is a column. At 760px on a 1080px screen it was
   a wide card with a tall-ish sidebar; the composition never got to be itself.

   So the height stops being a ceiling and becomes a MEASUREMENT:

     • `height`, not `max-height`. The shared rule only ever PERMITTED height
       on a content-sized box, so a two-card service step collapsed to its
       floor and the rail collapsed with it. Now the shell is the viewport and
       the `1fr` middle row of the grid takes whatever the header and the
       commit bar leave — which is what the grid was written for and never got
       handed. The nav lands on the bottom edge on every step.
     • The 760px term is gone for this template only. It is what was binding:
       `min(760, 968)` on a 1080px screen is 760, so the viewport term never
       had a say on any normal desktop.
     • `100dvh - 112px`, NOT `100dvh`. The 112px is measured page chrome, not
       padding — the embed route renders a 16px body gutter top and bottom and
       an identity banner (~56px plus a 14px margin) above the card. Take the
       full viewport and those ~102px push the shell past the fold, which puts
       a scrollbar on the page beside the one the body is already drawing. The
       reasoning is stated in full at the shared bound; this rule keeps the
       same subtraction for the same reason.
     • `vh` first, then `dvh`. A browser without `dvh` keeps the `vh` line;
       one with it takes the second, which is what stops a phone measuring
       against the URL-bar-collapsed viewport and overhanging the screen.

   WHAT IS DELIBERATELY LEFT ALONE:

     • The `min-height: clamp(360px, 52vh, 560px)` floor from the release
       block. Below roughly a 470px-tall viewport the floor exceeds this
       height and wins, and the page scrolls instead — the honest outcome, and
       the same one the shared bound documents. Do not zero it to "fix" that.
     • Every container width below 768px. There the rail is not a side column
       at all: it collapses under the pane as a wrapping summary strip on an
       implicit fourth grid row, so a fixed shell height would have the header,
       that strip and the commit bar all bidding against the `1fr` body for a
       fixed budget. Content-driven is right there, which is what it already
       does.

   Gated on `data-cwmin~="768"` rather than a media query for the reason the
   rest of the file is: the widget responds to the column it was embedded in,
   not to the window. :where() keeps the pair at (1,2,0) — the same score as
   the shared bound it has to beat, which source order then decides, and this
   is the last word in the file. */
/* ── ASKED FOR EXPLICITLY: the full viewport, not the viewport minus chrome ──
   The `- 112px` above is the history of this number and the reason it existed;
   it is now REQUESTED as a plain `100dvh`, twice, and that is the instruction.
   What it means in practice, so the trade is on the record rather than a
   surprise: the shell is exactly as tall as the window, and the ~112px of page
   above it (the embed's 16px body gutter and the identity banner) is real, so
   the bottom ~112px of the shell starts below the fold and the PAGE gains a
   scrollbar beside the one the body already draws. The commit bar still lands
   on the shell's bottom edge — it is just that reaching that edge now takes a
   page scroll on a viewport-height window.

   A surface that wants the old behaviour back has a token for it rather than an
   edit: `--so-bw-vchrome` (see the shared bound near line 34500) is subtracted
   here, so `#so-booking-widget { --so-bw-vchrome: 112px }` on the embed
   restores the previous height without touching this rule. It defaults to 0
   here — full viewport — because that is what was asked for.

   `vh` first then `dvh` for the reason stated above: a browser without `dvh`
   keeps the first line, and one with it takes the second so a phone measures
   the real visible viewport rather than the URL-bar-collapsed one. */
#so-booking-widget:where([data-cwmin~="768"])[data-template="tabbed"] .so-bw-container {
    height: calc(100vh - var(--so-bw-vchrome, 0px));
    height: calc(100dvh - var(--so-bw-vchrome, 0px));
    max-height: calc(100vh - var(--so-bw-vchrome, 0px)) !important;
    max-height: calc(100dvh - var(--so-bw-vchrome, 0px)) !important;
}

/* ==========================================================================
   31 · THE FOUR ARCHITECTURES THAT KEEP THE CARD AS A ROW
   --------------------------------------------------------------------------
   The base card is a centred stack now (see the note at `.so-bw-card`). Four
   live architectures lay their cards out themselves and must not inherit it.
   Each is excluded here rather than at the base, so the base states one design
   and the exceptions state why they are exceptions:

     counter, roster   The card is a GRID and `.so-bw-card-top` is a grid item
                       (`grid-area: body`). A flex direction on the card is
                       inert there, but the direction and centring on card-top
                       are not — a kiosk tile built around large left-aligned
                       targets would have its text centred under a mark that
                       the grid has already placed elsewhere.
     weekboard, wide   Their service card sets `.so-bw-card-top { display:
                       contents }` so its children become grid items of the
                       card. That makes the flex properties inert — but NOT
                       `text-align`, which is an inherited property and passes
                       straight through a box that was never generated. Wide
                       Canvas is a comparison ROW whose whole proposition is
                       one column of aligned marks down the page; centred text
                       is the one thing it cannot have.

   Placed last in the file because three of the four declare their own card
   rules further up at the same specificity, and source order is what settles
   it.

   No `text-align` restore is needed: the base does not centre it (see the note
   at `.so-bw-card`), so weekboard and wide inherit nothing and need no rule
   here at all. What remains is the DIRECTION, and only for the two whose
   card-top is a grid item that has to stay a row. */
#so-booking-widget[data-template="counter"] .so-bw-card-top {
    flex-direction: row;
    align-items: center;
}

/* ── …and the seven whose card is a PICTURE card ──────────────────────────
   MEASURED, at a 1180px embed on the service step: these are the live
   architectures that do NOT override the card's flex direction, so the base
   column reaches them and the mark lands above the name (mark offset from the
   card's centre: 0px on every one of them). They are also, exactly, the
   layouts that render a cover band — so the card reads cover, mark, name,
   facts, top to bottom. `usesMediaCard` in booking-widget.js is the same list
   and is what puts the mark below the band; keep the two in step.

   LEFT-ALIGNED, not centred. A cover already centres the eye at the top of the
   card, and under it a centred name and a centred description make three
   different centre lines in a 260px column — the name's, the description's
   wrapped last line, and the pill row's two edges. One left edge for all of
   them is what makes a grid of these scannable. It is also why the mark goes
   flush left rather than staying in the middle of the card.

   The other fourteen measured as rows — mark beside name — and are left
   alone. (Accordion measured as a picture card and Conversation as a row;
   both are withdrawn, so each list is one shorter than when it was taken.) */
#so-booking-widget[data-template="hero"] .so-bw-card-top,
#so-booking-widget[data-template="deck"] .so-bw-card-top,
#so-booking-widget[data-template="daypart"] .so-bw-card-top,
#so-booking-widget[data-template="frost"] .so-bw-card-top,
#so-booking-widget[data-template="bundle"] .so-bw-card-top {
    align-items: flex-start;
    text-align: left;
}
/* ── ONE MARK PER CARD, AND IT LIVES IN THE PICTURE AREA ──────────────────
   A service card shows the service's IMAGE when it has one and the fallback
   initials tile when it does not — never both, and the fallback occupies the
   SAME frame the photograph would have: same area, same height, same crop,
   same corner radius, because it is literally the same element.
   `.so-bw-card-media` is always emitted and carries `--placeholder` exactly
   when `serviceImageCount(svc) === 0` (booking-widget.js), so the band already
   is the "image or fallback" slot — it just has to be allowed to do that job.

   This replaces the arrangement these five had before, which was the reported
   fault. It hid the band on the photoless card and moved the mark OUT into a
   small round tile above the name, so:

     - a photographed service drew the cover AND that tile — two identifying
       marks for one service, the second redundant the moment the first exists;
     - a photoless service drew neither a cover nor anything in its place, so
       the fallback sat in a different position, at a different size, with a
       different radius from the image it was standing in for.

   Both halves came from treating the tile as the mark and the band as
   decoration. Reversed here: the band is the mark's frame in both states, and
   the lead tile is not drawn on these five at all. The placeholder band's own
   base rule (section 16) already centres the 68px tile in it and gives it the
   quiet wash that reads as "no photograph" rather than "photograph failed".

   Card HEIGHT is the other thing this restores: the band exists so a photoless
   service is exactly as tall as a photographed one, which is what keeps a grid
   row level. With the band hidden on some cards and not others, one card in a
   row was a band taller than its neighbours.

   Tabbed Rail keeps its lead tile and is deliberately not in this list — it
   draws no service cover at all (see "COVER · the eight grid templates"), so
   its single mark is already the tile and there is nothing to double up. */
#so-booking-widget[data-template="hero"] .so-bw-card-lead,
#so-booking-widget[data-template="deck"] .so-bw-card-lead,
#so-booking-widget[data-template="daypart"] .so-bw-card-lead,
#so-booking-widget[data-template="frost"] .so-bw-card-lead,
#so-booking-widget[data-template="bundle"] .so-bw-card-lead {
    display: none;
}

/* ═══════════════════════════════════════════════════════════════════════
   SECTION 32 · THE SERVICE PAGER
   ═══════════════════════════════════════════════════════════════════════
   A page control under the service grid, rendered by the shared step markup
   and revealed by `servicePageCount > 1` — which is hard-wired to 1 unless
   `usesServicePager` (Tabbed Rail alone today), so the other twenty-two
   architectures never draw this and need no per-template gate here.

   Why it exists at all: Tabbed Rail is the one layout whose step is a
   fixed-height pane beside a rail (see section 30), so a twelve-service
   catalogue scrolls inside a box that cuts the last row mid-card. Paging turns
   that into two clean rows of five, which is also why the page size is a
   multiple of the row (see svcPerPage in booking-widget.js).

   Everything here is built from the shared tokens rather than from literals, so
   it inherits each template's radius, border and primary colour instead of
   pinning one look across twenty-three palettes. */
#so-booking-widget .so-bw-pager {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-wrap: wrap;
    gap: 6px;
    /* Breathing room above, none below: the pane's own padding provides the
       floor, and doubling it puts the pager visibly adrift from the grid it
       belongs to. */
    margin-top: 14px;
    /* Grid-column 1/-1 in case a pane ever puts the wrap itself in a grid —
       harmless in a flex/block wrap, and it stops the pager being treated as a
       card-sized track if one does. */
    grid-column: 1 / -1;
}
/* Prev / Next carry a word as well as an arrow, because an arrow alone in a
   pane this wide reads as a carousel control rather than as paging. */
#so-booking-widget .so-bw-pager__step,
#so-booking-widget .so-bw-pager__page {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 5px;
    /* 32px, matching the card's fact pills rather than the nav's buttons: this
       control sits with the grid, not with the commit bar. */
    min-height: 32px;
    min-width: 32px;
    padding: 0 9px;
    border: 1px solid var(--so-bw-border);
    border-radius: var(--so-bw-radius-md, 8px);
    background: var(--so-bw-card-bg, var(--so-bw-bg));
    color: var(--so-bw-text);
    font-family: inherit;
    font-size: 13px;
    font-weight: 600;
    /* line-height 1 would clip a descender in the page number at this size —
       the same trap the stepbar label documents. */
    line-height: 1.25;
    cursor: pointer;
    transition: background-color 140ms ease, border-color 140ms ease, color 140ms ease;
}
#so-booking-widget .so-bw-pager__step:hover:not(:disabled),
#so-booking-widget .so-bw-pager__page:hover {
    /* The BORDER takes the raw brand hue; the TEXT takes the ink token, which
       is the same hue darkened enough to hold contrast on a light card. A pale
       brand (champagne, frost) fails as 13px text and passes as a 1px edge, so
       the two are not interchangeable — see the ink token's own definition. */
    border-color: var(--so-bw-primary);
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}
/* The current page is the one piece of state in this control, so it takes the
   primary fill — an outline-only "current" is indistinguishable from hover. */
#so-booking-widget .so-bw-pager__page--on,
#so-booking-widget .so-bw-pager__page--on:hover {
    background: var(--so-bw-primary);
    border-color: var(--so-bw-primary);
    color: var(--so-bw-on-primary, #fff);
}
/* Disabled ends stay in place rather than disappearing: a Prev button that
   vanishes on page 1 shifts every other control sideways on the first press. */
#so-booking-widget .so-bw-pager__step:disabled {
    opacity: 0.45;
    cursor: default;
}
#so-booking-widget .so-bw-pager__pages {
    display: flex;
    align-items: center;
    gap: 4px;
}
/* The x-show pair lives inside this span, so it must not add a box of its own —
   `display: contents` would drop the flex child, and a block would add a gap
   per slot including the elided ones. */
#so-booking-widget .so-bw-pager__slot {
    display: inline-flex;
    align-items: center;
}
#so-booking-widget .so-bw-pager__gap {
    display: inline-flex;
    align-items: flex-end;
    min-width: 18px;
    justify-content: center;
    color: var(--so-bw-text-secondary, var(--so-bw-text));
    font-size: 13px;
    line-height: 1.25;
    user-select: none;
}
/* The "4 / 12" readout, pushed to the far end so it reads as a status rather
   than as another button in the row. */
#so-booking-widget .so-bw-pager__count {
    margin-left: auto;
    color: var(--so-bw-text-secondary, var(--so-bw-text));
    font-size: 12px;
    font-weight: 500;
    font-variant-numeric: tabular-nums;
}
/* ── Narrow panes ────────────────────────────────────────────────────────
   Two things go at the phone tier, and only two. The words beside the arrows
   go, because "Prev 1 2 … 12 Next 4 / 12" wraps onto three lines in a 380px
   pane; and the readout goes with them, because the numbered buttons already
   say where the customer is and the count is the least load-bearing of the
   three. The arrows, the numbers and the elision all stay — the control still
   works exactly as it does at desktop width, in a third of the room.

   Container width, not viewport: this pane is 320px narrower than the embed on
   the paging template, so a media query would strip the labels at the wrong
   moment on both sides. */
#so-booking-widget:where([data-cw~="480"]) .so-bw-pager__steptext,
#so-booking-widget:where([data-cw~="480"]) .so-bw-pager__count {
    display: none;
}
#so-booking-widget:where([data-cw~="480"]) .so-bw-pager {
    gap: 4px;
}


/* ==========================================================================
   S13 . THE COMP PASS
   ==========================================================================
   Written against a supplied design comp for the service step. The structure was
   already right - framed list beside a framed summary, stepper above, action bar
   below - so almost all of this is presentation.

   FOUR ITEMS HERE REVERSE EARLIER DECISIONS IN THIS FILE, on instruction. Each is
   marked REVERSAL at its rule with what it overrides, because the reasoning behind
   the original is still written above it and would otherwise read as a live rule
   somebody had broken:

     REVERSAL the row carries BOTH a checkbox and a plus/minus button (the rule
       under "The checkbox is OFF" argues for one control; the comp shows two)
     REVERSAL the summary lists three trust items, two of them unbacked by data
     REVERSAL the card regains its outer border
     REVERSAL the action bar regains its stat icons and captions

   The accent is NOT hardcoded. The comp is blue and so is this install, but only
   because `colors_customized` is set here; everything below reads
   `--so-bw-primary` (or `--so-bw-primary-ink` where the accent is text, which the
   ui-consistency suite enforces). */

/* -- REVERSAL A1 . The checkbox comes back --------------------------------
   Overrides "The checkbox is OFF" earlier in this file. That rule's argument
   stands on its own merits - two affordances for one decision, and their states
   can disagree - and it is kept above rather than deleted so the trade-off stays
   visible. The comp shows both, which is the instruction.

   The box is ALWAYS drawn; only the tick inside it is conditional. That is the
   inverse of the element's own default, where `--visible` reveals the whole thing
   on selection - the comp needs an empty square on unselected rows, which is what
   makes it read as a checkbox rather than as a tick appearing from nowhere. */
#so-booking-widget[data-template="wide"] .so-bw-step--service .so-bw-card-check {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    /* BACK INTO FLOW. Something upstream pins this element (`position: absolute`,
       measured) so it floats at the card's top-right as a corner tick badge -
       which is why `order` alone did nothing and the box rendered to the RIGHT of
       the plus button. The comp puts it first in the row, so it has to be a flex
       item again; `inset: auto` clears whatever offsets came with the pinning. */
    position: static;
    inset: auto;
    order: -3;
    flex: 0 0 auto;
    width: 20px;
    height: 20px;
    box-sizing: border-box;
    border: 1.5px solid var(--so-bw-ds-n300, #C9CDD6);
    border-radius: var(--so-bw-r-6);
    background: var(--so-bw-ds-n0, #fff);
    color: var(--so-bw-on-primary, #fff);
    /* `visibility`, not `opacity`: the base rule hides this with visibility, and
       an opacity-only override would leave it invisible but still hit-testable. */
    visibility: visible;
    opacity: 1;
    transition: background .15s, border-color .15s;
}
/* The tick and the multi-pick order badge belong to the SELECTED state only. */
#so-booking-widget[data-template="wide"] .so-bw-step--service .so-bw-card-check > svg,
#so-booking-widget[data-template="wide"] .so-bw-step--service .so-bw-card-check .so-bw-card-check-order {
    display: none;
}
#so-booking-widget[data-template="wide"] .so-bw-step--service .so-bw-card-check--visible {
    border-color: var(--so-bw-primary);
    background: var(--so-bw-primary);
}
#so-booking-widget[data-template="wide"] .so-bw-step--service .so-bw-card-check--visible > svg {
    display: block;
    width: 13px;
    height: 13px;
}
/* Multi-pick keeps its numbering: with two or more services ticked the badge
   shows the order they will run in. Losing that to a plain tick would drop
   information the customer cannot get anywhere else. Alpine's `x-show` handles
   which of the two is live, so both are simply allowed to draw here. */
#so-booking-widget[data-template="wide"] .so-bw-step--service .so-bw-card-check--visible .so-bw-card-check-order {
    display: block;
    font-size: 11px;
    font-weight: 700;
    line-height: 1;
    color: var(--so-bw-on-primary, #fff);
}

/* -- A2 . The commit button becomes a circular plus/minus -----------------
   Same button, same handler, same `aria-label` (it already reads "Select X" /
   "Selected: X"), restyled. The glyph is a pseudo-element BECAUSE the label is
   already carried by that aria-label - a decorative `content` string is fine
   here, where it would not be if it were the only thing naming the control.
   The two label spans and the tick are hidden rather than removed from the
   markup, so the other twenty-one architectures keep the named button they
   render today. */
#so-booking-widget[data-template="wide"] .so-bw-step--service .so-bw-card-pick {
    width: 34px;
    min-width: 34px;
    height: 34px;
    min-height: 34px;
    padding: 0;
    border-radius: 50%;
    font-size: 0;
}
#so-booking-widget[data-template="wide"] .so-bw-step--service .so-bw-card-pick > svg,
#so-booking-widget[data-template="wide"] .so-bw-step--service .so-bw-card-pick > span {
    display: none;
}
#so-booking-widget[data-template="wide"] .so-bw-step--service .so-bw-card-pick::before {
    content: '+';
    font-size: 19px;
    font-weight: 400;
    line-height: 1;
    /* Optical centring: the glyph's bounding box sits low in the em. */
    margin-top: -2px;
}
#so-booking-widget[data-template="wide"] .so-bw-step--service .so-bw-card-pick--on::before {
    /* U+2212 MINUS SIGN, not a hyphen: at 19px a hyphen is a short, high dash
       that does not read as the counterpart to the plus. */
    content: '\2212';
}

/* -- A3 . The tile shows the service's own icon ---------------------------
   `serviceIconHtml(svc)` is already bound in the markup and keyed off the admin's
   icon picker (resources/admin/data/serviceIcons.ts), so the comp's glyph tiles are
   real per-service data. But the helper FALLS BACK to a `<span>` of initials when
   a service has no icon chosen, which is the common case on a fresh install - so
   this rule sets no `font-size: 0` to "hide the text". It did at first, and that
   blanked every tile on this catalogue: 12 services, none with an icon picked,
   twelve empty squares. Only the SVG is sized; the initials keep the shared
   avatar-tile type. */
#so-booking-widget[data-template="wide"] .so-bw-step--service .so-bw-card-media,
#so-booking-widget[data-template="wide"] .so-bw-step--service .so-bw-card-icon--avatar {
    border-radius: var(--so-bw-r-12);
}
#so-booking-widget[data-template="wide"] .so-bw-step--service .so-bw-card-media svg,
#so-booking-widget[data-template="wide"] .so-bw-step--service .so-bw-card-icon--avatar svg {
    width: 21px;
    height: 21px;
}
#so-booking-widget[data-template="wide"] .so-bw-step--service .so-bw-card-media-img {
    border-radius: var(--so-bw-r-12);
}
/* The price takes the accent on the chosen row only, so the committed row is
   legible at a glance down a column of otherwise identical prices. */
#so-booking-widget[data-template="wide"] .so-bw-step--service .so-bw-card--selected .so-bw-badge--price {
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}

/* -- B . The search takes its own full-width row --------------------------
   S11.3 lifted it into the header's second grid column at 220px to save a line of
   height. The comp puts it back under the header at full width, which also
   retires the two-column header grid that placement needed. */
#so-booking-widget[data-template="wide"] .so-bw-step--service {
    grid-template-columns: minmax(0, 1fr);
}
#so-booking-widget[data-template="wide"] .so-bw-step--service > .so-bw-svchead {
    grid-column: 1 / -1;
}
#so-booking-widget[data-template="wide"] .so-bw-step--service > .so-bw-search {
    grid-column: 1 / -1;
    grid-row: auto;
    width: auto;
    max-width: none;
    height: 40px;
    margin: 0 0 10px;
}

/* -- C1 . The heading, at the comp's scale -------------------------------- */
#so-booking-widget[data-template="wide"] .so-bw-brand__title {
    font-size: 26px;
    font-weight: 700;
    letter-spacing: -0.02em;
}
#so-booking-widget[data-template="wide"] .so-bw-brand__subtitle {
    font-size: 13.5px;
    color: var(--so-bw-ds-n500, var(--so-bw-text-secondary));
}

/* -- C2 . Chips as pills -------------------------------------------------
   Icons stay generic: category names are free text from the admin, so there is no
   icon field to read, and a per-name guess would fall back silently on anything
   unexpected - including any non-English name. */
#so-booking-widget[data-template="wide"] .so-bw-catchip {
    min-height: 34px;
    padding: 0 12px;
    border-radius: var(--so-bw-r-9);
    font-size: 12.5px;
    font-weight: 500;
}
#so-booking-widget[data-template="wide"] .so-bw-catchip--on {
    border-color: color-mix(in srgb, var(--so-bw-primary) 45%, transparent);
    background: color-mix(in srgb, var(--so-bw-primary) 8%, var(--so-bw-ds-n0));
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
    font-weight: 600;
}

/* -- REVERSAL C3 . The action bar's stat icons and captions come back -----
   Overrides the rule that hid them on the grounds that a caption under each value
   is noise on a single 13px line. That was true of the one-line arrangement it
   was written for; the comp gives each stat an icon tile and two lines, where the
   caption is what makes the number readable without a middot doing the work. */
#so-booking-widget[data-template="wide"] .so-bw-navstat__icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
    width: 32px;
    height: 32px;
    border-radius: var(--so-bw-r-10);
    background: color-mix(in srgb, var(--so-bw-primary) 10%, var(--so-bw-ds-n0));
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}
#so-booking-widget[data-template="wide"] .so-bw-navstat__icon svg {
    width: 16px;
    height: 16px;
}
#so-booking-widget[data-template="wide"] .so-bw-navstat {
    gap: 10px;
}
#so-booking-widget[data-template="wide"] .so-bw-navstat__text {
    display: flex;
    flex-direction: column;
    line-height: 1.25;
}
#so-booking-widget[data-template="wide"] .so-bw-navstat__text small {
    display: block;
    font-size: 11px;
    font-weight: 500;
    color: var(--so-bw-ds-n500, var(--so-bw-text-secondary));
}
#so-booking-widget[data-template="wide"] .so-bw-navstat__text strong {
    font-size: 14.5px;
    font-weight: 700;
}
/* The generated middot separated three items on one line. With icon tiles and
   captions the groups separate themselves, and a middot between two-line blocks
   floats against nothing. */
#so-booking-widget[data-template="wide"] .so-bw-navstat + .so-bw-navstat::before {
    content: none;
}
#so-booking-widget[data-template="wide"] .so-bw-navstats {
    gap: 26px;
}

/* -- REVERSAL C4 . The card regains its outer border ----------------------
   Overrides the `border: none` added in S12. It was removed because two bordered
   columns inside a bordered card is a box-in-box-in-box; in the comp it works
   because the page behind the card is grey, so the outer edge reads as separation
   from the page rather than as a third nested frame. On a white page it will read
   as the box-in-box it is - a property of the host page, not of this rule. */
#so-booking-widget[data-template="wide"] .so-bw-container {
    border: 1px solid var(--so-bw-border, var(--so-bw-ds-n200));
}

/* ── The search field stops floating over the first row ──────────────────
   REPORTED, with a screenshot: on Tabbed Rail the search bar was covering the
   top of the first row of cards — their mark and their name — leaving a row
   that showed a description and two pills with nothing naming it.

   It is the sticky rule in section 30.3 doing exactly what it says: the field
   lives inside `.so-bw-step`, which is inside the scroll region, so it is
   pinned to the top of that region while the list travels underneath. That is
   the right behaviour for a long catalogue in a tall pane, and it is the wrong
   behaviour here for a reason that only became true today — this template now
   PAGES the catalogue at ten cards (section 32), so the list underneath is at
   most two rows and the pane scrolls by a hundred-odd pixels. A control pinned
   through a hundred pixels of travel buys nothing, and it costs the first row
   its heading for the whole of that travel.

   MEASURED: at a 1920x800 viewport the pane has 103px of scroll and the bar
   clears card 1 by 2px, which is why this did not show up in the earlier
   sweeps; a shorter pane (an admin bar plus page content above it, as in the
   report) gives the cards more travel than the gap and the row goes under.

   `static` and nothing else: the field keeps its size, its icon, its clear
   button and its place in the flow. It scrolls with the cards, which on a
   two-row page means it is on screen whenever the cards are. The other
   twenty-two architectures keep the sticky bar — they still scroll a full
   catalogue and genuinely need it.

   `relative`, NOT `static`, and the difference is not cosmetic — I shipped
   `static` first and it broke the field. The magnifier and the clear button are
   `position: absolute` against THIS element (stated at the `.so-bw-search-icon`
   rule, ~line 12511), so its `position` is their containing block. `static`
   takes it out of the chain, the pair resolve against `.so-bw-container`
   instead, and the icon parks at a fixed spot on the SHELL: MEASURED at
   `top: 245.8px`, `left: 13px` — 11px outside the search box's own left edge
   and staying there while the field moves. `relative` participates in
   positioning without pinning anything, so the overlap fix stands and the icons
   are back on the input. The 38px `padding-left` reserving space for the
   magnifier is unchanged and now lines up with it again. */
#so-booking-widget[data-template="tabbed"] .so-bw-search {
    position: relative;
    /* z-index goes with the sticky behaviour. Left at 4 this would still be a
       stacking context sitting above the cards for no reason, and the selected
       card's ring is drawn on the card — a stray context above it clips the
       ring's top edge on whichever card sits directly below the field. */
    z-index: auto;
}

/* -- C5 . The step pill moves aside for the brand header -----------------
   S11.9 pins the "Step 1 of 6" pill to the header's vertical centre, which is
   right when the header holds only the stepper. With the brand header switched on
   (Studio -> title + subtitle) the header is 117px instead of 53px, and "centre"
   lands the pill ON the subtitle - measured pill top 146 against a subtitle
   occupying 142-162.

   `:has()` rather than a new attribute or a JS flag: the condition IS "this header
   contains a brand block", and that is exactly what the selector says. It also
   means the no-brand case keeps the centred pill it was designed for, with no
   second rule to keep in step.

   Aligned to the title's own cap line rather than the header's padding edge, so
   the two read as one row. */
#so-booking-widget[data-template="wide"] .so-bw-header:has(.so-bw-brand) .so-bw-step-indicator {
    top: 10px;
    transform: none;
}

/* -- C6 . A bigger progress bar, and room for the pager ------------------
   The stepper was trimmed hard in S11.9 (dots 20px, labels 11.5px) when the
   header was the thing giving up space. With the brand header above it the
   header is a block of its own again, and at 20px the dots read as small change
   next to a 26px title. Back up to 26px, which is the size the numerals were
   drawn for, and the labels with them.

   The track widens with the dots: at 560px six 26px dots leave the connectors
   doing most of the work, which is the "rule with numbers sitting on it" look the
   original 760px cap was reduced to avoid. 640px is the balance. */
#so-booking-widget[data-template="wide"] .so-bw-stepbar {
    max-width: min(100%, 640px);
}
#so-booking-widget[data-template="wide"] .so-bw-stepbar-dot {
    width: 26px;
    height: 26px;
    font-size: 12.5px;
}
#so-booking-widget[data-template="wide"] .so-bw-stepbar-label {
    font-size: 12.5px;
}
#so-booking-widget[data-template="wide"] .so-bw-stepbar-item {
    gap: 6px;
}

/* The pager sits inside the framed list, under the five rows. The shared
   `.so-bw-pager` rules already style it; this only gives it separation from the
   last card and keeps it on the list's own rail. */
#so-booking-widget[data-template="wide"] .so-bw-step--service .so-bw-pager {
    margin-top: 12px;
    padding-top: 12px;
    border-top: 1px solid #ECEAE6;
}

/* -- C7 . The Booking Summary scrolls, with no visible bar ---------------
   Bounded and scrollable, and the bar is hidden rather than removed. Scrolling
   itself is untouched - wheel, trackpad, touch, keyboard and find-in-page all
   still reach the content; only the paint is suppressed. That distinction is the
   whole reason this is acceptable rather than a cover-up.

   `position: static`, deliberately, even though the bound is back. The base rule
   pairs the bound WITH `position: sticky`, and the bound exists because a sticky
   box taller than its scrollport pins at `top` and never releases, stranding what
   sits below it. Dropping the pin removes that failure mode entirely, and the pin
   buys little now the list is paged to five rows and the columns are a similar
   height.

   `scrollbar-gutter: auto`: `stable` would reserve a track for a bar that is
   never painted, leaving a dead strip down the panel's right edge.

   THE TWO-ENGINE SPLIT IS NOT OPTIONAL. Declaring `scrollbar-width` unguarded
   switches Chromium off the `-webkit-` pseudo-elements for the element, and this
   file styles `.so-bw-body`'s bar through exactly those - so an unguarded
   declaration here can silently unpaint a bar somewhere else. It goes behind
   `@supports not selector(::-webkit-scrollbar)`, which is the pattern already
   established twice in this file for the same reason. */
#so-booking-widget[data-template="wide"] .so-bw-sumpanel {
    position: static;
    max-height: 100%;
    overflow-y: auto;
    overscroll-behavior: contain;
    scrollbar-gutter: auto;
}
/* Chromium and WebKit: the bar occupies no space and paints nothing. */
#so-booking-widget[data-template="wide"] .so-bw-sumpanel::-webkit-scrollbar {
    width: 0;
    height: 0;
    display: none;
}
/* Firefox and anything without the pseudo-elements. Gated so Chromium never
   sees it. */
@supports not selector(::-webkit-scrollbar) {
    #so-booking-widget[data-template="wide"] .so-bw-sumpanel {
        scrollbar-width: none;
    }
}

/* ==========================================================================
   33 · TABBED RAIL — THE RIGHT PANEL, REDESIGNED
   --------------------------------------------------------------------------
   MEASURED FIRST, on the live install at a 900px-tall viewport. The panel is
   858px tall (it is the viewport-height shell's full-height column) and held:

     rail-head      34px   eyebrow + step count
     rail-list     702px   FOUR 40px rows — 160px of content in a 702px box
     rail-total     43px   sitting BELOW the box, detached from it
     tail           28px

   So ~540px of empty bordered white, then a total floating under it with a
   hairline of its own. That is the "big empty area with TOTAL outside the box"
   in the report, and neither half of it was a styling accident:

     · the void is `.so-bw-rail-list { flex: 1 1 auto }`, which is right where
       the rail is a short strip and wrong where it is a full-height column;
     · the detachment is structural — `.so-bw-rail-total` is a SIBLING of the
       list in the shared markup, not a child of it, on all eleven rail
       architectures.

   The markup is left alone. Both are fixed here, in CSS, by making the two
   siblings read as one card: the list stops growing and gives up its bottom
   corners, the total takes them, and the seam between them becomes an
   internal divider instead of two facing borders. One object, no void.

   THE ROW ALSO CHANGES SHAPE. It was a baseline-aligned `space-between` pair —
   label left, value hard right — which is a table, and a table of four rows
   with three ragged right edges in a 271px column reads as debris. Label above
   value, both flush left under the glyph, gives the panel one left edge and
   lets a long service name use the full width instead of wrapping against the
   label. It also buys the touch target: the row is a control on this template
   (it jumps back to the step that set it) and at 40px it was under the 44px
   floor.

   Scoped to this template throughout. The other ten rail architectures keep
   the row they have — several are horizontal strips where a stacked row would
   be wrong, and this is not theirs to inherit. */

/* ── WHY EVERY LAYOUT RULE BELOW IS GATED ────────────────────────────────
   `:not(:where([data-cw~="900"]))` — the rail is only a COLUMN at or above a
   900px container. Below it, section 11's stacking rule moves it to
   `grid-row: 4` as a horizontal STRIP under the pane, and the shared sheet
   turns the list into a wrapping row (`flex-direction: row; flex-wrap: wrap;
   flex: 1 1 100%` at `[data-cw~="680"]`).

   CAUGHT BY THE 23-LAYOUT AUDIT, which I had shipped this section without
   re-running: 710 blocking `escaped` findings, every one of them
   `.so-bw-rail-k` / `.so-bw-rail-v` on this template. `flex: 0 0 auto` below
   outranks that `flex: 1 1 100%`, so in strip mode the list stopped taking the
   full width and shrink-wrapped its rows into ONE horizontal line running off
   the side of the widget — the findings show each row landing a row-width
   further right than the last (outLeft −31, then −1799, then −2139 on a 320px
   embed). Nothing was clipping it, which is exactly why the audit calls it
   escaped rather than hidden.

   So the redesign applies where the rail is a column and the strip keeps the
   layout that works for it. The purely cosmetic rules — the glyph tile, the
   pending colours, the focus ring, the total's type size — stay ungated: they
   are correct in both modes and were never what broke.

   `:where()` keeps the negation at zero, so these stay (1,2,0) — the same
   score they had, decided by source order as before. */

/* 1 · The card: list and total stop being two boxes ---------------------- */
#so-booking-widget:not(:where([data-cw~="900"]))[data-template="tabbed"] .so-bw-rail-list {
    /* The void. `0 0 auto` makes the box hug its rows; the space below it is
       the rail's own background, which is what a summary that is not yet full
       should look like. */
    flex: 0 0 auto;
    padding: 2px 14px;
    /* Bottom corners and bottom border go to the total below. */
    border-bottom: 0;
    border-bottom-left-radius: 0;
    border-bottom-right-radius: 0;
}
#so-booking-widget:not(:where([data-cw~="900"]))[data-template="tabbed"] .so-bw-rail-total {
    /* Same surface, same edge, no gap — the seam is a divider now, not the
       space between two cards. */
    margin-top: 0;
    padding: 13px 14px;
    border: 1px solid var(--so-bw-border);
    border-top: 1px solid var(--so-bw-ds-n100);
    border-radius: 0 0 var(--so-bw-ds-radius-card, 12px) var(--so-bw-ds-radius-card, 12px);
    background: var(--so-bw-card-bg);
}
/* The last row's hairline would double with the total's top border. */
#so-booking-widget:not(:where([data-cw~="900"]))[data-template="tabbed"] .so-bw-rail-row:last-child {
    border-bottom: 0;
}

/* 2 · The row: label over value, one left edge -------------------------- */
/* `:not(--tally)` is not tidiness, it is a bug I made and measured: the bundle
   tally row is hidden with `display: none` at (0,1,0), and a bare
   `[data-template="tabbed"] .so-bw-rail-row { display: block }` scores (1,1,0)
   and outranks it — so "SELECTED / 0 services" appeared in the panel on this
   template, a fifth row that only Dusk is supposed to render. Excluding it here
   leaves its `display` untouched, which is the only declaration keeping it
   hidden. Everything else in this block is safe for it either way. */
#so-booking-widget:not(:where([data-cw~="900"]))[data-template="tabbed"] .so-bw-rail-row:not(.so-bw-rail-row--tally) {
    display: block;
    /* 44px: this row is a control here. The padding does the work rather than
       a fixed height, so a two-line service name grows the row instead of
       overflowing it. */
    min-height: 44px;
    padding: 9px 0;
    border-radius: var(--so-bw-r-7);
    transition: background-color 160ms ease;
}
#so-booking-widget[data-template="tabbed"] .so-bw-rail-k {
    display: flex;
    align-items: center;
    gap: 8px;
    font-family: var(--so-bw-mono);
    font-size: 10px;
    letter-spacing: 0.11em;
}
#so-booking-widget:not(:where([data-cw~="900"]))[data-template="tabbed"] .so-bw-rail-v {
    display: block;
    /* 20px tile + 8px gap: the value starts where the LABEL starts, not where
       the glyph does, so the panel has a single text edge. */
    padding-left: 28px;
    margin-top: 3px;
    font-size: 13px;
    font-weight: 600;
    text-align: left;
    line-height: 1.35;
}
/* The glyph gets a tile, so the four facts are scannable by shape rather than
   by reading four small mono labels. Tinted from the brand token, not a
   literal, so it follows the admin's palette. The flat neutral is declared
   first as the fallback for engines without color-mix — legible rather than
   absent — and @supports upgrades it. */
#so-booking-widget[data-template="tabbed"] .so-bw-rail-icon {
    width: 20px;
    height: 20px;
    margin-right: 0;
    border-radius: var(--so-bw-r-6);
    background: var(--so-bw-ds-n50, #f4f4f5);
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
    opacity: 1;
    vertical-align: 0;
    flex-shrink: 0;
}
@supports (background: color-mix(in oklab, red 10%, transparent)) {
    #so-booking-widget[data-template="tabbed"] .so-bw-rail-icon {
        background: color-mix(in oklab, var(--so-bw-primary) 12%, transparent);
    }
}
#so-booking-widget[data-template="tabbed"] .so-bw-rail-icon svg {
    width: 12px;
    height: 12px;
}

/* 3 · Pending vs decided ------------------------------------------------- */
/* A row whose fact is not chosen yet says so by weight as well as by colour —
   the em dash is the text, the dimming reinforces it. The tile drops to
   neutral, so the tinted tiles read as "done" down the column. */
#so-booking-widget[data-template="tabbed"] .so-bw-rail-row--unset .so-bw-rail-v {
    color: var(--so-bw-ds-n500);
    font-weight: 500;
}
#so-booking-widget[data-template="tabbed"] .so-bw-rail-row--unset .so-bw-rail-icon {
    background: var(--so-bw-ds-n50, #f4f4f5);
    color: var(--so-bw-ds-n500);
}

/* 4 · The rows that are buttons look like it -----------------------------
   Hover and a visible keyboard ring, both absent before. The ring is inset
   because the row sits inside the card's 14px inset and an outward ring would
   clip against the border. */
#so-booking-widget:not(:where([data-cw~="900"]))[data-template="tabbed"] .so-bw-rail-row--link {
    margin-inline: -8px;
    padding-inline: 8px;
    cursor: pointer;
}
#so-booking-widget[data-template="tabbed"] .so-bw-rail-row--link:hover {
    background: var(--so-bw-ds-n25, rgba(0, 0, 0, 0.03));
}
#so-booking-widget[data-template="tabbed"] .so-bw-rail-row--link:focus-visible {
    outline: 2px solid var(--so-bw-primary);
    outline-offset: -2px;
    border-radius: var(--so-bw-r-7);
}

/* 5 · The total reads as the answer ------------------------------------- */
#so-booking-widget[data-template="tabbed"] .so-bw-rail-total span:last-child {
    font-size: 19px;
}

/* The row transition is the only motion added here, and it goes under reduced
   motion like the rest of the file. */
@media (prefers-reduced-motion: reduce) {
    #so-booking-widget[data-template="tabbed"] .so-bw-rail-row {
        transition: none;
    }
}

/* Pager focus ring, same reason and same shape: these buttons were the one new
   control in section 32 with no keyboard affordance of its own, and this
   stylesheet declares rings per control rather than once globally (see the
   `:focus:not(:focus-visible)` note in section 17). */
#so-booking-widget .so-bw-pager__step:focus-visible,
#so-booking-widget .so-bw-pager__page:focus-visible {
    outline: 2px solid var(--so-bw-primary);
    outline-offset: 2px;
}

/* -- C8 . A scroll port per column ---------------------------------------
   The list and the summary scroll independently again: each is bounded to the
   body's height and carries its own `overflow-y`, so working down a long
   catalogue no longer drags the summary out of view, and a long summary no longer
   pushes the list.

   `.so-bw-body` KEEPS its own `overflow-y: auto` - it is the shell's single scroll
   region for every other architecture and the ui-consistency suite pins it. With
   both children bounded it simply never has anything left to scroll.

   `max-height: 100%` resolves because the body's height is definite; `min-height:
   0` is the load-bearing half, since a flex item defaults to `min-height: auto`
   and would refuse to shrink below its content - the column would grow instead of
   scrolling and the body would overflow exactly as before.

   NO `scrollbar-width` here. The list keeps a visible bar (it is the main reading
   surface, and the bar is the only thing that says "there is more of this page"),
   and the summary's is hidden at its own rule through the `@supports` split.
   Declaring the standard property unguarded would switch Chromium off the
   `-webkit-` pseudo-elements and unpaint bars elsewhere in this file. */
/* `max-height: 100%` DOES NOT WORK HERE, and this file already recorded why: a
   percentage max-height needs the flex container's height to be definite to the
   child, and `.so-bw-body` is `flex-wrap: wrap` - a multi-line container, whose
   items are sized by their own line. Measured: both columns kept `overflow: auto`
   and neither scrolled, while the body scrolled instead.

   `nowrap` + `stretch` is the mechanism that does work: on a single line, stretch
   sizes each column to the body's own height, which makes it a real bound and
   `overflow-y` meaningful. Wrapping is restored at the stacked tiers below,
   because stacking here IS wrapping.

   Consequence, stated because it is visible: the summary is now as tall as the
   list rather than as tall as its contents, so a short summary shows empty space
   below its last row. That is the price of it having its own scroll port. */
#so-booking-widget[data-template="wide"] .so-bw-body {
    flex-wrap: nowrap;
    align-items: stretch;
}
#so-booking-widget[data-template="wide"] .so-bw-step-host,
#so-booking-widget[data-template="wide"] .so-bw-sumpanel {
    max-height: none;
    min-height: 0;
    overflow-y: auto;
    /* A wheel gesture that reaches the end of a column stops there rather than
       chaining to the page: two adjacent ports that hand off to the document feel
       like one broken one. */
    overscroll-behavior: contain;
}
/* Stacked, there are no columns to scroll separately, and a column port inside
   the body's port is two nested regions for one gesture. Both tiers release it;
   the summary's own bound is released beside it. */
    #so-booking-widget:where([data-cw~="900"])[data-template="wide"] .so-bw-step-host,
    #so-booking-widget:where([data-cw~="900"])[data-template="wide"] .so-bw-sumpanel,
    #so-booking-widget[data-size="small"][data-template="wide"] .so-bw-step-host,
    #so-booking-widget[data-size="small"][data-template="wide"] .so-bw-sumpanel {
        max-height: none;
        overflow-y: visible;
    }
    /* And the wrapping comes back with them - with `nowrap` the two `flex: 0 0
       100%` columns would sit on one line and overflow sideways instead of
       stacking. `flex-start` too, so a short summary is not stretched to match a
       long list once they are one on top of the other. */
    #so-booking-widget:where([data-cw~="900"])[data-template="wide"] .so-bw-body,
    #so-booking-widget[data-size="small"][data-template="wide"] .so-bw-body {
        flex-wrap: wrap;
        align-items: flex-start;
    }

/* -- C9 . The header shares one axis -------------------------------------
   The step bar was already centred (measured 119px each side at Medium, 459px at
   Full) while the brand block above it was left-aligned, so the two read as
   misaligned even though neither was wrong on its own. Centring the block puts the
   mark, the title, the subtitle and the bar on one vertical axis.

   The mark and the title are a two-column grid inside `.so-bw-brand`, so centring
   is `justify-content` on that grid rather than `text-align` - the latter would
   centre the text INSIDE its column and leave the pair still pinned left. The
   subtitle sits in the same column as the title, hence the separate `text-align`
   for it.

   The step pill is untouched: it is absolutely positioned at the top right and
   belongs to the card's edge, not to this axis. */
#so-booking-widget[data-template="wide"] .so-bw-brand {
    justify-content: center;
    text-align: center;
}
#so-booking-widget[data-template="wide"] .so-bw-brand__text {
    text-align: center;
}
#so-booking-widget[data-template="chatbook"] .so-bw-chatmenu-btn:hover {
    background: rgba(255, 255, 255, .18);
}
#so-booking-widget[data-template="chatbook"] .so-bw-chatmenu-btn:focus-visible {
    outline: 2px solid #fff;
    outline-offset: -2px;
}
#so-booking-widget[data-template="chatbook"] .so-bw-chatmenu-pop {
    position: absolute;
    top: calc(100% + 6px);
    right: 0;
    z-index: 40;
    min-width: 168px;
    padding: 6px;
    border-radius: var(--so-bw-r-12);
    background: var(--so-bw-ds-n0);
    border: 1px solid var(--so-bw-ds-n200);
    box-shadow: 0 12px 28px -12px rgba(16, 24, 48, .3);
}
#so-booking-widget[data-template="chatbook"] .so-bw-chatmenu-item {
    display: flex;
    align-items: center;
    gap: 10px;
    width: 100%;
    min-height: 40px;
    padding: 8px 10px;
    border: 0;
    border-radius: var(--so-bw-r-8);
    background: transparent;
    color: var(--so-bw-ds-n900);
    font-family: inherit;
    font-size: 13.5px;
    font-weight: 600;
    text-align: left;
    cursor: pointer;
}
#so-booking-widget[data-template="chatbook"] .so-bw-chatmenu-item:hover {
    background: var(--so-bw-ds-n50);
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}
#so-booking-widget[data-template="chatbook"] .so-bw-chatmenu-item:focus-visible {
    outline: 2px solid var(--so-bw-primary);
    outline-offset: -2px;
}

/* ── 36.4 · The body is the thread canvas ────────────────────────────────
   Tinted, so the white bubbles read as objects ON something rather than as
   panels of the same sheet. This is the layout's one full-bleed surface. */
#so-booking-widget[data-template="chatbook"] .so-bw-body {
    display: flex;
    flex-direction: column;
    gap: 10px;
    background: var(--so-bw-ds-n25);
    padding: 14px var(--so-bw-gutter) 10px;
}

/* ── 36.5 · The turns ────────────────────────────────────────────────────
   Bot on the left behind an avatar gutter, customer on the right. The avatar
   is `align-items: flex-end` so it sits at the FOOT of a bubble that wraps to
   several lines, which is where a speaker's mark belongs. */
#so-booking-widget[data-template="chatbook"] .so-bw-chatlog {
    display: flex;
    flex-direction: column;
    gap: 8px;
}
#so-booking-widget[data-template="chatbook"] .so-bw-chatturn {
    display: flex;
    align-items: flex-end;
    gap: 8px;
}
#so-booking-widget[data-template="chatbook"] .so-bw-chatturn--said {
    justify-content: flex-end;
}
#so-booking-widget[data-template="chatbook"] .so-bw-chatavatar {
    flex: 0 0 auto;
    width: 28px;
    height: 28px;
    border-radius: 999px;
    background: var(--so-bw-primary);
    color: #fff;
    display: flex;
    align-items: center;
    justify-content: center;
}
#so-booking-widget[data-template="chatbook"] .so-bw-chatavatar svg { width: 17px; height: 17px; }
#so-booking-widget[data-template="chatbook"] .so-bw-chatbubble {
    display: block;
    max-width: 78%;
    padding: 9px 12px;
    font-family: inherit;
    font-size: 13.5px;
    line-height: 1.5;
    text-align: left;
}
#so-booking-widget[data-template="chatbook"] .so-bw-chatbubble--ask {
    background: var(--so-bw-ds-n0);
    border: 1px solid var(--so-bw-ds-n100);
    color: var(--so-bw-ds-n700);
    border-radius: var(--so-bw-r-16) var(--so-bw-r-16) var(--so-bw-r-16) var(--so-bw-r-4);
}
/* The customer's reply is also the way back to that answer, so it is a real
   button — hence the reset of what a UA button brings with it. The "Change"
   affordance is always visible rather than hover-only: this is the layout's
   primary Back, and a touch device has no hover to reveal it with. */
#so-booking-widget[data-template="chatbook"] .so-bw-chatbubble--said {
    display: flex;
    align-items: baseline;
    gap: 10px;
    border: 0;
    cursor: pointer;
    background: var(--so-bw-primary);
    color: #fff;
    font-weight: 600;
    border-radius: var(--so-bw-r-16) var(--so-bw-r-16) var(--so-bw-r-4) var(--so-bw-r-16);
}
#so-booking-widget[data-template="chatbook"] .so-bw-chatbubble-text {
    display: block;
    min-width: 0;
    overflow-wrap: anywhere;
}
#so-booking-widget[data-template="chatbook"] .so-bw-chatedit {
    flex: 0 0 auto;
    font-size: 11px;
    font-weight: 600;
    text-decoration: underline;
}
#so-booking-widget[data-template="chatbook"] .so-bw-chatbubble--said:focus-visible {
    outline: 2px solid var(--so-bw-primary);
    outline-offset: 2px;
}
/* n500 on white is 5.9:1 — a timestamp is quiet, not faint. */
#so-booking-widget[data-template="chatbook"] .so-bw-chattime {
    display: block;
    margin-top: 3px;
    font-size: 11px;
    line-height: 1.3;
    color: var(--so-bw-ds-n500);
}

/* ── 36.6 · The live step IS the newest bot bubble ───────────────────────
   The single rule this whole layout is built on. `.so-bw-step-host` holds
   every step and shows one; drawing that one as a bubble is what puts the REAL
   service cards, calendar, slot grid, custom-field pages, review summary and
   payment form inside the conversation. Nothing is re-implemented, so nothing
   can fall out of step with the rest of the widget.

   The 36px left margin is the avatar gutter (28px + the 8px turn gap), so the
   live bubble's left edge lines up with every bot bubble above it. */
#so-booking-widget[data-template="chatbook"] .so-bw-step {
    background: var(--so-bw-ds-n0);
    border: 1px solid var(--so-bw-ds-n100);
    border-radius: var(--so-bw-r-16) var(--so-bw-r-16) var(--so-bw-r-16) var(--so-bw-r-4);
    padding: 12px;
    margin-left: 36px;
    /* `flex: 0 0 auto`, against the shared `.so-bw-step { flex: 1 1 auto }`.
       That default is right for a bounded card: the step FILLS whatever height
       the card has left and its inner list scrolls. It is wrong for a bubble.
       A stretched step gives `.so-bw-datetime` a definite height, its single
       grid row resolves to that height, and a slot panel taller than it
       overflows the bubble in full view — measured at 440px of panel inside a
       232px row the moment the alternative-slot chips appeared.

       Sizing to content instead is both the fix and the metaphor: a message
       grows to fit what it says, and `.so-bw-body` — already the scroll region
       above 768px — takes up the scrolling, which is what puts the newest turn
       at the bottom for chatScrollToLatest() to reach. */
    flex: 0 0 auto;
}
/* `.so-bw-step-desc` is NOT hidden, and that is a decision rather than an
   omission. It looks redundant — the bot turn above already asked the
   question — but on four steps it is bound rather than literal and carries
   copy that appears nowhere else: the multi-location fallback explanation, the
   deposit and cancellation policy on the review step, and the waitlist note.
   Hiding it would silently drop policy text on this layout alone. It stays,
   set as the bot's second line. */
#so-booking-widget[data-template="chatbook"] .so-bw-step-desc {
    margin: 0 0 10px;
    font-size: 12.5px;
    line-height: 1.5;
    color: var(--so-bw-ds-n500);
}

/* ── 36.7 · Choices are quick replies ────────────────────────────────────
   One column of full-width rows, because a reply in a thread is a line you
   tap, not a tile you browse. 48px minimum on every one of them — the Material
   touch floor, and the reason the compact rows below still take a thumb. */
#so-booking-widget[data-template="chatbook"] .so-bw-card-grid {
    display: flex;
    flex-direction: column;
    gap: 8px;
}
#so-booking-widget[data-template="chatbook"] .so-bw-card:not(.so-bw-sp__panel) {
    min-height: 48px;
    border-radius: var(--so-bw-r-12);
    border: 1px solid var(--so-bw-ds-n200);
    background: var(--so-bw-ds-n0);
}
/* Chosen, said twice: a 2px brand border AND the check the markup already
   emits. Never hue alone — the same rule every selected state in this section
   follows. */
#so-booking-widget[data-template="chatbook"] .so-bw-card--selected:not(.so-bw-sp__panel) {
    border-color: var(--so-bw-primary);
    border-width: 2px;
    background: color-mix(in srgb, var(--so-bw-primary) 5%, #fff);
}
#so-booking-widget[data-template="chatbook"] .so-bw-card:not(.so-bw-sp__panel):hover {
    border-color: var(--so-bw-primary);
}
#so-booking-widget[data-template="chatbook"] .so-bw-card-title {
    font-size: 13.5px;
    font-weight: 600;
    color: var(--so-bw-ds-n900);
}
#so-booking-widget[data-template="chatbook"] .so-bw-card-meta {
    font-size: 11.5px;
    color: var(--so-bw-ds-n500);
}
#so-booking-widget[data-template="chatbook"] .so-bw-badge {
    font-size: 11px;
    font-weight: 600;
}
/* The staff step keeps the catalogue's own three-up grid — a person is chosen
   by face, and a column of rows hides the faces below the fold. Falls to two
   up in a narrow box rather than to one, so a photo never fills the bubble. */
#so-booking-widget[data-template="chatbook"] .so-bw-card-grid--staff {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 108px), 1fr));
    gap: 8px;
}
/* "Any Available" is not a person, so it does not sit in the face grid — it is
   the escape hatch under it, full width, the way the reference draws it. */
#so-booking-widget[data-template="chatbook"] .so-bw-card--any { grid-column: 1 / -1; }

/* ── 36.8 · Times ────────────────────────────────────────────────────────
   Three across at the widths this layout runs at — a wrapping row of pills
   makes the customer scan a ragged edge, and time is read in columns. */
#so-booking-widget[data-template="chatbook"] .so-bw-slot-list {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 92px), 1fr));
    gap: 8px;
}
#so-booking-widget[data-template="chatbook"] .so-bw-time-slot,
#so-booking-widget[data-template="chatbook"] .so-bw-slot-row {
    min-height: 44px;
    border-radius: var(--so-bw-r-10);
    border: 1px solid var(--so-bw-ds-n200);
    font-size: 12.5px;
    font-weight: 600;
}
#so-booking-widget[data-template="chatbook"] .so-bw-time-slot--selected,
#so-booking-widget[data-template="chatbook"] .so-bw-slot-row--selected {
    border-color: var(--so-bw-primary);
    border-width: 2px;
    background: color-mix(in srgb, var(--so-bw-primary) 6%, #fff);
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}
#so-booking-widget[data-template="chatbook"] .so-bw-slot-group-title {
    font-size: 11px;
    font-weight: 700;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--so-bw-ds-n500);
}

/* ── 36.9 · The month ────────────────────────────────────────────────────
   The chosen day is a filled brand disc — the one place in the layout where a
   selected thing is a solid rather than a tint, because a calendar cell is too
   small to carry a border weight AND a number legibly. */
#so-booking-widget[data-template="chatbook"] .so-bw-calendar-day {
    min-height: 36px;
    border-radius: 999px;
    font-size: 12.5px;
}
#so-booking-widget[data-template="chatbook"] .so-bw-calendar-day--selected {
    background: var(--so-bw-primary);
    border-color: var(--so-bw-primary);
    color: #fff;
    font-weight: 700;
}
#so-booking-widget[data-template="chatbook"] .so-bw-calendar-month {
    font-size: 13.5px;
    font-weight: 700;
    color: var(--so-bw-ds-n900);
}
#so-booking-widget[data-template="chatbook"] .so-bw-calendar-weekdays {
    font-size: 10.5px;
    font-weight: 700;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--so-bw-ds-n500);
}

/* ── 36.10 · Fields ──────────────────────────────────────────────────────
   The label rides INSIDE the field's box, above the value, so a filled form
   reads as a stack of answered lines rather than a stack of prompts. The group
   is what draws the border; the input inside it is stripped, so focus has to
   be re-expressed on the group — see the `:focus-within` ring below, without
   which this trade would cost the form its keyboard affordance. */
#so-booking-widget[data-template="chatbook"] .so-bw-form { gap: 8px; }
#so-booking-widget[data-template="chatbook"] .so-bw-form-group {
    padding: 7px 12px 8px;
    border: 1px solid var(--so-bw-ds-n200);
    border-radius: var(--so-bw-r-12);
    background: var(--so-bw-ds-n0);
}
#so-booking-widget[data-template="chatbook"] .so-bw-form-group:focus-within {
    border-color: var(--so-bw-primary);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--so-bw-primary) 18%, transparent);
}
#so-booking-widget[data-template="chatbook"] .so-bw-label {
    display: block;
    margin: 0 0 1px;
    font-size: 11px;
    font-weight: 500;
    color: var(--so-bw-ds-n500);
}
#so-booking-widget[data-template="chatbook"] .so-bw-input,
#so-booking-widget[data-template="chatbook"] .so-bw-textarea,
#so-booking-widget[data-template="chatbook"] .so-bw-select {
    min-height: 0;
    padding: 0;
    border: 0;
    border-radius: 0;
    background: transparent;
    font-size: 13.5px;
    font-weight: 600;
    color: var(--so-bw-ds-n900);
}
#so-booking-widget[data-template="chatbook"] .so-bw-input:focus,
#so-booking-widget[data-template="chatbook"] .so-bw-textarea:focus,
#so-booking-widget[data-template="chatbook"] .so-bw-select:focus {
    outline: none;
    box-shadow: none;
}
/* An invalid field must not be signalled by the ring alone — the group's own
   border carries it too, so the error survives a red/grey confusion. */
#so-booking-widget[data-template="chatbook"] .so-bw-form-group:has(.so-bw-input--invalid) {
    border-color: var(--so-bw-danger, #dc2626);
}

/* ── 36.11 · The summary before the commit ───────────────────────────────
   Label left, value right, hairline between — a receipt, which is what the
   customer is being asked to check. The total is the only line that changes
   weight and colour, because it is the only line that changes the decision. */
#so-booking-widget[data-template="chatbook"] .so-bw-summary {
    border: 1px solid var(--so-bw-ds-n100);
    border-radius: var(--so-bw-r-12);
    padding: 4px 12px;
    background: var(--so-bw-ds-n0);
}
#so-booking-widget[data-template="chatbook"] .so-bw-summary-row {
    padding: 9px 0;
    border-bottom: 1px solid var(--so-bw-ds-n50);
}
#so-booking-widget[data-template="chatbook"] .so-bw-summary-row:last-child { border-bottom: 0; }
#so-booking-widget[data-template="chatbook"] .so-bw-summary-label {
    font-size: 12.5px;
    color: var(--so-bw-ds-n500);
}
#so-booking-widget[data-template="chatbook"] .so-bw-summary-value {
    font-size: 12.5px;
    font-weight: 600;
    color: var(--so-bw-ds-n900);
}
#so-booking-widget[data-template="chatbook"] .so-bw-summary-row--total .so-bw-summary-label,
#so-booking-widget[data-template="chatbook"] .so-bw-summary-row--total .so-bw-summary-value {
    font-size: 14px;
    font-weight: 700;
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}

/* ── 36.12 · Paying ──────────────────────────────────────────────────────
   Same row language as the service list, so "choose a method" is visibly the
   same kind of act as "choose a service" — one tap on a full-width line. */
#so-booking-widget[data-template="chatbook"] .so-bw-pay-methods {
    display: flex;
    flex-direction: column;
    gap: 8px;
}
#so-booking-widget[data-template="chatbook"] .so-bw-pay-method {
    min-height: 48px;
    padding: 10px 12px;
    border: 1px solid var(--so-bw-ds-n200);
    border-radius: var(--so-bw-r-12);
    background: var(--so-bw-ds-n0);
    font-size: 13px;
    font-weight: 600;
}
#so-booking-widget[data-template="chatbook"] .so-bw-pay-method--on {
    border-color: var(--so-bw-primary);
    border-width: 2px;
    background: color-mix(in srgb, var(--so-bw-primary) 5%, #fff);
}

/* ── 36.13 · The reply bar ───────────────────────────────────────────────
   Kept, not hidden. Auto-advance carries the one-tap steps, but the steps that
   CANNOT auto-advance — a multi-select service step, the details form, the
   review — still need a commit, and a thread with no send control leaves those
   dead. Two pills: a tinted secondary that reads as "change something" and a
   solid primary that sends. */
#so-booking-widget[data-template="chatbook"] .so-bw-nav {
    gap: 8px;
    padding: 10px var(--so-bw-gutter);
    background: var(--so-bw-ds-n0);
    border-top: 1px solid var(--so-bw-ds-n100);
}
#so-booking-widget[data-template="chatbook"] .so-bw-btn {
    min-height: 44px;
    border-radius: 999px;
    font-size: 13.5px;
    font-weight: 600;
}
#so-booking-widget[data-template="chatbook"] .so-bw-btn--secondary {
    border-color: color-mix(in srgb, var(--so-bw-primary) 28%, #fff);
    background: color-mix(in srgb, var(--so-bw-primary) 6%, #fff);
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}

/* ── 36.14 · A full day answered with the nearest openings ───────────────
   Reuses `.so-bw-suggest-opt`, the chip Conversation already ships, inside a
   bot-toned panel — so the alternatives are styled and committed by code that
   already existed. See `chatAlternativeSlots`. */
#so-booking-widget[data-template="chatbook"] .so-bw-chatalt {
    display: block;
    background: var(--so-bw-ds-n50);
    border-radius: var(--so-bw-r-12);
    padding: 12px;
    margin-top: 4px;
}
#so-booking-widget[data-template="chatbook"] .so-bw-chatalt-lead {
    margin: 0 0 10px;
    font-size: 12.5px;
    line-height: 1.5;
    color: var(--so-bw-ds-n700);
}
#so-booking-widget[data-template="chatbook"] .so-bw-chatalt .so-bw-suggest {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
}
/* The shared chip is `width: 100%` with `justify-content: space-between`,
   which is right where it ships — Conversation stacks three full-width rows in
   a column. In a bubble they are quick replies, so they shrink to their
   content and wrap. Without this the `flex-wrap` above is inert and the three
   alternatives render as three stacked bars: the shape of a list, not of an
   answer. Scoped to `.so-bw-chatalt`, so Conversation's own suggested times
   are untouched. */
#so-booking-widget[data-template="chatbook"] .so-bw-chatalt .so-bw-suggest-opt {
    width: auto;
    flex: 0 1 auto;
    justify-content: flex-start;
    gap: 8px;
    min-height: 44px;
    padding: 8px 14px;
    border-radius: 999px;
}

/* ── 36.15 · The end of the thread ───────────────────────────────────────
   The log STAYS on the confirmation screen: what the customer answered is the
   receipt's own context, and clearing it at the moment of success would be the
   one place the thread forgot the conversation. The replies lose their Change
   affordance — nothing is editable once it is booked — which mirrors what
   section 17 does to the Accordion recap off the same `--done` modifier. */
#so-booking-widget[data-template="chatbook"] .so-bw-container--done .so-bw-chatedit { display: none; }
#so-booking-widget[data-template="chatbook"] .so-bw-container--done .so-bw-chatbubble--said {
    cursor: default;
    pointer-events: none;
}
#so-booking-widget[data-template="chatbook"] .so-bw-confirmation { text-align: left; }
#so-booking-widget[data-template="chatbook"] .so-bw-confirmation-icon {
    width: 40px;
    height: 40px;
    margin: 0 0 10px;
}
#so-booking-widget[data-template="chatbook"] .so-bw-confirmation-title {
    font-size: 17px;
    font-weight: 700;
    text-align: left;
}
#so-booking-widget[data-template="chatbook"] .so-bw-confirmation-text {
    font-size: 12.5px;
    line-height: 1.55;
    color: var(--so-bw-ds-n500);
    text-align: left;
}
#so-booking-widget[data-template="chatbook"] .so-bw-confirmation-details {
    border: 1px solid var(--so-bw-ds-n100);
    border-radius: var(--so-bw-r-12);
    padding: 4px 12px;
    background: var(--so-bw-ds-n0);
}
#so-booking-widget[data-template="chatbook"] .so-bw-confirmation-detail {
    padding: 9px 0;
    border-bottom: 1px solid var(--so-bw-ds-n50);
}
#so-booking-widget[data-template="chatbook"] .so-bw-confirmation-detail:last-child { border-bottom: 0; }
#so-booking-widget[data-template="chatbook"] .so-bw-confirmation-detail-label {
    font-size: 12.5px;
    color: var(--so-bw-ds-n500);
}
#so-booking-widget[data-template="chatbook"] .so-bw-confirmation-detail-value {
    font-size: 12.5px;
    font-weight: 600;
    color: var(--so-bw-ds-n900);
}
/* The closing actions are the thread's last quick replies, so they take the
   same full-width row language as every other choice in it. */
#so-booking-widget[data-template="chatbook"] .so-bw-confirmation-actions { align-items: stretch !important; }
#so-booking-widget[data-template="chatbook"] .so-bw-confirmation .so-bw-btn { width: 100%; }

/* ── 36.16 · Motion ──────────────────────────────────────────────────────
   A new turn slides up 4px as it lands. That is the ONE piece of motion the
   layout adds, and it is doing a job: in a log that grows downward, a bubble
   that simply appears is hard to tell from one that was always there.

   Reduced motion removes the travel and keeps the fade, so the arrival is
   still marked for someone who has asked not to be moved. */
@media (prefers-reduced-motion: no-preference) {
    #so-booking-widget[data-template="chatbook"] .so-bw-chatturn {
        animation: so-bw-chat-in 180ms ease-out both;
    }
}
@keyframes so-bw-chat-in {
    from { opacity: 0; transform: translateY(4px); }
    to   { opacity: 1; transform: none; }
}

/* ── 36.17 · The phone, which is what this layout is for ─────────────────
   Below 480px of CONTAINER the avatar gutter is a sixth of the line length, so
   it goes and the bubbles take the width back. `data-cw` is the container
   width stamped by _wireContainerWidth(), not the viewport — a 420px widget in
   a desktop sidebar gets this treatment too, which is correct. */
#so-booking-widget:where([data-cw~="480"])[data-template="chatbook"] .so-bw-chatavatar { display: none; }
#so-booking-widget:where([data-cw~="480"])[data-template="chatbook"] .so-bw-step { margin-left: 0; }
#so-booking-widget:where([data-cw~="480"])[data-template="chatbook"] .so-bw-chatbubble { max-width: 86%; }
#so-booking-widget:where([data-cw~="480"])[data-template="chatbook"] .so-bw-body {
    padding-left: 10px;
    padding-right: 10px;
}
#so-booking-widget:where([data-cw~="480"])[data-template="chatbook"] .so-bw-nav {
    padding-left: 10px;
    padding-right: 10px;
}

/* ── 36.18 · The reply carries its own receipt ───────────────────────────
   Timestamp and delivered tick sit on the same baseline as the Change link, so
   the bubble has one meta row rather than three floating fragments.

   The reply is a TINT with dark text, not a solid brand fill. Two reasons, and
   the second is the load-bearing one: a solid fill puts 11px meta text on the
   brand colour, where a pale brand drops it under 4.5:1 and the site owner has
   no way to know — the widget lets an admin pick any primary. On the tint the
   meta is n500-on-near-white at 5.4:1 whatever hue is chosen. The bubble still
   reads unmistakably as "yours": it is the only tinted, right-aligned,
   tick-bearing thing on the canvas. */
#so-booking-widget[data-template="chatbook"] .so-bw-chatbubble--said {
    display: block;
    background: color-mix(in srgb, var(--so-bw-primary) 10%, #fff);
    border: 1px solid color-mix(in srgb, var(--so-bw-primary) 22%, #fff);
    color: var(--so-bw-ds-n900);
}
#so-booking-widget[data-template="chatbook"] .so-bw-chatmeta {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 6px;
    margin-top: 3px;
}
#so-booking-widget[data-template="chatbook"] .so-bw-chatmeta .so-bw-chattime { margin-top: 0; }
#so-booking-widget[data-template="chatbook"] .so-bw-chattick {
    flex: 0 0 auto;
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}
#so-booking-widget[data-template="chatbook"] .so-bw-chatedit {
    font-size: 11px;
    font-weight: 600;
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
    text-decoration: underline;
}

/* ── 36.19 · Restart, in words ───────────────────────────────────────────
   The wide-screen affordance. Hidden below the three-pane breakpoint, where
   the ⋮ menu carries the same action — 36.22 does the swap, and exactly one of
   the two is visible at any width. */
#so-booking-widget[data-template="chatbook"] .so-bw-chatrestart {
    display: none;
    align-items: center;
    gap: 7px;
    flex: 0 0 auto;
    min-height: 36px;
    padding: 7px 14px;
    border: 1px solid rgba(255, 255, 255, .34);
    border-radius: 999px;
    background: transparent;
    color: #fff;
    font-family: inherit;
    font-size: 12.5px;
    font-weight: 600;
    white-space: nowrap;
    cursor: pointer;
}
#so-booking-widget[data-template="chatbook"] .so-bw-chatrestart:hover {
    background: rgba(255, 255, 255, .16);
    border-color: rgba(255, 255, 255, .6);
}
#so-booking-widget[data-template="chatbook"] .so-bw-chatrestart:focus-visible {
    outline: 2px solid #fff;
    outline-offset: 2px;
}

/* ── 36.20 · The progress rail (desktop only) ────────────────────────────
   chatbook-only markup, so it is off everywhere by default and 36.22 is the
   only thing that turns it on. Below the three-pane breakpoint it stays off:
   it restates what the thread already shows, which earns a spare column on a
   wide screen and costs a scroll on a phone. */
#so-booking-widget .so-bw-chatside { display: none; }

#so-booking-widget[data-template="chatbook"] .so-bw-chatside-head {
    padding: 0 0 14px;
    border-bottom: 1px solid var(--so-bw-ds-n100);
}
#so-booking-widget[data-template="chatbook"] .so-bw-chatside-title {
    font-size: 14px;
    font-weight: 700;
    color: var(--so-bw-ds-n900);
}
#so-booking-widget[data-template="chatbook"] .so-bw-chatside-count {
    margin-top: 2px;
    font-size: 11.5px;
    color: var(--so-bw-ds-n500);
}
#so-booking-widget[data-template="chatbook"] .so-bw-chatside-bar {
    margin-top: 10px;
    height: 4px;
    border-radius: 999px;
    background: var(--so-bw-ds-n100);
    overflow: hidden;
}
#so-booking-widget[data-template="chatbook"] .so-bw-chatside-bar-fill {
    height: 100%;
    border-radius: 999px;
    background: var(--so-bw-primary);
}
#so-booking-widget[data-template="chatbook"] .so-bw-chatside-list {
    display: flex;
    flex-direction: column;
    padding: 14px 0 0;
}
/* The connector between the numerals is drawn on the row, not between rows, so
   it cannot desynchronise from a list whose length changes with the flow. The
   last row suppresses it. 16px is the numeral's centre. */
#so-booking-widget[data-template="chatbook"] .so-bw-chatside-row {
    position: relative;
    display: flex;
    align-items: flex-start;
    gap: 12px;
    padding: 8px 6px 16px;
    border-radius: var(--so-bw-r-8);
}
#so-booking-widget[data-template="chatbook"] .so-bw-chatside-row::before {
    content: "";
    position: absolute;
    left: 21px;
    top: 32px;
    bottom: 4px;
    width: 2px;
    border-radius: 999px;
    background: var(--so-bw-ds-n100);
}
#so-booking-widget[data-template="chatbook"] .so-bw-chatside-row:last-child::before { display: none; }
#so-booking-widget[data-template="chatbook"] .so-bw-chatside-row--done::before {
    background: color-mix(in srgb, var(--so-bw-primary) 32%, #fff);
}
#so-booking-widget[data-template="chatbook"] .so-bw-chatside-row[role="button"] { cursor: pointer; }
#so-booking-widget[data-template="chatbook"] .so-bw-chatside-row[role="button"]:hover {
    background: var(--so-bw-ds-n25);
}
#so-booking-widget[data-template="chatbook"] .so-bw-chatside-row:focus-visible {
    outline: 2px solid var(--so-bw-primary);
    outline-offset: 1px;
}
#so-booking-widget[data-template="chatbook"] .so-bw-chatside-num {
    position: relative;
    z-index: 1;
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 24px;
    height: 24px;
    border-radius: 999px;
    background: var(--so-bw-ds-n100);
    color: var(--so-bw-ds-n500);
    font-size: 11.5px;
    font-weight: 700;
    line-height: 1;
}
/* Done is a filled disc with a tick; active is a filled disc with its number
   and a halo. Two states, distinguished by the GLYPH as well as the colour, so
   the rail still reads without hue. */
#so-booking-widget[data-template="chatbook"] .so-bw-chatside-row--done .so-bw-chatside-num {
    background: var(--so-bw-primary);
    color: #fff;
}
#so-booking-widget[data-template="chatbook"] .so-bw-chatside-row--active .so-bw-chatside-num {
    background: var(--so-bw-primary);
    color: #fff;
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--so-bw-primary) 18%, transparent);
}
#so-booking-widget[data-template="chatbook"] .so-bw-chatside-text {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
}
#so-booking-widget[data-template="chatbook"] .so-bw-chatside-label {
    font-size: 12.5px;
    font-weight: 600;
    color: var(--so-bw-ds-n900);
}
#so-booking-widget[data-template="chatbook"] .so-bw-chatside-value {
    font-size: 11.5px;
    line-height: 1.4;
    color: var(--so-bw-ds-n500);
    overflow-wrap: anywhere;
}
#so-booking-widget[data-template="chatbook"] .so-bw-chatside-row--active .so-bw-chatside-value {
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
    font-weight: 600;
}
#so-booking-widget[data-template="chatbook"] .so-bw-chatside-foot {
    display: flex;
    align-items: flex-start;
    gap: 10px;
    margin-top: auto;
    padding-top: 16px;
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}
#so-booking-widget[data-template="chatbook"] .so-bw-chatside-foot svg { flex: 0 0 auto; margin-top: 1px; }
#so-booking-widget[data-template="chatbook"] .so-bw-chatside-foot-text {
    display: flex;
    flex-direction: column;
    gap: 2px;
    font-size: 11.5px;
    line-height: 1.4;
    color: var(--so-bw-ds-n500);
}
#so-booking-widget[data-template="chatbook"] .so-bw-chatside-foot-text strong {
    font-size: 12px;
    font-weight: 700;
    color: var(--so-bw-ds-n900);
}

/* ── 36.21 · The summary rail (desktop only) ─────────────────────────────
   `.so-bw-rail` already existed, already carried Service / Specialist / Date /
   Time / Total, and was already wired to the live selection — it was simply
   only ever revealed for three retired two-pane layouts. Reusing it is the
   reason the right-hand column needed no new markup at all.

   The reveal is (1,2,0), the same as the retired layouts' own reveal, and it
   comes later in the file — so it answers the (1,1,0) `display: none` default
   without touching what those layouts get. */
#so-booking-widget[data-template="chatbook"] .so-bw-rail-head {
    padding: 0 0 12px;
    border-bottom: 1px solid var(--so-bw-ds-n100);
}
#so-booking-widget[data-template="chatbook"] .so-bw-rail-title {
    font-size: 14px;
    font-weight: 700;
    color: var(--so-bw-ds-n900);
}
#so-booking-widget[data-template="chatbook"] .so-bw-rail-sub {
    margin-top: 2px;
    font-size: 11.5px;
    color: var(--so-bw-ds-n500);
}
#so-booking-widget[data-template="chatbook"] .so-bw-rail-list { padding: 4px 0 0; }
#so-booking-widget[data-template="chatbook"] .so-bw-rail-row {
    display: flex;
    flex-direction: column;
    gap: 3px;
    padding: 11px 0;
    border-bottom: 1px solid var(--so-bw-ds-n50);
}
/* `font-family: inherit` and `text-align: left` are both undoing the retired
   two-pane rail's own styling, which sets IBM Plex Mono and right-aligns the
   values — a ticket-stub register that suits those layouts and reads as a
   terminal readout next to a conversation. The rail is shared markup, so
   restyling it for this layout means answering what it already carries. */
#so-booking-widget[data-template="chatbook"] .so-bw-rail-k {
    font-family: inherit;
    font-size: 11px;
    font-weight: 600;
    letter-spacing: .04em;
    text-transform: uppercase;
    text-align: left;
    color: var(--so-bw-ds-n500);
}
#so-booking-widget[data-template="chatbook"] .so-bw-rail-v {
    font-family: inherit;
    font-size: 13px;
    font-weight: 600;
    line-height: 1.4;
    text-align: left;
    color: var(--so-bw-ds-n900);
    overflow-wrap: anywhere;
}
#so-booking-widget[data-template="chatbook"] .so-bw-rail-total { font-family: inherit; }
/* An unanswered line is quiet rather than absent — the rail is a plan as much
   as a receipt, so the customer can see what is still to come. */
#so-booking-widget[data-template="chatbook"] .so-bw-rail-row--unset .so-bw-rail-v {
    font-weight: 500;
    color: var(--so-bw-ds-n300);
}
#so-booking-widget[data-template="chatbook"] .so-bw-rail-total {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 12px;
    margin-top: 12px;
    padding-top: 14px;
    border-top: 1px solid var(--so-bw-ds-n100);
    font-size: 15px;
    font-weight: 700;
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}

/* ── 36.22 · Three panes, above 960px of container ───────────────────────
   Progress on the left, the conversation in the middle, the running order on
   the right — the arrangement the desktop study asks for.

   Built with an ABSOLUTELY POSITIONED pair of rails over a padded container,
   not with a grid. That is the pattern the two-pane layouts in section 7
   already use, and the reason to follow it here is robustness rather than
   consistency: `.so-bw-container` has ten direct children (the live region,
   both rails, the header, the membership and hold banners, the body, the nav,
   the payment-retry panel and the powered-by badge) and gains more over time.
   A grid has to place every one of them or auto-flow will invent rows for the
   ones it was not told about — so a banner added in two years' time would land
   in a column nobody chose. Taking the two rails out of flow means the centre
   column is just the normal document, and anything added to it lands where it
   would have anyway.

   `data-cwmin` is the CONTAINER width stamped by _wireContainerWidth(), not
   the viewport, so a widget in a 900px page column correctly stays single —
   which a media query could not tell. */
#so-booking-widget:where([data-cwmin~="960"])[data-template="chatbook"] .so-bw-container {
    /* One place to change the geometry. The bar height is DECLARED rather than
       measured because both rails hang off it — see the fixed height on the
       chat bar below, which is what makes the declaration true. */
    --so-bw-chat-side: 260px;
    --so-bw-chat-sum: 320px;
    --so-bw-chat-bar: 64px;
    /* The thread can afford the catalogue inset once it has a column of its
       own. Still the token, for the reason 36.1 gives. */
    --so-bw-gutter: 24px;
    /* 560px is the THREAD's width, not the card's, and section 23.1 has been
       reading it as the card's — which is correct while the thread IS the
       card. With a rail on each side the same 560 leaves 4px of conversation
       between them, so the token is restated for the three-pane case:
       260 + 660 + 320. The centre lands at 660, which is the thread at a
       comfortable desktop measure rather than the 560 phone one.

       Restated as the TOKEN, not as `max-width`, for the same reason 23.1
       exists — the Studio's size radio multiplies this, so Small gives a
       992px card that still holds three columns and Large gives 1550. A flat
       max-width here would have been replaced by that radio, not scaled. */
    --so-bw-tpl-maxw: 1240px;
    position: relative;
    padding-left: var(--so-bw-chat-side);
    padding-right: var(--so-bw-chat-sum);
}
/* The header is the one full-bleed band, so it reaches back out over both
   rails — the blue runs edge to edge above all three columns. */
#so-booking-widget:where([data-cwmin~="960"])[data-template="chatbook"] .so-bw-header {
    margin-left: calc(var(--so-bw-chat-side) * -1);
    margin-right: calc(var(--so-bw-chat-sum) * -1);
}
#so-booking-widget:where([data-cwmin~="960"])[data-template="chatbook"] .so-bw-chatbar {
    height: var(--so-bw-chat-bar);
    padding: 0 var(--so-bw-gutter);
}
#so-booking-widget:where([data-cwmin~="960"])[data-template="chatbook"] .so-bw-chatrestart { display: inline-flex; }
#so-booking-widget:where([data-cwmin~="960"])[data-template="chatbook"] .so-bw-chatmenu { display: none; }

#so-booking-widget:where([data-cwmin~="960"])[data-template="chatbook"] .so-bw-chatside,
#so-booking-widget:where([data-cwmin~="960"])[data-template="chatbook"] .so-bw-rail {
    display: flex;
    flex-direction: column;
    position: absolute;
    top: var(--so-bw-chat-bar);
    bottom: 0;
    /* Each rail scrolls on its own, so a long service name or a fifteen-step
       flow can never push the conversation's height around. */
    overflow-y: auto;
    padding: 18px 16px;
    background: var(--so-bw-ds-n0);
}
#so-booking-widget:where([data-cwmin~="960"])[data-template="chatbook"] .so-bw-chatside {
    left: 0;
    width: var(--so-bw-chat-side);
    border-right: 1px solid var(--so-bw-ds-n100);
}
#so-booking-widget:where([data-cwmin~="960"])[data-template="chatbook"] .so-bw-rail {
    right: 0;
    width: var(--so-bw-chat-sum);
    border-left: 1px solid var(--so-bw-ds-n100);
    /* Beats the (1,2,0) `position: absolute; left: 0` the retired two-pane
       layouts set on this same element — those rules and these are the same
       specificity, and this block comes later, so `left` has to be released
       explicitly or the rail would sit on top of the progress column. */
    left: auto;
}
/* The thread gets room to breathe once it is only a third of the card, and a
   ceiling so a long transcript scrolls inside the pane instead of growing the
   page under the two rails — which are absolutely positioned to `bottom: 0`
   and so would stretch with it. */
#so-booking-widget:where([data-cwmin~="960"])[data-template="chatbook"] .so-bw-body {
    padding: 20px var(--so-bw-gutter) 12px;
    max-height: min(560px, 68vh);
}
#so-booking-widget:where([data-cwmin~="960"])[data-template="chatbook"] .so-bw-nav {
    padding: 12px var(--so-bw-gutter);
}
/* The canvas tint is the CENTRE column's, not the card's — the rails are white
   panels beside it, which is what separates the conversation from the chrome
   that describes it. */
#so-booking-widget:where([data-cwmin~="960"])[data-template="chatbook"] .so-bw-container {
    background: var(--so-bw-ds-n25);
}

/* ── 36.23 · Between the two ─────────────────────────────────────────────
   Under 960px the rails are gone and the thread is the whole card, so the
   bubbles may use the width the avatar gutter no longer has to share. */
#so-booking-widget:where([data-cw~="900"])[data-template="chatbook"] .so-bw-slot-list {
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 104px), 1fr));
}
/* A start–end range needs a line of its own; the shared row prints them side
   by side, which at three columns wraps mid-time ("2:30 PM to 3:30 / PM"). */
#so-booking-widget[data-template="chatbook"] .so-bw-slot-range {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 1px;
    line-height: 1.25;
}
#so-booking-widget[data-template="chatbook"] .so-bw-slot-till {
    font-size: 10.5px;
    font-weight: 500;
    color: var(--so-bw-ds-n500);
}

/* ── 36.24 · The service grid — three up, two up, one up ─────────────────
   36.7 makes every choice list a single column of full-width rows, which is
   right for staff, location and add-ons: those are short labels where a row is
   the honest shape. A SERVICE is not — it has a photograph, a length, a price
   and a sentence of description, and stacking those as full-width rows pushes
   the third service below the fold of the bubble and turns choosing into
   scrolling.

   So services alone get a grid, and the count steps with the CONTAINER width
   stamped on the root by _wireContainerWidth(): one up on a phone, two from
   600px, three from 960px where the centre column is ~612px of usable width
   and three 190px tiles fit without crowding.

   `grid-auto-rows: 1fr` plus `height: 100%` on the card is what makes the row
   uniform: every tile in a row takes the height of the tallest, so a service
   with a long name and one with none still line their prices up. Without it
   the grid is a ragged edge and the eye has nothing to compare along. */
#so-booking-widget[data-template="chatbook"] .so-bw-service-group .so-bw-card-grid {
    display: grid;
    grid-template-columns: 1fr;
    grid-auto-rows: 1fr;
    gap: 8px;
}
#so-booking-widget:where([data-cwmin~="600"])[data-template="chatbook"] .so-bw-service-group .so-bw-card-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
}
#so-booking-widget:where([data-cwmin~="960"])[data-template="chatbook"] .so-bw-service-group .so-bw-card-grid {
    grid-template-columns: repeat(3, minmax(0, 1fr));
}
#so-booking-widget[data-template="chatbook"] .so-bw-card--service {
    height: 100%;
    display: flex;
    flex-direction: column;
    padding: 0;
    overflow: hidden;
}

/* ── 36.25 · Compact card anatomy ────────────────────────────────────────
   The card the catalogue ships is built for a full-width step: a tall header
   band, generous padding, a description that runs as long as it likes. Inside
   a chat bubble that is a page, not a reply. Every number below is the same
   card at conversational scale — the band halves, the padding drops to the
   8/10 rung, and the two text blocks are clamped so the tile's height is a
   function of the grid rather than of how much the admin typed.

   Nothing is REMOVED. The photo, the name, the description, the duration and
   the price are all still there, which is the point: the customer is choosing
   a service, and a chat is not a reason to tell them less about it. */
#so-booking-widget[data-template="chatbook"] .so-bw-card--service .so-bw-card-media {
    height: 84px;
    flex: 0 0 auto;
    border-radius: 0;
}
#so-booking-widget[data-template="chatbook"] .so-bw-card--service .so-bw-card-media-img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}
#so-booking-widget[data-template="chatbook"] .so-bw-card--service .so-bw-card-top {
    flex: 1 1 auto;
    min-height: 0;
    padding: 8px 10px 10px;
}
#so-booking-widget[data-template="chatbook"] .so-bw-card--service .so-bw-card-body {
    display: flex;
    flex-direction: column;
    gap: 3px;
    height: 100%;
}
/* One line for the name, and the RESERVED second line released.
   `#so-booking-widget[data-template] .so-bw-card--service .so-bw-card-title`
   clamps to two lines with `!important` and holds the space open with
   `min-height: calc(2 * 1.3em)` — deliberately, so a two-line name on one card
   cannot push its neighbour's badges out of line. In a 3-up grid of 169px
   tiles that reservation is 17px of empty space on every single-line card, and
   `grid-auto-rows: 1fr` already does the alignment job it was protecting. So
   both are answered here, exactly the way `wide`, `dusk` and `editorial`
   answer them. `!important` is required to meet `!important`; `min-height: 0`
   is the release. The full name stays in the title attribute and on Review. */
#so-booking-widget[data-template="chatbook"] .so-bw-card--service .so-bw-card-title {
    font-size: 12.5px;
    font-weight: 600;
    line-height: 1.35;
    -webkit-line-clamp: 1 !important;
    line-clamp: 1;
    min-height: 0;
}
/* Two lines of description — enough for a real sentence, capped so it cannot
   set the height of the row. Same reservation released for the same reason. */
#so-booking-widget[data-template="chatbook"] .so-bw-card--service .so-bw-card-meta--clamp {
    font-size: 11px;
    line-height: 1.4;
    -webkit-line-clamp: 2 !important;
    line-clamp: 2;
    min-height: 0;
    margin-bottom: 0;
}
/* The badges sit on the bottom edge whatever the description did above them,
   so duration and price read across the row as a row. `min-height: 0` and
   `align-items: center` drop a further 22px the shared rule was holding for a
   taller badge than this template draws. */
#so-booking-widget[data-template="chatbook"] .so-bw-card--service .so-bw-card-badges {
    margin-top: auto;
    padding-top: 6px;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 4px;
    min-height: 0;
}
/* Sized so duration AND price fit one line inside a 169px tile — the 3-up
   width, which is the tightest this template ever runs at. At the shipped
   10.5px/7px they measured ~80px each against 149px of usable width and
   wrapped, putting the price on its own row and making every selected card a
   row taller than its neighbours. */
#so-booking-widget[data-template="chatbook"] .so-bw-card--service .so-bw-badge {
    padding: 2px 6px;
    font-size: 10px;
    line-height: 1.5;
    border-radius: 999px;
    white-space: nowrap;
}
#so-booking-widget[data-template="chatbook"] .so-bw-card--service .so-bw-badge--price {
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
    background: color-mix(in srgb, var(--so-bw-primary) 9%, #fff);
}
/* "Read more" opens the description modal. Kept, shrunk — and the outline is
   left to the theme, which is where the stray focus box on this control comes
   from in the first place. */
#so-booking-widget[data-template="chatbook"] .so-bw-card--service .so-bw-card-more {
    font-size: 10.5px;
    padding: 0;
}

/* ── 36.26 · Chosen, and drawn rather than just coloured ─────────────────
   Three simultaneous signals, because one is never enough: a 2px brand border
   (up from 1px, so the weight changes and not only the hue), a wash of the
   brand at 6%, and the tick — which DRAWS itself rather than appearing.

   The draw is the confirmation. A checkmark that pops into existence is a
   state; one that is struck in 260ms is an acknowledgement, and that is the
   difference between a form registering input and a concierge saying "got
   it". `stroke-dashoffset` on the polyline the card already renders — no new
   element, and it degrades to a plain visible tick wherever the animation
   does not run. */
#so-booking-widget[data-template="chatbook"] .so-bw-card--service.so-bw-card--selected {
    border-color: var(--so-bw-primary);
    border-width: 2px;
    background: color-mix(in srgb, var(--so-bw-primary) 6%, #fff);
    box-shadow: 0 6px 18px -10px color-mix(in srgb, var(--so-bw-primary) 55%, transparent);
}
/* Lifted out of the text row and pinned to the corner of the media band.
   In flow it is a 22px sibling of `.so-bw-card-body` behind a 16px gap, so it
   takes 38px off the body's width the moment a card is selected — enough, in a
   169px tile, to wrap the price onto a second line and make the chosen card
   taller than the two beside it. The corner is also where the reference puts
   it, and where a selection mark reads without competing with the text. */
#so-booking-widget[data-template="chatbook"] .so-bw-card--service { position: relative; }
#so-booking-widget[data-template="chatbook"] .so-bw-card--service .so-bw-card-check {
    position: absolute;
    top: 6px;
    right: 6px;
    z-index: 2;
    width: 22px;
    height: 22px;
    border-radius: 999px;
    background: var(--so-bw-primary);
    color: #fff;
    box-shadow: 0 0 0 2px #fff;
}
#so-booking-widget[data-template="chatbook"] .so-bw-card-check {
    width: 22px;
    height: 22px;
    border-radius: 999px;
    background: var(--so-bw-primary);
    color: #fff;
}
/* With the check gone from the row, the 16px gap it needed goes too. */
#so-booking-widget[data-template="chatbook"] .so-bw-card--service .so-bw-card-top { gap: 0; }
#so-booking-widget[data-template="chatbook"] .so-bw-card-check svg { width: 13px; height: 13px; }
@media (prefers-reduced-motion: no-preference) {
    #so-booking-widget[data-template="chatbook"] .so-bw-card-check--visible {
        animation: so-bw-chat-pop 260ms cubic-bezier(.34, 1.56, .64, 1) both;
    }
    /* 24 is the polyline's own length, rounded up: (20,6)→(9,17)→(4,12).
       `stroke-dashoffset` is set ONLY inside the keyframe, never as a static
       declaration here, and that distinction is load-bearing: with the offset
       declared on the element, anything that stops the animation from running
       — a `* { animation: none }` reset in a host theme, a print stylesheet,
       an extension — leaves the offset at 24 and the tick permanently
       invisible. The card would then show a chosen state with no check in it,
       which is the one signal that has to survive. Declared this way the
       resting state is a fully drawn tick and the animation is pure addition. */
    #so-booking-widget[data-template="chatbook"] .so-bw-card-check--visible svg polyline {
        stroke-dasharray: 24;
        animation: so-bw-chat-draw 260ms 90ms ease-out;
    }
}
@keyframes so-bw-chat-pop {
    from { opacity: 0; transform: scale(.4); }
    to   { opacity: 1; transform: none; }
}
@keyframes so-bw-chat-draw {
    from { stroke-dashoffset: 24; }
    to   { stroke-dashoffset: 0; }
}

/* ── 36.26b · The team, at the same scale ────────────────────────────────
   The staff step arrived at the same problem from the other side: the shared
   card reserves room for a photo, a name, a bio and a profile link at
   full-step scale, so three colleagues filled 800px of thread. Same treatment
   as the services — a smaller avatar, one line of bio, and the reserved
   heights released — so the whole team is one glance rather than one scroll.

   The grid keeps its own `--staff` two/three-up from 36.7; only the tile's
   internals change here. */
#so-booking-widget[data-template="chatbook"] .so-bw-card--staff {
    min-height: 0;
    padding: 10px;
    gap: 8px;
    align-items: center;
}
#so-booking-widget[data-template="chatbook"] .so-bw-card--staff .so-bw-card-icon--avatar {
    --so-bw-icon-size: 40px;
    --so-bw-icon-svg-size: 20px;
    --so-bw-icon-text-size: 14px;
    flex: 0 0 auto;
}
#so-booking-widget[data-template="chatbook"] .so-bw-card--staff .so-bw-card-title {
    font-size: 12.5px;
    line-height: 1.3;
    min-height: 0;
    margin-bottom: 0;
}
/* One line of bio. A specialist's biography is a paragraph on the staff page
   and a subtitle here; clamping it is what keeps the three tiles the same
   height without asking the admin to write shorter. */
#so-booking-widget[data-template="chatbook"] .so-bw-card--staff .so-bw-card-meta {
    font-size: 11px;
    line-height: 1.4;
    margin-bottom: 0;
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 1;
    line-clamp: 1;
    overflow: hidden;
}
#so-booking-widget[data-template="chatbook"] .so-bw-card--staff .so-bw-card-profile {
    font-size: 10.5px;
    padding: 0;
    margin-top: 2px;
}
/* "Any Available" is the escape hatch, not a teammate — full width, and no
   taller than the line it carries. */
#so-booking-widget[data-template="chatbook"] .so-bw-card--any {
    min-height: 0;
    padding: 10px;
}
#so-booking-widget[data-template="chatbook"] .so-bw-card--any .so-bw-card-icon--avatar {
    --so-bw-icon-size: 34px;
    --so-bw-icon-svg-size: 17px;
    --so-bw-icon-text-size: 12px;
}

/* ── 36.27 · The answer, as the thing that was chosen ────────────────────
   A reply bubble carrying the service's own photograph, name, length and
   price. It is what makes the grid safe to collapse: the choice does not
   disappear when the step does, it MOVES — from a tile you were picking to a
   card you have picked, in the same visual language. Tap it to change it,
   like any other reply. */
#so-booking-widget[data-template="chatbook"] .so-bw-chatbubble--rich {
    padding: 6px 6px 6px 7px;
    max-width: 86%;
}
#so-booking-widget[data-template="chatbook"] .so-bw-chatpicks {
    display: flex;
    flex-direction: column;
    gap: 6px;
}
#so-booking-widget[data-template="chatbook"] .so-bw-chatpick {
    display: flex;
    align-items: center;
    gap: 9px;
    text-align: left;
}
#so-booking-widget[data-template="chatbook"] .so-bw-chatpick-media {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 38px;
    height: 38px;
    border-radius: var(--so-bw-r-8);
    overflow: hidden;
    background: color-mix(in srgb, var(--so-bw-primary) 16%, #fff);
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
    font-size: 12px;
    font-weight: 700;
}
#so-booking-widget[data-template="chatbook"] .so-bw-chatpick-media--round { border-radius: 999px; }
#so-booking-widget[data-template="chatbook"] .so-bw-chatpick-media img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}
#so-booking-widget[data-template="chatbook"] .so-bw-chatpick-body {
    display: flex;
    flex-direction: column;
    gap: 1px;
    min-width: 0;
}
#so-booking-widget[data-template="chatbook"] .so-bw-chatpick-name {
    font-size: 13px;
    font-weight: 600;
    line-height: 1.3;
    color: var(--so-bw-ds-n900);
    overflow-wrap: anywhere;
}
#so-booking-widget[data-template="chatbook"] .so-bw-chatpick-meta {
    display: flex;
    align-items: center;
    gap: 5px;
    font-size: 11.5px;
    color: var(--so-bw-ds-n500);
}
#so-booking-widget[data-template="chatbook"] .so-bw-chatpick-price {
    font-weight: 700;
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}
#so-booking-widget[data-template="chatbook"] .so-bw-chatbubble--rich .so-bw-chatmeta {
    padding: 0 4px 1px;
}

/* ── 36.28 · The composing beat ──────────────────────────────────────────
   While `chatTyping` is true the answer has landed and the next question has
   not been asked yet, so the live question and the reply bar stand down and
   the dots are the only thing in the log. That ordering is the entire effect:
   answer → pause → question, instead of the whole screen repainting at once.

   It costs nothing in engine terms. The step has ALREADY advanced; only the
   paint is deferred, for 620ms, and `_chatType()` skips the wait entirely
   under reduced motion so the question appears immediately. */
#so-booking-widget[data-template="chatbook"] .so-bw-container--typing .so-bw-step,
#so-booking-widget[data-template="chatbook"] .so-bw-container--typing .so-bw-nav,
#so-booking-widget[data-template="chatbook"] .so-bw-container--typing .so-bw-chatturn--live {
    display: none !important;
}
#so-booking-widget[data-template="chatbook"] .so-bw-chatbubble--typing {
    padding: 11px 14px;
}
#so-booking-widget[data-template="chatbook"] .so-bw-chatdots {
    display: flex;
    align-items: center;
    gap: 4px;
}
#so-booking-widget[data-template="chatbook"] .so-bw-chatdot {
    width: 6px;
    height: 6px;
    border-radius: 999px;
    background: var(--so-bw-ds-n300);
}
@media (prefers-reduced-motion: no-preference) {
    #so-booking-widget[data-template="chatbook"] .so-bw-chatdot {
        animation: so-bw-chat-bounce 1.1s infinite ease-in-out both;
    }
    #so-booking-widget[data-template="chatbook"] .so-bw-chatdot:nth-child(2) { animation-delay: .15s; }
    #so-booking-widget[data-template="chatbook"] .so-bw-chatdot:nth-child(3) { animation-delay: .3s; }
}
@keyframes so-bw-chat-bounce {
    0%, 70%, 100% { opacity: .45; transform: translateY(0); }
    35%           { opacity: 1;   transform: translateY(-3px); }
}

/* ── 36.29 · Arrival, and the rhythm of the log ──────────────────────────
   Tighter between a question and its own answer than between one exchange and
   the next, so the thread groups into turns instead of reading as one evenly
   spaced list. This is the spacing doing the work the timestamps used to be
   asked to do. */
#so-booking-widget[data-template="chatbook"] .so-bw-chatlog { gap: 4px; }
#so-booking-widget[data-template="chatbook"] .so-bw-chatturn--ask { margin-top: 10px; }
#so-booking-widget[data-template="chatbook"] .so-bw-chatlog > .so-bw-chatturn:first-child { margin-top: 0; }
/* The live step is the newest bubble, so it arrives the way a bubble does. */
@media (prefers-reduced-motion: no-preference) {
    #so-booking-widget[data-template="chatbook"] .so-bw-step {
        animation: so-bw-chat-in 220ms ease-out both;
    }
    #so-booking-widget[data-template="chatbook"] .so-bw-card--service,
    #so-booking-widget[data-template="chatbook"] .so-bw-time-slot,
    #so-booking-widget[data-template="chatbook"] .so-bw-suggest-opt {
        transition: border-color 140ms ease, background-color 140ms ease,
                    box-shadow 140ms ease, transform 140ms ease;
    }
    #so-booking-widget[data-template="chatbook"] .so-bw-card--service:hover {
        transform: translateY(-1px);
    }
}

/* ── 36.30 · The rails stay quiet ────────────────────────────────────────
   The conversation is the subject; the two columns beside it are reference.
   They say so by being a step down in weight and a step up in restraint —
   n25 rather than white, so the thread is the only lit surface of the three.
   The summary still updates live; it simply stops competing to be read. */
#so-booking-widget:where([data-cwmin~="960"])[data-template="chatbook"] .so-bw-chatside,
#so-booking-widget:where([data-cwmin~="960"])[data-template="chatbook"] .so-bw-rail {
    background: var(--so-bw-ds-n25);
}
#so-booking-widget:where([data-cwmin~="960"])[data-template="chatbook"] .so-bw-body {
    background: var(--so-bw-ds-n0);
}

/* ── 36.31 · One row, then ask ───────────────────────────────────────────
   A choice step shows its FIRST ROW and offers the rest behind a toggle:
   three tiles on a desktop, two on a tablet, one on a phone. In a thread the
   list is a reply, not a page — twelve services push the question that
   introduced them off the top of the pane and turn a conversation back into a
   catalogue. Three or fewer are shown whole (see chatCanCollapse): at that
   size the toggle costs more than it saves.

   Nothing is filtered in JS. The full list is rendered, so search, keyboard
   navigation, selection, the auto-advance and the "N selected" tally all
   still see every item — only the paint is cut. Expanding is therefore free
   and cannot lose state.

   THE THREE TIERS ARE MUTUALLY EXCLUSIVE, via `:not([data-cwmin~="…"])`,
   rather than three cumulative rules. `data-cwmin` lists EVERY breakpoint the
   container is at or above, so at 1000px the 600 rule and the 960 rule both
   match; written cumulatively the 600 tier would go on hiding the third card
   that the 960 tier is trying to show, and each tier would need a companion
   rule to undo the one below it. Exclusive ranges need no undoing.

   `:nth-child(… of S)` — the SELECTOR list form — is what makes this count
   the cards rather than the DOM. Alpine leaves its `<template x-for>` in place
   as a real element, and the staff grid additionally opens with the full-width
   "Any Available" card, so plain `:nth-child` counts both and cuts the row in
   the wrong place. `of .so-bw-card:not(.so-bw-card--any)` counts exactly the
   things a customer would count. Behind `@supports` because the form is newer
   than the rest of this file: where it is unsupported the list simply renders
   in full, which is the behaviour that shipped before this block existed. */
@supports selector(:nth-child(1 of .so-bw-card)) {
    /* One up — below 600px of container. */
    #so-booking-widget:not([data-cwmin~="600"])[data-template="chatbook"] .so-bw-chatcut
        > .so-bw-card:nth-child(n+2 of .so-bw-card:not(.so-bw-card--any)) { display: none !important; }
    /* Two up — 600px to 959px. */
    #so-booking-widget[data-cwmin~="600"]:not([data-cwmin~="960"])[data-template="chatbook"] .so-bw-chatcut
        > .so-bw-card:nth-child(n+3 of .so-bw-card:not(.so-bw-card--any)) { display: none !important; }
    /* Three up — 960px and above. */
    #so-booking-widget[data-cwmin~="960"][data-template="chatbook"] .so-bw-chatcut
        > .so-bw-card:nth-child(n+4 of .so-bw-card:not(.so-bw-card--any)) { display: none !important; }
}

/* The toggle. Emitted for every layout, revealed for this one — the same
   contract as `.so-bw-suggest` and the rest of section 36's primitives. */
#so-booking-widget .so-bw-chatmore { display: none; }
#so-booking-widget[data-template="chatbook"] .so-bw-chatmore {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    width: 100%;
    min-height: 40px;
    margin-top: 8px;
    padding: 8px 14px;
    border: 1px solid var(--so-bw-ds-n200);
    border-radius: 999px;
    background: var(--so-bw-ds-n0);
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
    font-family: inherit;
    font-size: 12.5px;
    font-weight: 600;
    cursor: pointer;
}
#so-booking-widget[data-template="chatbook"] .so-bw-chatmore:hover {
    border-color: var(--so-bw-primary);
    background: color-mix(in srgb, var(--so-bw-primary) 5%, #fff);
}
#so-booking-widget[data-template="chatbook"] .so-bw-chatmore:focus-visible {
    outline: 2px solid var(--so-bw-primary);
    outline-offset: 2px;
}
#so-booking-widget[data-template="chatbook"] .so-bw-chatmore-chev--up { transform: rotate(180deg); }
@media (prefers-reduced-motion: no-preference) {
    #so-booking-widget[data-template="chatbook"] .so-bw-chatmore-chev {
        transition: transform 160ms ease;
    }
}

/* ── 36.32 · The image is part of the card, not a panel inside it ────────
   The band and the tile have to read as ONE rounded object. Three things are
   needed and all three are load-bearing:

     · the card clips (`overflow: hidden`), so the photo cannot square off the
       corners it sits in;
     · the band carries the card's own top corners, less the 1px border it
       sits inside — without the subtraction the two curves are concentric but
       not parallel, which is the hairline of background that shows between a
       rounded image and a rounded border;
     · the image fills the band (`object-fit: cover`, 100%/100%), so a portrait
       photo crops instead of letterboxing onto the band's tint.

   The selected card draws a 2px border, so its inner radius is offset by 2
   rather than 1 — otherwise choosing a service visibly shifts its photo.

   NOTE ON THE CARD'S OWN `overflow`. It is declared below and it does NOT
   take effect: `#so-booking-widget[data-template] .so-bw-card` sets
   `overflow: visible !important` as a deliberate global invariant ("text may
   break, the box may not clip") that no non-important rule can answer. That
   is fine here, and worth not fighting — the only child that needs clipping
   is the media band, which carries its own `overflow: hidden` and its own
   matched corners, so the seam is closed at the element that actually has an
   image in it. Measured: the band's inset from the card equals the border
   width exactly on all three sides, and its radius tracks the card's less
   that border. The declaration stays because it states the intent and becomes
   live for free if that invariant is ever relaxed. */
#so-booking-widget[data-template="chatbook"] .so-bw-card--service {
    overflow: hidden;
    border-radius: var(--so-bw-r-12);
}
#so-booking-widget[data-template="chatbook"] .so-bw-card--service .so-bw-card-media {
    overflow: hidden;
    border-radius: calc(var(--so-bw-r-12) - 1px) calc(var(--so-bw-r-12) - 1px) 0 0;
}
#so-booking-widget[data-template="chatbook"] .so-bw-card--service.so-bw-card--selected .so-bw-card-media {
    border-radius: calc(var(--so-bw-r-12) - 2px) calc(var(--so-bw-r-12) - 2px) 0 0;
}
#so-booking-widget[data-template="chatbook"] .so-bw-card--service .so-bw-card-media-img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
    border-radius: 0;
}

/* ── 36.33 · The team, laid out as people ────────────────────────────────
   The staff grid was inheriting grid-auto-rows: 1fr, which sizes EVERY row to
   the tallest cell in the grid — and the full-width "Any Available" card is in
   row one. So one roomy escape hatch made all three colleagues 213px tall, in
   163px-wide columns, with the name wrapping to four lines beside a 40px
   avatar. Two fixes, both structural rather than cosmetic:

     · rows size to their own content, so Any Available stops setting the
       height of the people below it;
     · the tile stacks. A horizontal avatar-then-text row needs ~240px to read
       and these columns are 163px, which is what crushed the name into a
       column two words wide. Stacked, the same 163px is generous. */
#so-booking-widget[data-template="chatbook"] .so-bw-card-grid--staff {
    grid-auto-rows: auto;
    /* stretch, not start: rows now size to their own content, so the two
       tiles in a row should also FINISH together — with `start` a colleague
       with no bio left a ragged bottom edge beside one with a long bio. */
    align-items: stretch;
}
#so-booking-widget[data-template="chatbook"] .so-bw-card--staff:not(.so-bw-card--any) {
    flex-direction: column;
    align-items: center;
    gap: 6px;
    padding: 12px 10px;
    text-align: center;
}
/* Flex column so View profile can be pushed to the foot of the tile: with the
   row stretched, the buttons then line up across it whatever each bio did
   above them. */
#so-booking-widget[data-template="chatbook"] .so-bw-card--staff:not(.so-bw-card--any) .so-bw-card-body {
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    width: 100%;
    min-width: 0;
    flex: 1 1 auto;
}
#so-booking-widget[data-template="chatbook"] .so-bw-card--staff:not(.so-bw-card--any) .so-bw-card-profile {
    margin-top: auto;
}
/* The bio is one line. It uses `.so-bw-card-meta` WITHOUT the `--clamp`
   modifier, so nothing was clamping it and a two-sentence biography ran to
   three lines in a 168px tile. The clamp needs all three properties together —
   `-webkit-line-clamp` alone does nothing without `display: -webkit-box`,
   which is why the earlier single declaration had no effect. */
#so-booking-widget[data-template="chatbook"] .so-bw-card--staff:not(.so-bw-card--any) .so-bw-card-meta {
    display: -webkit-box !important;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 1 !important;
    line-clamp: 1;
    overflow: hidden;
    margin-bottom: 0;
}
/* The tick is rendered on every tile whether or not it is chosen, so in a
   stacked card it held an empty 22px row plus its gap at the foot of all of
   them — the dead space under every View profile button. Pinned to the
   corner, the same move 36.26 makes on the service tile and for the same
   reason: a selection mark belongs on the card, not in its flow. */
#so-booking-widget[data-template="chatbook"] .so-bw-card--staff:not(.so-bw-card--any) { position: relative; }
#so-booking-widget[data-template="chatbook"] .so-bw-card--staff:not(.so-bw-card--any) .so-bw-card-check {
    position: absolute;
    top: 6px;
    right: 6px;
    z-index: 2;
}
/* Two lines for a person, not one: a full name is not a truncatable label,
   and "Maya Krishnamurthy-" is worse than a wrap. */
#so-booking-widget[data-template="chatbook"] .so-bw-card--staff:not(.so-bw-card--any) .so-bw-card-title {
    -webkit-line-clamp: 2 !important;
    line-clamp: 2;
}
/* Any Available keeps the horizontal row — it is an instruction, not a face. */
#so-booking-widget[data-template="chatbook"] .so-bw-card--any {
    flex-direction: row;
    align-items: center;
    text-align: left;
}

/* ── 36.34 · View profile is a control, not a footnote ───────────────────
   It shipped as a 10.5px text link that wrapped to two lines inside a narrow
   tile ("View / profile"), which reads as an accident. As the tile's own
   secondary action it gets a real target: the full width of the card, a
   border, a 32px box. It stays visually quieter than the card it sits in —
   selecting the person is the primary act and reading about them is the
   aside. */
#so-booking-widget[data-template="chatbook"] .so-bw-card-profile {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 5px;
    width: 100%;
    min-height: 32px;
    margin-top: 4px;
    padding: 6px 10px;
    border: 1px solid var(--so-bw-ds-n200);
    border-radius: 999px;
    background: var(--so-bw-ds-n0);
    color: var(--so-bw-ds-n700);
    font-size: 11.5px;
    font-weight: 600;
    line-height: 1.2;
    white-space: nowrap;
}
#so-booking-widget[data-template="chatbook"] .so-bw-card-profile:hover {
    border-color: var(--so-bw-primary);
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
    background: color-mix(in srgb, var(--so-bw-primary) 5%, #fff);
}

/* ── 36.35 · The expander is compact ─────────────────────────────────────
   A full-width pill read as a third button competing with Back and Continue.
   It is a disclosure, so it takes the size of one: centred, only as wide as
   its label, and quieter than either real action. 36px keeps it comfortably
   past the 24px target minimum while staying visibly subordinate. */
#so-booking-widget[data-template="chatbook"] .so-bw-chatmore {
    width: auto;
    min-height: 36px;
    margin: 10px auto 0;
    padding: 6px 14px;
    font-size: 12px;
    background: transparent;
    border-color: var(--so-bw-ds-n200);
    color: var(--so-bw-ds-n500);
}
#so-booking-widget[data-template="chatbook"] .so-bw-chatmore:hover {
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
    border-color: var(--so-bw-primary);
    background: color-mix(in srgb, var(--so-bw-primary) 5%, #fff);
}

/* ── 36.36 · One primary action in the reply bar ─────────────────────────
   Back was a tinted pill the same size as Continue, so the bar offered two
   equally weighted choices and the eye had to read them to find the commit.
   Back becomes a ghost — label plus chevron, no fill, no border — and
   Continue keeps the whole of the accent. Back also stops stretching:
   flex: 0 0 auto against the nav's space-between means it takes its label's
   width and Continue keeps the emphasis of the larger target.

   Contrast is unchanged: n500 on white is 5.9:1, and the control keeps the
   focus ring the shared button rules give it. */
#so-booking-widget[data-template="chatbook"] .so-bw-nav .so-bw-btn--secondary {
    flex: 0 0 auto;
    padding: 8px 14px;
    border-color: transparent;
    background: transparent;
    color: var(--so-bw-ds-n500);
    font-weight: 600;
}
#so-booking-widget[data-template="chatbook"] .so-bw-nav .so-bw-btn--secondary:hover {
    background: var(--so-bw-ds-n50);
    color: var(--so-bw-ds-n900);
    border-color: transparent;
}

/* ── 36.37 · The scrollbar stops touching the words ──────────────────────
   .so-bw-body is the scroll region, and its right padding was the same gutter
   as its left — so the thumb sat directly against the bubbles. The extra 6px
   is the breathing room between the two.

   `scrollbar-gutter: stable` is restated rather than introduced: the shared
   `.so-bw-body` rule already sets it for every template, so the track is
   reserved whether or not it is currently shown and the thread does not shift
   sideways the moment it grows past one screen. Kept here so this block reads
   as the whole answer to "why does the content clear the scrollbar", instead
   of half of it sitting 21,000 lines away. */
#so-booking-widget[data-template="chatbook"] .so-bw-body {
    scrollbar-gutter: stable;
    padding-right: calc(var(--so-bw-gutter) + 6px);
}

/* ── 36.38 · Member benefits, at banner height ───────────────────────────
   The banner is authored with INLINE styles, so every value here needs
   !important to reach it — the same thing section 28 does for Dusk, and the
   reason both blocks look heavier than they are.

   It is a status strip, not a feature card: the customer needs to know the
   discount is on and how much it is. At 12/16 padding with a 36px mark and a
   22px percentage it stood ~76px tall at the top of a 420px floating panel —
   a tenth of the panel spent on something already reflected in every price
   below it. Roughly halved, with the hierarchy intact. */
#so-booking-widget[data-template="chatbook"] .so-bw-member-banner {
    padding: 8px 12px !important;
    margin-bottom: 10px !important;
    gap: 10px !important;
    border-radius: var(--so-bw-r-10) !important;
    box-shadow: none !important;
}
#so-booking-widget[data-template="chatbook"] .so-bw-member-banner__mark {
    width: 28px !important;
    height: 28px !important;
}
#so-booking-widget[data-template="chatbook"] .so-bw-member-banner__mark svg { width: 15px; height: 15px; }
#so-booking-widget[data-template="chatbook"] .so-bw-member-banner__label {
    font-size: 9.5px !important;
    letter-spacing: .07em !important;
}
#so-booking-widget[data-template="chatbook"] .so-bw-member-banner__plan { font-size: 12.5px !important; }
#so-booking-widget[data-template="chatbook"] .so-bw-member-banner__tier {
    font-size: 9px !important;
    padding: 1px 6px !important;
}
#so-booking-widget[data-template="chatbook"] .so-bw-member-banner__off { padding-left: 10px !important; }
#so-booking-widget[data-template="chatbook"] .so-bw-member-banner__pct { font-size: 16px !important; }
#so-booking-widget[data-template="chatbook"] .so-bw-member-banner__offlabel { font-size: 9px !important; }

/* ==========================================================================
   37 · HERO — THE STAFF STEP
   --------------------------------------------------------------------------
   REPORTED against Hero: the staff step is mostly empty when a business has
   two or three people; the cards are taller than their content; the selected
   card is hard to spot; "Any Available" reads as just another row; and
   "View profile" competes with the card's own name for attention.

   Nothing here changes the step's ARCHITECTURE. It is still one grid of
   avatar-beside-text cards inside the lifted white panel, still the same
   markup, still the same primitives — the tuning is width, height, weight and
   colour, all scoped to `[data-template="hero"]` so the other twenty-two
   templates read exactly as they did.

   The three hooks this section aims at (`--staff` on the grid and on every
   card, `--any` on the first one) were added to the staff step's markup for
   it. They carry no styling of their own anywhere else in the file.

   WHY THE STAFF GRID AND NOT `.so-bw-card-grid`: the location step emits the
   same two classes, and none of the decisions below are true of it — a
   location has no bio to clamp, no profile button, and no default option.
   ========================================================================== */

/* ── 37.1 · The grid stops stretching two cards across a desktop ──────────
   The shared grid is `repeat(auto-fit, minmax(min(100%, 240px), 1fr))`, and
   the `1fr` is the whole report: auto-fit COLLAPSES the tracks nobody filled
   and then hands their width to the tracks that remain. On a 1200px panel

     6 staff   4 tracks x 291   cards full of content
     2 staff   2 tracks x 594   an avatar, a name, and 500px of nothing

   Same rule, opposite result, because the stretch has no upper bound.

   So the base keeps `1fr` — with a full roster it is right, and it wastes
   nothing — and a SHORT roster switches to a fixed 320px track. The cards then
   sit at a readable width next to each other and `justify-content: center`
   places the cluster in the panel instead of ragging it left.

   Counted with sibling combinators rather than `:nth-child(4)`: Alpine leaves
   the `<template x-for>` element in the DOM as a sibling of the cards it
   inserts, so nth-child counts a node that never renders and the threshold
   lands one card early. `~` only ever sees real cards.

   `align-content: start` is the vertical half of the same story — the grid is
   a bounded scroll region (`--scroll` caps it at 60vh), and a bounded grid
   distributes leftover height into its rows by default, which is how three
   cards came to be spread down a 500px box. */
#so-booking-widget[data-template="hero"] .so-bw-card-grid--staff {
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr));
    justify-content: center;
    align-content: start;
    gap: 12px;
}
#so-booking-widget[data-template="hero"] .so-bw-card-grid--staff:not(:has(> .so-bw-card ~ .so-bw-card ~ .so-bw-card ~ .so-bw-card)) {
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 320px));
}

/* ── 37.2 · A shorter card, mostly by not printing a whole biography ──────
   Three sources of height, in the order they cost the most:

     the bio      unbounded (`.so-bw-card-meta` is deliberately unclamped)
     the avatar   48px, which sets the card's floor on its own
     the padding  12/14

   The clamp is the one that matters and it is the one the shared rule argues
   AGAINST — for services, correctly: a clamped description is a catalogue of
   identical stubs and there is nowhere else to read the rest. A staff bio has
   somewhere else. "View profile" is directly underneath it and opens the full
   text, so two lines here is a summary with a way in, not a truncation.

   The avatar drops 48 -> 44 through the design-system tokens rather than a raw
   width, so the tile, its SVG and its initials all move together. Declared on
   the CARD: `.so-bw-card-icon` reads `width: var(--so-bw-icon-size)` at (0,1,0)
   and a per-template rule that sets the token directly on the tile can lose to
   it — inheritance from the parent has no such fight to pick. */
#so-booking-widget[data-template="hero"] .so-bw-card--staff {
    --so-bw-icon-size: 44px;
    --so-bw-icon-svg-size: 24px;
    --so-bw-icon-text-size: 17px;
    padding: 10px 12px;
    gap: 10px;
}
#so-booking-widget[data-template="hero"] .so-bw-card--staff .so-bw-card-meta {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    overflow: hidden;
    margin-bottom: 0;
}

/* ── 37.3 · Selection you can find without hunting ────────────────────────
   WHERE THE SELECTION RING ACTUALLY LIVES, because the obvious edit is a dead
   letter here. Section 14's block draws it as a `::after` OVERLAY and, in the
   same rule, kills the shadow it replaced:

     #so-booking-widget[data-template] .so-bw-card.so-bw-card--selected,
     #so-booking-widget .so-bw-card.so-bw-card--selected { box-shadow: none !important }

   with the wash pinned by an equally !important `color-mix(primary 15%)` in
   section 9. So a per-template `box-shadow` on a selected card computes to
   `none` and a per-template `background` never lands — no warning, no visible
   change, and the rule reads perfectly correct in the file. Both were written
   that way deliberately (an inset ring vanishes behind a photo-led card's own
   children), so this section works WITH the overlay rather than around it.

   What Hero adds on top of that shared 2px ring is a halo: a second, wider
   band at low opacity drawn on the overlay itself. It reads as depth at a
   glance from across the grid, where the ring alone reads as a slightly darker
   edge, and because it is a shadow on an absolutely-positioned overlay it
   costs no layout — the card does not move, and neither do its neighbours.

   The name takes the accent's ink so the state is carried by the card's
   CONTENT as well as by its edge — the one part of the card a customer is
   already looking at.

   `color-mix` rather than `rgba(var(--so-bw-primary-rgb) …)`: templates
   override `--so-bw-primary`, and only some restate the -rgb triplet, so that
   token cannot be trusted to track the brand. */
#so-booking-widget[data-template="hero"] .so-bw-card--staff.so-bw-card--selected::after {
    box-shadow: 0 0 0 4px color-mix(in srgb, var(--so-bw-primary) 20%, transparent);
}
#so-booking-widget[data-template="hero"] .so-bw-card--staff.so-bw-card--selected .so-bw-card-title {
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}
/* The tick was already there and already the right shape; it just appeared
   instantly, at full size, in the corner of a card that had not otherwise
   moved. Scaling it in off the same `--visible` flag that already toggles it
   makes it read as the answer to the click. */
#so-booking-widget[data-template="hero"] .so-bw-card--staff .so-bw-card-check {
    width: 26px;
    height: 26px;
    transform: scale(0.7);
    transition: visibility 0s,
                opacity var(--so-bw-transition),
                transform var(--so-bw-transition);
}
#so-booking-widget[data-template="hero"] .so-bw-card--staff .so-bw-card-check--visible {
    transform: scale(1);
}
@media (prefers-reduced-motion: reduce) {
    #so-booking-widget[data-template="hero"] .so-bw-card--staff .so-bw-card-check {
        transform: none;
        transition: visibility 0s, opacity var(--so-bw-transition);
    }
}

/* ── 37.4 · "Any Available" leads, because it is the default ──────────────
   It is the option most customers take and the only one that cannot fail on
   availability, and it was rendered as the first of N identical rows.

   Raised WITHOUT taking it out of the grid: spanning it across the row would
   break 37.1's cluster the moment the roster is short, and a card of a
   different shape stops reading as one of the choices. What it gets instead is
   a tinted surface, a definite edge, the avatar the others gave up, and a
   heavier name — the same card, weighted.

   The tint is mixed INTO the card surface rather than laid over it, so it
   stays a card on a dark template instead of a light patch.

   Deliberately (1,2,0), below 37.3's (1,3,0): once this card is chosen the
   selected treatment must win outright, or the default would be the one option
   whose selection is ambiguous. */
#so-booking-widget[data-template="hero"] .so-bw-card--any {
    --so-bw-icon-size: 48px;
    --so-bw-icon-svg-size: 26px;
    --so-bw-icon-text-size: 18px;
    background: color-mix(in srgb, var(--so-bw-primary) 7%, var(--so-bw-card-bg));
    border-color: color-mix(in srgb, var(--so-bw-primary) 40%, var(--so-bw-border));
}
#so-booking-widget[data-template="hero"] .so-bw-card--any .so-bw-card-title {
    font-size: 16px;
    font-weight: 700;
}
#so-booking-widget[data-template="hero"] .so-bw-card--any .so-bw-card-meta {
    color: var(--so-bw-text-secondary);
}

/* ── 37.5 · "View profile" is a detour, and now looks like one ────────────
   It was accent-coloured 12px semibold with an icon, directly under a 15px
   name in body ink — the loudest thing in the card was the one control that
   does not book anything. As a hairline chip in secondary ink it is still
   obviously pressable, still finds the accent on hover, and no longer argues
   with the name above it.

   Its own `min-height: 32px` (section 21) goes, because that was a target-size
   floor for a bare text button with no box of its own; the chip's padding and
   border now measure ~26px, over WCAG 2.2's 24px minimum. The touch block
   below is what actually carries fingers, and it is restated here because
   section 29's `@media (hover: none)` rule is (1,1,0) and these are (1,3,0). */
#so-booking-widget[data-template="hero"] .so-bw-card--staff .so-bw-card-profile {
    margin-top: 7px;
    min-height: 0;
    padding: 4px 10px;
    border: 1px solid var(--so-bw-border);
    border-radius: 999px;
    background: transparent;
    font-size: 11.5px;
    font-weight: 500;
    color: var(--so-bw-text-secondary);
    transition: color var(--so-bw-transition),
                border-color var(--so-bw-transition);
}
#so-booking-widget[data-template="hero"] .so-bw-card--staff .so-bw-card-profile:hover {
    opacity: 1;
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
    border-color: var(--so-bw-primary);
}
/* The shared focus ring rounds to `--so-bw-r-4`, which was right for a text
   button and traces the wrong shape around a pill. */
#so-booking-widget[data-template="hero"] .so-bw-card--staff .so-bw-card-profile:focus-visible {
    border-radius: 999px;
}
#so-booking-widget[data-template="hero"] .so-bw-card--staff .so-bw-card-profile svg {
    width: 11px;
    height: 11px;
}
@media (hover: none) {
    #so-booking-widget[data-template="hero"] .so-bw-card--staff .so-bw-card-profile {
        padding: 7px 12px;
    }
}

/* ── 37.6 · The phone ─────────────────────────────────────────────────────
   `min(100%, 240px)` already collapses 37.1 to one full-width column on a
   narrow container, so the grid needs nothing here. What does need saying is
   that the card gives its width back: the 48px "Any Available" avatar plus the
   chip below the bio is a lot of furniture on a 320px row, so the default
   returns to the roster's 44px and the gap tightens.

   `data-cw` is the WIDGET's width, not the viewport — a 420px floating panel
   on a desktop gets this too, which is the point. */
#so-booking-widget:where([data-cw~="480"])[data-template="hero"] .so-bw-card--any {
    --so-bw-icon-size: 44px;
    --so-bw-icon-svg-size: 24px;
    --so-bw-icon-text-size: 17px;
}
#so-booking-widget:where([data-cw~="480"])[data-template="hero"] .so-bw-card--any .so-bw-card-title {
    font-size: 15px;
}
#so-booking-widget:where([data-cw~="480"])[data-template="hero"] .so-bw-card-grid--staff {
    gap: 10px;
}


/* ═══════════════════════════════════════════════════════════════════════════
   40 · THE SHELL EMBEDS — one component, for all twenty-four
   ═══════════════════════════════════════════════════════════════════════════

   Everything in this section is scoped to a widget rendered INSIDE a floating
   bubble or a popup dialog, and applies to every template. Inline embeds are
   deliberately untouched: each architecture was designed against a full-width
   page column and keeps exactly what it has there. A shell is a different
   brief — a 420px box capped at 80vh, sitting over someone else's page — and
   the same card that reads well in a column reads cramped in it.

   ── WHY THE SPECIFICITY LOOKS HEAVY ────────────────────────────────────────
   Every selector below is written `.so-bw-<shell> #so-booking-widget[data-template] …`.
   The shell classes are only classes, and almost everything they need to
   answer is an ID rule (`#so-booking-widget .so-bw-card-profile`, (1,1,0)) or
   a template rule ((1,2,0)). A `.so-bw-floating__panel .so-bw-card-profile`
   is (0,2,0) and loses to both. Including the ID and the `[data-template]`
   attribute puts these at (1,3,0), which clears the catalogue without needing
   `!important` anywhere except where the markup itself is inline-styled.

   ── WHAT IS NOT DONE HERE ──────────────────────────────────────────────────
   No template's ARCHITECTURE is overridden. The two-pane rails still pane,
   Dusk stays dark, Coral Pop keeps its radius, Week Board keeps its columns.
   This section changes the frame, the choice lists, the two chrome bars and
   the member strip — the parts every template shares. */

/* ── 40.1 · One shell, not two ───────────────────────────────────────────
   The double-card bug, and it affected all twenty-four: the shell paints a
   white panel with a 14px radius and a drop shadow, then the widget painted
   its OWN container inside it — another background, another radius, a border,
   a second shadow — with the root's 12–24px page inset around all of it. Two
   rounded boxes, one inside the other, with a rim of grey between them.

   The fix keeps the PANEL as the frame (its radius and shadow are what the
   page sees) and lets the CONTAINER fill it edge to edge. Note the direction:
   the container keeps its own BACKGROUND and gives up its border, radius,
   shadow and inset. That ordering matters — stripping the container's
   background instead would have shown the panel's white through Dusk's dark
   surface, Frost's gradient, Editorial's ivory and Coral Pop's blush. Filling
   the panel with the template's own colour is what makes this work for all
   twenty-four rather than for the white ones. */
.so-bw-floating__panel,
.so-bw-popup__dialog {
    padding: 0;
    /* The panel clips, so the widget's own header takes the panel's top
       corners and its nav takes the bottom ones. */
    overflow: hidden;
}
/* `!important` is required, and only here. Six templates set a root inset for
   the PHONE with `!important` — e.g.
   `#so-booking-widget:where([data-cw~="480"])[data-template="dialog"]
   { padding: 14px 6px !important }` — and a 420px floating panel matches that
   same `data-cw~="480"` gate. Those rules are right about a phone viewport and
   wrong about a bubble: in a bubble the shell already supplies the inset, so
   the widget's own becomes the rim of grey that made it look like a second
   card. Nothing else in this section needs to shout. */
.so-bw-floating__panel #so-booking-widget,
.so-bw-popup__dialog #so-booking-widget {
    padding: 0 !important;
}
.so-bw-floating__panel #so-booking-widget[data-template][data-position] .so-bw-container,
.so-bw-popup__dialog #so-booking-widget[data-template][data-position] .so-bw-container {
    max-width: 100%;
    margin: 0;
    border: 0;
    border-radius: 0;
    box-shadow: none;
}

/* ── 40.2 · Room between the panel and its launcher ──────────────────────
   The shell ships `bottom: calc(100% + 12px)`, which reads as the panel
   resting ON the button rather than floating above it. 18px sits in the
   16–20px the design calls for and is enough to read as a gap at every
   trigger size the Studio offers. */
/* `:has(#so-booking-widget)` is carrying the specificity, not just narrowing
   the target. The shell's own `bottom: calc(100% + 12px)` is emitted by
   BookingShortcode::positionStyles() as inline CSS AFTER this stylesheet, so a
   plain `.so-bw-floating__panel` here is a (0,1,0) tie that loses on order —
   measured failing on all 72 template/width combinations. `:has()` takes the
   specificity of its argument, so the ID inside it lifts this to (1,1,0) and
   it wins regardless of source order. */
.so-bw-floating__panel:has(#so-booking-widget) {
    bottom: calc(100% + 18px);
}

/* ── 40.3 · The scrollbar stops touching the words ───────────────────────
   `.so-bw-body` is the scroll region and its right padding was the same
   gutter as its left, so the thumb sat against the content. In a 420px panel
   that is the difference between a list and a cramped list. The shared
   `.so-bw-body` rule already sets `scrollbar-gutter: stable`, so the track is
   reserved whether or not it is shown; this adds the clearance. */
.so-bw-floating__panel #so-booking-widget[data-template] .so-bw-body,
.so-bw-popup__dialog #so-booking-widget[data-template] .so-bw-body {
    padding-right: calc(var(--so-bw-gutter) + 6px);
}

/* ── 40.4 · Member benefits, at banner height ────────────────────────────
   The strip is authored with INLINE styles, so reaching it needs `!important`
   — the same thing section 28 does for Dusk, and the only reason this block
   looks heavy. At 12/16 padding with a 36px mark and a 22px percentage it
   stood ~76px tall at the top of a 420px panel: a tenth of the shell spent
   restating a discount that every price below it already reflects. Roughly
   halved, hierarchy intact — the label, the plan, the tier chip and the
   percentage all survive, smaller. */
.so-bw-floating__panel #so-booking-widget[data-template] .so-bw-member-banner,
.so-bw-popup__dialog #so-booking-widget[data-template] .so-bw-member-banner {
    padding: 8px 12px !important;
    margin-bottom: 10px !important;
    gap: 10px !important;
    box-shadow: none !important;
}
.so-bw-floating__panel #so-booking-widget[data-template] .so-bw-member-banner__mark,
.so-bw-popup__dialog #so-booking-widget[data-template] .so-bw-member-banner__mark {
    width: 28px !important;
    height: 28px !important;
}
.so-bw-floating__panel #so-booking-widget[data-template] .so-bw-member-banner__mark svg,
.so-bw-popup__dialog #so-booking-widget[data-template] .so-bw-member-banner__mark svg {
    width: 15px;
    height: 15px;
}
.so-bw-floating__panel #so-booking-widget[data-template] .so-bw-member-banner__label,
.so-bw-popup__dialog #so-booking-widget[data-template] .so-bw-member-banner__label {
    font-size: 9.5px !important;
    letter-spacing: .07em !important;
}
.so-bw-floating__panel #so-booking-widget[data-template] .so-bw-member-banner__plan,
.so-bw-popup__dialog #so-booking-widget[data-template] .so-bw-member-banner__plan {
    font-size: 12.5px !important;
}
.so-bw-floating__panel #so-booking-widget[data-template] .so-bw-member-banner__tier,
.so-bw-popup__dialog #so-booking-widget[data-template] .so-bw-member-banner__tier {
    font-size: 9px !important;
    padding: 1px 6px !important;
}
.so-bw-floating__panel #so-booking-widget[data-template] .so-bw-member-banner__off,
.so-bw-popup__dialog #so-booking-widget[data-template] .so-bw-member-banner__off {
    padding-left: 10px !important;
}
.so-bw-floating__panel #so-booking-widget[data-template] .so-bw-member-banner__pct,
.so-bw-popup__dialog #so-booking-widget[data-template] .so-bw-member-banner__pct {
    font-size: 16px !important;
}
.so-bw-floating__panel #so-booking-widget[data-template] .so-bw-member-banner__offlabel,
.so-bw-popup__dialog #so-booking-widget[data-template] .so-bw-member-banner__offlabel {
    font-size: 9px !important;
}

/* ── 40.5 · The team, laid out as people ─────────────────────────────────
   In a 420px panel the staff grid gives each tile ~165px, and the shared card
   is a horizontal avatar-then-text row that needs about 240px to read — so
   the name wrapped into a column two words wide beside the photo. Stacked,
   the same 165px is generous.

   `grid-auto-rows: auto` is the other half. The grid was sizing EVERY row to
   the tallest cell in it, and the full-width "Any Available" card is in row
   one — so one roomy escape hatch made every colleague below it as tall as
   itself. Rows now take their own content; `align-items: stretch` then makes
   the two tiles in a row finish together, and `margin-top: auto` on the
   profile button lines those up across the row whatever each bio did above. */
.so-bw-floating__panel #so-booking-widget[data-template] .so-bw-card-grid--staff,
.so-bw-popup__dialog #so-booking-widget[data-template] .so-bw-card-grid--staff {
    grid-auto-rows: auto;
    align-items: stretch;
}
.so-bw-floating__panel #so-booking-widget[data-template] .so-bw-card--staff:not(.so-bw-card--any),
.so-bw-popup__dialog #so-booking-widget[data-template] .so-bw-card--staff:not(.so-bw-card--any) {
    position: relative;
    flex-direction: column;
    align-items: center;
    gap: 6px;
    padding: 12px 10px;
    text-align: center;
}
.so-bw-floating__panel #so-booking-widget[data-template] .so-bw-card--staff:not(.so-bw-card--any) .so-bw-card-body,
.so-bw-popup__dialog #so-booking-widget[data-template] .so-bw-card--staff:not(.so-bw-card--any) .so-bw-card-body {
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    width: 100%;
    min-width: 0;
    flex: 1 1 auto;
}
/* One line of bio. It uses `.so-bw-card-meta` WITHOUT the `--clamp` modifier,
   so nothing clamps it and a two-sentence biography ran to three lines in a
   165px tile. All three properties are needed together — `-webkit-line-clamp`
   does nothing without `display: -webkit-box`. */
.so-bw-floating__panel #so-booking-widget[data-template] .so-bw-card--staff:not(.so-bw-card--any) .so-bw-card-meta,
.so-bw-popup__dialog #so-booking-widget[data-template] .so-bw-card--staff:not(.so-bw-card--any) .so-bw-card-meta {
    display: -webkit-box !important;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 1 !important;
    line-clamp: 1;
    overflow: hidden;
    margin-bottom: 0;
}
/* Two lines for a person: a full name is not a truncatable label, and
   "Maya Krishnamurthy-" is worse than a wrap. */
.so-bw-floating__panel #so-booking-widget[data-template] .so-bw-card--staff:not(.so-bw-card--any) .so-bw-card-title,
.so-bw-popup__dialog #so-booking-widget[data-template] .so-bw-card--staff:not(.so-bw-card--any) .so-bw-card-title {
    -webkit-line-clamp: 2 !important;
    line-clamp: 2;
    min-height: 0;
}
/* The tick renders on every tile whether chosen or not, so in a stacked card
   it held an empty 22px row plus its gap at the foot of all of them — the dead
   space under every button. A selection mark belongs on the card, not in its
   flow. */
.so-bw-floating__panel #so-booking-widget[data-template] .so-bw-card--staff:not(.so-bw-card--any) .so-bw-card-check,
.so-bw-popup__dialog #so-booking-widget[data-template] .so-bw-card--staff:not(.so-bw-card--any) .so-bw-card-check {
    position: absolute;
    top: 6px;
    right: 6px;
    z-index: 2;
}
/* "Any Available" keeps the horizontal row — it is an instruction, not a face. */
.so-bw-floating__panel #so-booking-widget[data-template] .so-bw-card--any,
.so-bw-popup__dialog #so-booking-widget[data-template] .so-bw-card--any {
    flex-direction: row;
    align-items: center;
    text-align: left;
    min-height: 0;
}

/* ── 40.6 · View profile is a control, not a footnote ────────────────────
   It ships as a ~10px text link, which in a narrow tile wraps to two lines
   ("View / profile") and reads as an accident. As the tile's own secondary
   action it gets a real target: the width of the card, a border, a 32px box,
   pushed to the foot so the row lines up. It stays quieter than the card it
   sits in — choosing the person is the primary act, reading about them is the
   aside. */
.so-bw-floating__panel #so-booking-widget[data-template] .so-bw-card-profile,
.so-bw-popup__dialog #so-booking-widget[data-template] .so-bw-card-profile {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 5px;
    width: 100%;
    min-height: 32px;
    margin-top: auto;
    padding: 6px 10px;
    border: 1px solid var(--so-bw-border, var(--so-bw-ds-n200));
    border-radius: 999px;
    background: var(--so-bw-card-bg, var(--so-bw-ds-n0));
    color: var(--so-bw-text, var(--so-bw-ds-n700));
    font-size: 11.5px;
    font-weight: 600;
    line-height: 1.2;
    white-space: nowrap;
}
.so-bw-floating__panel #so-booking-widget[data-template] .so-bw-card-profile:hover,
.so-bw-popup__dialog #so-booking-widget[data-template] .so-bw-card-profile:hover {
    border-color: var(--so-bw-primary);
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}

/* ── 40.7 · The list shows its first row and offers the rest ─────────────
   A twelve-item list that reads fine in a page column is a scroll marathon in
   a 420px bubble. Above three items the list cuts to its first row and the
   toggle reveals the rest; at three or fewer it is shown whole, because there
   the control costs more than it saves. `chatCanCollapse()` is the gate and
   `inShellEmbed` is what opens it here.

   Nothing is filtered in JS — the full list is still rendered, so search,
   keyboard navigation, selection, the running tally and auto-advance all see
   every item. Only the paint is cut, which is why expanding is free.

   The three tiers are mutually exclusive via `:not([data-cwmin~="…"])`:
   `data-cwmin` lists every breakpoint the container is at or above, so
   cumulative rules would have the 600 tier hiding the third card the 960 tier
   is trying to show. `:nth-child(… of S)` counts the CARDS — Alpine leaves
   its `<template x-for>` in the DOM and the staff grid opens with the
   full-width "Any Available" card, both of which plain `:nth-child` counts.
   Behind `@supports` because that form is newer than the rest of this file;
   without it the list simply renders whole, which is what shipped before. */
@supports selector(:nth-child(1 of .so-bw-card)) {
    .so-bw-floating__panel #so-booking-widget:not([data-cwmin~="600"])[data-template] .so-bw-chatcut
        > .so-bw-card:nth-child(n+2 of .so-bw-card:not(.so-bw-card--any)),
    .so-bw-popup__dialog #so-booking-widget:not([data-cwmin~="600"])[data-template] .so-bw-chatcut
        > .so-bw-card:nth-child(n+2 of .so-bw-card:not(.so-bw-card--any)) { display: none !important; }

    .so-bw-floating__panel #so-booking-widget[data-cwmin~="600"]:not([data-cwmin~="960"])[data-template] .so-bw-chatcut
        > .so-bw-card:nth-child(n+3 of .so-bw-card:not(.so-bw-card--any)),
    .so-bw-popup__dialog #so-booking-widget[data-cwmin~="600"]:not([data-cwmin~="960"])[data-template] .so-bw-chatcut
        > .so-bw-card:nth-child(n+3 of .so-bw-card:not(.so-bw-card--any)) { display: none !important; }

    .so-bw-floating__panel #so-booking-widget[data-cwmin~="960"][data-template] .so-bw-chatcut
        > .so-bw-card:nth-child(n+4 of .so-bw-card:not(.so-bw-card--any)),
    .so-bw-popup__dialog #so-booking-widget[data-cwmin~="960"][data-template] .so-bw-chatcut
        > .so-bw-card:nth-child(n+4 of .so-bw-card:not(.so-bw-card--any)) { display: none !important; }
}

/* The toggle is a DISCLOSURE, so it takes the size of one — centred, as wide
   as its label, and quieter than either real action. A full-width pill read as
   a third button competing with Back and Continue. 36px stays comfortably past
   the 24px target minimum while remaining visibly subordinate. */
.so-bw-floating__panel #so-booking-widget[data-template] .so-bw-chatmore,
.so-bw-popup__dialog #so-booking-widget[data-template] .so-bw-chatmore {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    width: auto;
    min-height: 36px;
    margin: 10px auto 0;
    padding: 6px 14px;
    border: 1px solid var(--so-bw-border, var(--so-bw-ds-n200));
    border-radius: 999px;
    background: transparent;
    color: var(--so-bw-text-secondary, var(--so-bw-ds-n500));
    font-family: inherit;
    font-size: 12px;
    font-weight: 600;
    cursor: pointer;
}
.so-bw-floating__panel #so-booking-widget[data-template] .so-bw-chatmore:hover,
.so-bw-popup__dialog #so-booking-widget[data-template] .so-bw-chatmore:hover {
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
    border-color: var(--so-bw-primary);
}
.so-bw-floating__panel #so-booking-widget[data-template] .so-bw-chatmore:focus-visible,
.so-bw-popup__dialog #so-booking-widget[data-template] .so-bw-chatmore:focus-visible {
    outline: 2px solid var(--so-bw-primary);
    outline-offset: 2px;
}
.so-bw-floating__panel #so-booking-widget[data-template] .so-bw-chatmore-chev--up,
.so-bw-popup__dialog #so-booking-widget[data-template] .so-bw-chatmore-chev--up {
    transform: rotate(180deg);
}

/* ── 40.8 · One primary action in the nav ────────────────────────────────
   Back and Continue shipped as two pills of equal weight, so the bar offered
   two equally-loud choices and the eye had to read them to find the commit.
   Back becomes a ghost — label plus chevron, no fill, no border — and
   Continue keeps the whole of the accent. Back also stops stretching, so
   Continue has the larger target as well as the stronger colour.

   `--so-bw-text-secondary` rather than a literal grey, so this reads
   correctly on the dark and tinted templates too — on Dusk the ghost is light
   text on charcoal, not grey on charcoal. */
.so-bw-floating__panel #so-booking-widget[data-template] .so-bw-nav .so-bw-btn--secondary,
.so-bw-popup__dialog #so-booking-widget[data-template] .so-bw-nav .so-bw-btn--secondary {
    flex: 0 0 auto;
    padding: 8px 14px;
    border-color: transparent;
    background: transparent;
    color: var(--so-bw-text-secondary, var(--so-bw-ds-n500));
    font-weight: 600;
    box-shadow: none;
}
.so-bw-floating__panel #so-booking-widget[data-template] .so-bw-nav .so-bw-btn--secondary:hover,
.so-bw-popup__dialog #so-booking-widget[data-template] .so-bw-nav .so-bw-btn--secondary:hover {
    background: var(--so-bw-bg-tertiary, var(--so-bw-ds-n50));
    color: var(--so-bw-text, var(--so-bw-ds-n900));
    border-color: transparent;
}

/* ── 40.9 · The header and the member strip are ONE top section ──────────
   Three separate things made the top of a floating widget read as a stack of
   cards rather than a single component:

     1 · the header carried its OWN corner radius (16px on nine templates,
         28px on Coral Pop) inside a panel clipped at 14px. Two different
         curves on the same corner is the "inconsistent rounding" — and where
         the header's radius was the LARGER of the two, the panel's white
         showed through the gap between them;
     2 · the member banner floated as a rounded, shadowed, inset card with a
         12px gap under the header, which is the stacked-card look itself;
     3 · on grid-based architectures the banner was not even adjacent to the
         header — auto-placement dropped it several rows down, so the "top
         section" was split by whatever landed between them (measured at 365px
         on Split, 426px on Tabbed).

   All three are answered here. The header gives up its radius and lets the
   PANEL clip the corner, so there is exactly one curve. The banner goes
   full-bleed, square, unshadowed and flush. And `order` pulls the two together
   at the top of the flow — it applies to flex and to auto-placed grid items
   alike, which is every container here except Dusk, whose named `member` grid
   area already places it directly under `head`.

   The result is one continuous band from the panel's top corners down to the
   body, whatever template is inside it. */
.so-bw-floating__panel #so-booking-widget[data-template][data-position] .so-bw-header,
.so-bw-popup__dialog #so-booking-widget[data-template][data-position] .so-bw-header {
    order: -2;
    border-radius: 0;
}
.so-bw-floating__panel #so-booking-widget[data-template] .so-bw-member-banner,
.so-bw-popup__dialog #so-booking-widget[data-template] .so-bw-member-banner {
    order: -1;
    margin: 0 !important;
    border-radius: 0 !important;
    /* The banner sits between two full-bleed bands, so it is one too. Any
       horizontal inset here reopens the gap the radius used to sit in. */
    width: auto !important;
    align-self: stretch;
    /* nowrap with a min-width:0 body: at 420px the three columns (mark, plan,
       percentage) wrapped onto a second line and the strip grew to 93px —
       taller than the header it is meant to continue. Unwrapped it holds one
       row at every width this shell reaches, and the plan name ellipsises
       rather than the strip growing. */
    flex-wrap: nowrap !important;
}
.so-bw-floating__panel #so-booking-widget[data-template] .so-bw-member-banner__body,
.so-bw-popup__dialog #so-booking-widget[data-template] .so-bw-member-banner__body {
    min-width: 0 !important;
    overflow: hidden;
}
.so-bw-floating__panel #so-booking-widget[data-template] .so-bw-member-banner__plan,
.so-bw-popup__dialog #so-booking-widget[data-template] .so-bw-member-banner__plan {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
/* The percentage must not be squeezed by a long plan name — it is the number
   the strip exists to state. */
.so-bw-floating__panel #so-booking-widget[data-template] .so-bw-member-banner__off,
.so-bw-popup__dialog #so-booking-widget[data-template] .so-bw-member-banner__off {
    flex: 0 0 auto;
}

/* ── 40.10 · The one deliberate exception, and the last insets ───────────
   FROST IS NOT A BUG. Section 33 gives `glass-frost` its own floating
   treatment on purpose: a 560px flex-column panel, near full height, with
   12px of padding on the widget root that IS the gradient field its whole
   proposition rests on ("frosted panels floating on a gradient"). Zeroing
   that padding as a double-container would delete the template's signature,
   and its panel is modal-like rather than bubble-like, so the 16–20px
   launcher gap is not a rule it should be held to either.

   So both are written `:not(...glass-frost)` rather than applied blindly. An
   audit that "fixes" a designed exception has broken something, not repaired
   it — this is the difference between the two, stated where a future reader
   will hit it. */
.so-bw-floating__panel:has(#so-booking-widget[data-template-id="glass-frost"]) {
    bottom: calc(100% + 12px);
}
.so-bw-floating__panel:has(> #so-booking-widget[data-template-id="glass-frost"]) > #so-booking-widget {
    padding: 12px !important;
}

/* The container's own inset. Most templates leave it at 0 and carry their
   padding on the header, body and nav — but Frost sets 4px and Inline caps
   the card at 380px, both of which show as a rim inside a 420px panel. In a
   shell the panel supplies the frame, so the card fills it. `!important` on
   max-width because section 23.1's sizing rule is the same (1,3,0) and this
   has to beat it for every template rather than tie. */
.so-bw-floating__panel:not(:has(#so-booking-widget[data-template-id="glass-frost"])) #so-booking-widget[data-template][data-position] .so-bw-container,
.so-bw-popup__dialog #so-booking-widget[data-template][data-position] .so-bw-container {
    padding: 0;
    max-width: 100% !important;
    width: 100%;
}
/* …except where the container's padding is not an INSET but the LAYOUT.
   Chat Booking's three-pane arrangement reserves the two rails' columns with
   `padding-left: 260px` / `padding-right: 320px` on the container and hangs
   both rails off it, absolutely positioned into that reserved space (section
   36.22). Zeroing it there does not tighten a rim — it deletes the layout:
   the rails come down on top of the conversation, and the header, which
   reaches back OVER them with matching negative margins so the band runs edge
   to edge, has nothing to reach back across and bleeds outside the card
   instead. The container is `overflow: clip`, so the overhang is cropped
   rather than merely untidy.

   MEASURED on the live page, a full-width popup at 1440: the header rendered
   -240..1740 inside a card at 20..1420 — 580px of chrome, the avatar and the
   thread title among it, cut off on both sides.

   Gated on the same `[data-cwmin~="960"]` the three-pane rules themselves use,
   so it can only fire when those rules are what is being repaired. Below that
   width Chat Booking is a single column, its container padding is 0 anyway,
   and the flush edges 40.10 exists for are untouched. Each branch mirrors its
   counterpart above (`:not(:has(...))` included) so the two tie on specificity
   and this one wins on order — the same pattern the popup guards use
   elsewhere in this section. */
.so-bw-floating__panel:not(:has(#so-booking-widget[data-template-id="glass-frost"])) #so-booking-widget:where([data-cwmin~="960"])[data-template="chatbook"][data-position] .so-bw-container,
.so-bw-popup__dialog #so-booking-widget:where([data-cwmin~="960"])[data-template="chatbook"][data-position] .so-bw-container {
    padding-left: var(--so-bw-chat-side);
    padding-right: var(--so-bw-chat-sum);
}

/* ── 40.11 · The grid architectures, in a 420px box ──────────────────────
   Agenda, Split, Tabbed and Accordion lay their container out as a GRID with the
   header explicitly placed (`grid-column: 1; grid-row: 1`). Explicit
   placement ignores `order`, so 40.9 could not pull the member strip up
   beneath the header — auto-placement dropped it several rows down and the
   "top section" was split by whatever landed between (375px on Split, 423px
   on Tabbed).

   In a 420px shell those grids have one column anyway: the second pane and
   the tab rail both collapse at this width by the templates' own
   narrow-width rules. Stating the single column as a flex
   column instead makes source order the visual order again, which is what
   puts the header and the strip together — and changes nothing else, because
   a one-column grid and a column flexbox lay out identically.

   Dusk is deliberately absent: it places the banner with a NAMED grid area
   (`"head" "member" …`) that already sits directly under the header, so it
   was correct before this and stays untouched. */
.so-bw-floating__panel #so-booking-widget[data-template="agenda"] .so-bw-container,
.so-bw-floating__panel #so-booking-widget[data-template="tabbed"] .so-bw-container,
.so-bw-popup__dialog #so-booking-widget[data-template="agenda"] .so-bw-container,
.so-bw-popup__dialog #so-booking-widget[data-template="tabbed"] .so-bw-container{
    display: flex;
    flex-direction: column;
}

/* ==========================================================================
   40 · DAYPART — THE SEARCH FIELD IS THE BOTTOM OF THE HEAD
   ==========================================================================

   Numbered 40 but placed BEFORE 38/39, and that is deliberate. Coral Pop's
   pair has to close the file — it overrides earlier sections at equal
   !important, so it only works from the end — and the suite enforces it
   ("no section may follow it", widget-ui-consistency). Appending here would
   have broken that invariant for no gain: every rule below wins on
   SPECIFICITY rather than source order (the lightest is (1,3,0) against the
   (1,2,0) sticky rule in section 23), so the block reads the same wherever it
   sits. Nothing in 38/39 can reach it either — those are keyed on
   `[data-template-id="coral-pop"]`, whose layout is "soft", and a widget is
   never both.

   Scoped to `[data-template="daypart"]`, which is daypart-teal alone — it is
   the only template on that layout key.

   Daypart's head was three bands stacked: the masthead, then the widget's
   hairline, then a 24px gap, then a sentence of prose, then the field. The
   field read as the first row of the LIST rather than the last row of the
   HEAD, and the customer met four lines of chrome before a single control.

   This joins the two. Section 23 already pins `.so-bw-search` to the top of
   the scrollport; what was missing is that it did not LOOK attached to
   anything. Three things make it one block with the header:

     • SAME SURFACE. The strip paints `--so-bw-ds-n0`, which is exactly what
       the daypart header rule at ~10405 paints. Two elements, one white band,
       no seam — the same reason it is written as the token and not as a hex.
     • NO GAP. The header closes on `padding-bottom: 0` and the step's top
       padding is dropped, so the strip's own 14px is the ONLY space between
       the stepper and the field. That distance then reads the same whether
       the list is scrolled or sitting at the top.
     • ONE EDGE, AT THE BOTTOM. The hairline moves off the header and onto the
       strip. Left on the header it would cut between the stepper and the
       field and make the field the top of the list again, which is the exact
       thing being fixed. Colour, not width: `border-bottom-color:
       transparent` keeps the header's 1px in the box model, so nothing below
       shifts by a pixel when the gate flips.

   GATED ON THE FIELD ACTUALLY BEING THERE. `.so-bw-search` is rendered on the
   service, staff and location steps and revealed by x-show only once the list
   is long enough to need it (>5 services, >2 staff, >2 locations). Alpine
   hides it with an inline `display: none`, so `:not([style*="display: none"])`
   is what distinguishes rendered-and-shown from rendered-and-hidden — the same
   idiom sections 39 uses for the filter chips. Without the gate a step with no
   field would lose the header's closing line and its top padding and have
   nothing to put back in their place. */
#so-booking-widget[data-template="daypart"]:has(.so-bw-step:not([style*="display: none"]) > .so-bw-search:not([style*="display: none"])) .so-bw-header {
    padding-bottom: 0;
    border-bottom-color: transparent;
}
/* The step's 24px top padding is what held the field away from the header.
   Dropped only for the step that HAS a visible field — the strip's own padding
   does that spacing instead, and it is padding the strip paints. Side and
   bottom padding are untouched. */
#so-booking-widget[data-template="daypart"][data-template] .so-bw-step:has(> .so-bw-search:not([style*="display: none"])) {
    padding-top: 0;
}
/* FULL-BLEED. The strip is pulled out over the step's side padding and given
   it back as its own, so the white band runs edge to edge like the header
   above it. Capped to the content box it would span only the middle, and cards
   would travel visibly through the two channels beside the field — which reads
   as a transparent search bar, because you are watching content move at the
   same height as the bar.

   `--so-bw-gutter` and not a literal: that is the token `.so-bw-step` pads
   with, so the field's left edge lands on the CARDS it filters and follows
   them when the tiers tighten it to 20px and 16px.

   Which is the alignment to prefer where the two disagree. Above 480px of
   container width they do not: `--so-bw-header-pad-x` is `var(--so-bw-gutter)`
   too, so field, cards and brand title share one spine. Below it the mobile
   tier at ~4171 pins the header to a literal 22px while the gutter goes to
   16px — a 6px stair-step that predates this section and applies to every
   template. Matching the header there would buy a straight edge at the top of
   the strip and cost the field its alignment with the list underneath it,
   which is the pairing the customer is actually reading. */
#so-booking-widget[data-template="daypart"][data-template] .so-bw-search {
    margin: 0 calc(var(--so-bw-gutter) * -1) 16px;
    padding: 14px var(--so-bw-gutter);
    /* `background-color`, not the `background` shorthand — the shorthand also
       resets background-image, which would silently drop a gradient a Studio
       theme had set on this element rather than layering over it. */
    background-color: var(--so-bw-ds-n0);
    border-bottom: 1px solid var(--so-bw-header-border);
}
/* The icon and the clear button are absolutely positioned against
   `.so-bw-search`, and that box now starts a full gutter OUTSIDE the input. A
   bare `left: 13px` therefore lands 11px to the LEFT of the field's own edge,
   stranding the glyph on the strip. Add the inset back so both sit where they
   did relative to the control they belong to. */
#so-booking-widget[data-template="daypart"][data-template] .so-bw-search-icon {
    left: calc(var(--so-bw-gutter) + 13px);
}
#so-booking-widget[data-template="daypart"][data-template] .so-bw-search-clear {
    right: calc(var(--so-bw-gutter) + 8px);
}
/* THE BLURB ABOVE THE FIELD GOES — "Select one or more services. They'll be
   booked back-to-back in the order you pick them.", and its equivalents on the
   staff and location steps.

   Scoped by ADJACENCY rather than by step: only the `<p>` immediately above a
   visible search field is removed, so "Pick a date and time for your
   appointment." and the rest survive on the steps that carry no field. It is
   the sentence that was wedged between the header and the search bar that this
   is about, not the pattern of describing a step.

   `display: none` and not a markup change: the sentence is real information on
   the other templates, and this is a per-template chrome decision — the same
   call section 39 makes for Coral Pop, for the same reason. Nothing is
   stranded by hiding it: it is a static <p>, not a control.

   WHAT IS LOST, stated rather than glossed: the service blurb is the only
   place the widget promises that multiple services run back-to-back IN PICK
   ORDER. `.so-bw-multi-summary` appears once two are chosen but carries a
   count, a combined duration and a price — not the sequence. On this template
   that ordering is discoverable from the review step instead. */
#so-booking-widget[data-template="daypart"][data-template] .so-bw-step-desc:has(+ .so-bw-search:not([style*="display: none"])) {
    display: none;
}


/* ==========================================================================
   38 · CORAL POP SCROLLS AS ONE BLOCK — inside the pane, not the page
   ==========================================================================

   Coral Pop KEEPS section 23's bounded shell. The container stays capped at
   `min(760px, calc(100dvh - 112px))`, `.so-bw-body` stays the single scroll
   region, and the page outside the widget still does not move. That is the
   height the design is drawn for and nothing here changes it.

   What changes is what happens INSIDE that region. Section 23 pins the search
   field there, and earlier sections pin three more elements. Coral Pop is the
   soft register — one full-width card per row, 22px radii, generous padding —
   and at that scale each of those detaching at its own moment reads as several
   loose pieces sliding over one another rather than one design.

   So the pane gets ONE fixed head and one moving part. The header and the
   search strip hold still together — the header already does, being outside
   the scroll region, and the strip is pinned to join it. Everything else
   travels with the cards. The commit bar sits below, outside the region, and
   never moved to begin with.

   SPECIFICITY. Every rule in section 23 is `#id[attr] .class` + !important.
   These match that weight and win on source order, being later in the file. The
   doubled `[data-template-id]` is the same trick the Coral Pop search rule at
   ~11887 uses: it lifts the selector one attribute above the section-23 rules
   so the outcome does not rest on source order alone.
   ========================================================================== */

/* ── 1 · The chrome holds still; only the cards move ──────────────────────
   The header is a SIBLING of `.so-bw-body`, so it sits outside the scroll
   region and is fixed by construction — nothing here is needed for it.

   The search strip is not: it lives inside `.so-bw-step`, inside the scroll
   region. It used to join the header by being `position: sticky; top: 0`, and
   that is what this rule now replaces.

   WHY STICKY WAS THE WRONG MECHANISM. A sticky element is still IN the
   scrolling flow — it is scrolled up to its offset and then held, and the
   content it was sitting above keeps travelling underneath it. So the strip
   had to be an opaque full-bleed mask (~11887, z-index 20, negative margins)
   purely to hide the cards passing behind it, and any gap in that mask showed
   as a see-through search bar. That fault has been fixed twice — see commit
   f1db6ff and the note at the 520px cap in section 39 — because the mask is
   load-bearing chrome that exists only to service the pinning.

   THE STRIP IS NOW OUT OF THE SCROLL, not held against it. Part 3 below moves
   the scroller down one level, from `.so-bw-body` to the list wrapper inside
   the step. The strip sits above that wrapper, in the part of the step that no
   longer scrolls, so it holds still because nothing can move it — not because
   it is pinned against something moving. Cards cannot pass behind it: they are
   inside a box that ends where the strip begins.

   `relative`, NOT `static`, and this is the trap part 2 documents at length:
   the magnifier and the clear "×" are `position: absolute` against this
   element (~7435). Dropping the containing block does not merely unpin the
   strip, it lets those children escape to the nearest positioned ancestor —
   the icon leaves the field and lands against the widget shell. */
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-search {
    position: relative;
    top: auto;
    flex: 0 0 auto;
}
/* The row that travels with the strip rather than with the cards: the filter
   chips it discloses. A chip row that scrolled away from its own button would
   leave the button reading "on" with nothing visible to turn off. */
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-filter-chips {
    flex: 0 0 auto;
}

/* ── 2 · The rest of the scroll region does not pin ───────────────────────
   These three are `position: sticky` from earlier sections and are the parts
   that read as separate pieces sliding over one another. They travel with the
   cards instead.

   `relative`, NOT `static`. Going static was a real bug: `.so-bw-search` and
   these each act as the containing block for absolutely-positioned children —
   the magnifier and the clear button are `position: absolute` against the
   search strip (~7435) — and removing the containing block does not merely
   unstick the parent, it lets those children escape to the nearest positioned
   ancestor. The icon left the field and landed against the widget shell.
   `relative` unpins the box while keeping it an anchor for its own children.

   The nav is listed for completeness rather than to move it: it is a sibling
   of `.so-bw-body`, so the bounded flex column already parks it at the foot of
   the card. */
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-nav,
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-multi-summary,
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-slot-panel-head {
    position: relative;
}

/* Step top padding is zeroed at ~11868 so the pinned field does not arrive
   under an empty band — the strip's own 10px does that spacing instead. With
   the field pinned again that reasoning holds, so the 12px this section added
   while it was unpinned is withdrawn. */
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-step {
    padding-top: 0;
}

/* ── 3 · THE SCROLLER MOVES DOWN ONE LEVEL — on the two picking steps ─────
   Section 23 made `.so-bw-body` the single scroll region and released the four
   list ceilings so nothing could compete with it. That is still the rule for
   this template on every step BUT the two that carry a search strip.

   THE POINT IS THAT THE STRIP STOPS BEING PART OF WHAT SCROLLS. Sticky held it
   still while the list ran underneath it; this holds it still by putting the
   scroll boundary below it. The step becomes a column — strip and chips at the
   top at their natural height, the list taking whatever is left and scrolling
   inside itself — so the fixed head is fixed by layout, and the opaque mask the
   sticky version needed is no longer doing any work.

   STILL ONE SCROLLBAR, WHICH IS THE CONSTRAINT SECTION 23 ACTUALLY CARES
   ABOUT. The list is sized to exactly the space the step has left, so the body
   around it has nothing to overflow and never grows a second bar. What section
   23 forbids is a list with a FIXED ceiling inside a scrolling body — a short
   box scrolling within a taller one — and `max-height: none` is kept below to
   keep that ceiling off. This is the opposite arrangement: the inner box is
   what the pane's height resolves to.

   GATED WITH `:has()`, and the gate is the safety property. Only a step that
   CONTAINS a scroller may be squashed to the pane height: `min-height: 0` on a
   step without one would crop it to the pane with no way to reach the rest,
   which on the customer-details step means a form whose last fields cannot be
   scrolled to. Steps that do not match keep block layout and overflow the body
   exactly as they do today.

   `display: flex` carries no !important ON PURPOSE. x-show hides an inactive
   step with an inline `display: none`, and inline styles beat a stylesheet
   rule but not an !important one — so an !important here would render all nine
   steps at once. */
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-body {
    display: flex;
    flex-direction: column;
}
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-step:has(.so-bw-scroll-wrap),
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-step:has(.so-bw-card-grid--scroll) {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-height: 0;
}
/* The list itself. `min-height: 0` is what lets it be shorter than its content
   — without it a flex item refuses to shrink past its intrinsic height and the
   overflow reappears on the body, which is the arrangement this replaces.

   FULL-BLEED, so the scrollbar rides the pane's edge rather than floating in a
   channel 28px inboard of it, and so a card's hover shadow is not clipped by
   the scroll box. The body's side padding is pulled back off and re-applied
   inside, which keeps the cards on the same left edge as the header and the
   strip — the spine section 39 aligns everything to. */
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-scroll-wrap,
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-card-grid--scroll {
    flex: 1 1 auto;
    max-height: none !important;
    min-height: 0 !important;
    overflow-y: auto !important;
    /* `overflow-y: auto` computes overflow-x to auto as well, and a stray pixel
       of width would then hang a horizontal bar under the list. */
    overflow-x: hidden;
    margin-left: calc(var(--so-bw-cp-pad-x) * -1);
    margin-right: calc(var(--so-bw-cp-pad-x) * -1);
    padding-left: var(--so-bw-cp-pad-x);
    padding-right: var(--so-bw-cp-pad-x);
    /* Clearance under the head's closing line. Inside the scroll box on
       purpose: it holds the first row off the line at rest and then scrolls
       away with the content, so a scrolled list runs right up to the line
       instead of leaving a white channel under it. A margin here would be
       outside the box and would hold that channel open permanently.

       Off `--so-bw-cp-d-base`, NOT `--so-bw-cp-d`. This is pane spacing, not
       tile spacing, and one of the two selectors above is itself a
       `.so-bw-card-grid` — so the tile multiplier from section 1e applies to it
       and not to the other, and the two lists would sit differently under an
       identical head. */
    padding-top: calc(14px * var(--so-bw-cp-d-base));
}
/* The time-slot list is NOT moved. Its step has no search strip to free, so
   there is nothing to gain, and section 23's reasoning applies to it
   unchanged: it stays released and travels with the body. */
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-slot-scroll {
    max-height: none !important;
    min-height: 0 !important;
    overflow-y: visible !important;
}

/* ==========================================================================
   41 · TABBED RAIL TAKES THE WHOLE VIEWPORT HEIGHT
   --------------------------------------------------------------------------
   Placed before section 39 for the reason section 40 states — the cascade
   suite requires Coral Pop to be the file's last section.

   REQUESTED: the shell must be 100vh. Section 30 already says exactly that
   (`height: calc(100dvh - var(--so-bw-vchrome, 0px))`), and MEASURED it was not
   happening: 828px in a 900px window, on both the public page and the Studio's
   standalone preview. 828 = 900 - 72, and the 72 is section 34.1:

     --so-bw-desk-inset: 32px    the root's 12px top padding + page gutter
     --so-bw-desk-sep:   40px    the root's bottom margin
     --so-bw-desk-ceil:  min(860px, calc(100dvh - inset - sep))
     max-height: var(--so-bw-desk-ceil) !important

   That rule scores (1,4,0) WITH `!important`, so it outranks section 30's
   (1,2,0) `!important` regardless of order — section 30 was setting a height the
   ceiling then clipped. This is why the earlier "height is now 100vh" report was
   wrong: I verified the declaration and not the computed value.

   So this restates the TOKENS rather than fighting the declaration. 34.1's own
   comment invites exactly that ("read through tokens rather than written inline,
   so 34.2 and 34.3 restate one value per template"), and 34.2 lists the
   templates that do it. Redefining the tokens on the same element makes 34.1's
   `min-height`/`max-height` resolve to 100dvh with nothing overridden.

   BOTH FLOOR AND CEILING, pinned to the same value. A floor below the ceiling
   would let the shell settle shorter on a step with little content — which is
   the behaviour every other template wants and the opposite of what a
   standing two-pane workspace wants, since the rail is only a rail if it is a
   full column.

   AND THE ROOT'S OWN CHROME GOES WITH IT, which is the half that keeps this
   honest. Zeroing the allowance without removing what it allowed for would put
   the card's last 72px below the fold and hand the page a scrollbar — the exact
   failure 34.1's two tokens exist to prevent. The 12px top padding and the 40px
   bottom separator are dropped here so the arithmetic stays true: nothing is
   above or below the card to make room for.

   Desktop only (`data-cwmin~="960"`), matching 34.1. Below that tier the rail
   collapses under the pane as a strip and the shell is content-driven — see
   section 30's "what is deliberately left alone". */
#so-booking-widget[data-cwmin~="960"][data-template="tabbed"][data-template-id] .so-bw-container {
    --so-bw-desk-inset: 0px;
    --so-bw-desk-sep: 0px;
    --so-bw-desk-floor: 100dvh;
    --so-bw-desk-ceil: 100dvh;
}
/* The chrome the tokens above stopped reserving. Vertical only — the 24px side
   padding is this template's page gutter and has nothing to do with height. */
#so-booking-widget:where([data-cwmin~="960"])[data-template="tabbed"] {
    padding-top: 0;
    padding-bottom: 0;
    margin-bottom: 0;
}

/* ==========================================================================
   40 · FULL WIDTH MEANS 100vw, WITH OR WITHOUT THE THEME'S HELP
   --------------------------------------------------------------------------
   Numbered 40 but placed BEFORE section 39, deliberately: the cascade suite
   asserts that Coral Pop's redesign is the LAST section in this file, and that
   invariant exists so its scoped overrides cannot be silently outranked by
   something appended later. Appending here broke it (caught by the suite), so
   this sits above it. Nothing in this section is template-scoped, so source
   order against 39 does not matter — the two never touch the same property.

   REQUESTED directly and repeatedly: at Widget Size = Full Width the widget
   must occupy 100vw. This states that as a width rather than inferring it.

   WHY IT IS NEEDED WHEN THE MEASUREMENTS SAID IT ALREADY WORKED. Everything
   before this depended on the THEME cooperating. The chain was: PHP emits
   `class="alignfull"`, the block theme's own rule
   `.has-global-padding > .alignfull { margin-inline: calc(pad * -1) }` cancels
   its gutter, and `max-width: none` here lets the root take the width that
   leaves. On Twenty Twenty-Four that resolves to the full window and measures
   1600 of 1600 — which is why every probe I ran came back full-bleed.

   But every link in it is the theme's. A CLASSIC theme has no `.alignfull`
   styles at all, so the class is inert and `max-width: none` resolves to the
   parent's content box — whatever column, padding or wrapper that page puts
   the shortcode in. Same for a block theme whose content wrapper is not
   `.has-global-padding`, a page builder's row, or any hand-built template with
   a fixed-width container. In all of those, Full Width silently means "as wide
   as the slot", and nothing in this stylesheet was asserting otherwise.

   HOW THE BLEED WORKS. `50%` is half the PARENT's width; `50vw` is half the
   viewport. Their difference is exactly the inset on one side, whatever
   produced it — padding, margin, a max-width column, or all three — so the
   negative margins cancel it without this rule needing to know which. Paired
   with `width: 100vw` the box then spans the window from either side.

   `width`, not `max-width`: the existing `.alignfull[data-size="full"]` rule
   sets `max-width: none` at (1,2,0) and would outrank a `max-width` here at
   (1,1,0). They are different properties, so they compose instead of
   competing — `width: 100vw` under `max-width: none` resolves to 100vw.

   SCOPED TO INLINE. A popup or floating widget is positioned by its own shell
   CSS; a 100vw box with negative auto-cancelling margins inside a fixed overlay
   is not a full-width widget, it is a broken one.

   THE SCROLLBAR, which is the one real cost and is handled rather than
   ignored: `100vw` counts the vertical scrollbar's width, so on a page that
   scrolls, a 100vw child overflows by ~15px and the browser adds a HORIZONTAL
   scrollbar. This template is now viewport-height by request, so the page
   nearly always scrolls. The guard below clips that overflow at the document.

   `overflow-x: clip`, NOT `hidden`, and the distinction is load-bearing:
   `hidden` on the root element makes it a scroll container, which breaks
   `position: sticky` everywhere in the theme (its header, its admin bar
   offsets) and is a classic way to wreck a site from a plugin stylesheet.
   `clip` clips without creating a scroll container, so nothing else changes.

   `:has()` keeps it conditional on a full-width widget actually being present,
   so no page without one is touched. In an engine without `:has()` the rule is
   simply dropped and the only consequence is the ~15px horizontal scrollbar —
   the widget is still 100vw. */
/* `!important` on all four, and it is earned rather than reached for. MEASURED
   the version without it against a page whose theme does NOT honour alignfull
   (class removed, parent a 760px column with 40px padding — a classic theme or
   a page-builder row): the widget rendered 645px, not 100vw. WordPress's own
   constrained-layout rule is why —

     .is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)) {
         max-width: var(--wp--style--global--content-size);
         margin-left: auto !important;
         margin-right: auto !important;
     }

   — so its `!important` auto margins cancel the bleed, and its `max-width`
   (645px here) caps a plain `width: 100vw`, since max-width always wins over
   width. Nothing at any specificity beats `!important` except `!important`.

   This is the one declaration in the file where that is unambiguously right:
   the admin has explicitly chosen Full Width, there is no narrower intent for
   it to trample, and the alternative is a control that silently does nothing on
   an unknown share of themes. `max-width` is restated here for the same reason
   — without it the theme's cap survives and the width never lands. */
#so-booking-widget[data-position="inline"][data-size="full"] {
    width: 100vw !important;
    max-width: 100vw !important;
    margin-left: calc(50% - 50vw) !important;
    margin-right: calc(50% - 50vw) !important;
}
html:has(#so-booking-widget[data-position="inline"][data-size="full"]) {
    overflow-x: clip;
}
/* The container follows the root rather than re-deriving the width: it is
   already `max-width: 100%` at this size (section 23), so this only removes the
   rounding and the side borders that would otherwise draw a seam a few pixels
   in from the window edge — the same call the phone tier already makes when the
   widget goes edge to edge. */
#so-booking-widget[data-position="inline"][data-size="full"] .so-bw-container {
    /* `width: 100%` is not redundant beside `max-width: 100%` — it is the fix
       for a real bug, and the bug only appears when the ROOT is a flex column.
       MEASURED in the Studio's expanded tab preview at 1600px: max-width
       resolved to 100% and the container still laid out at 962.8px with
       `margin-left: 318.578px` on both sides.
       `#so-booking-widget .so-bw-container { margin-inline: auto }` is why. In
       flex layout, AUTO CROSS-AXIS MARGINS SUPPRESS STRETCHING: the item is
       sized to its content and the free space goes to the margins, so a
       `max-width` of 100% never becomes a width of 100%. The expanded preview
       sets `body > #so-booking-widget { display: flex; flex-direction: column }`
       (see expandedPreviewStyles in BookingShortcode.php), which is what put the
       widget in that context — on a normal page the root is a block and the same
       auto margins simply centre a full-width box, harmlessly.
       Stating the width makes the free space zero, so the auto margins resolve
       to zero and the box stretches — correct in a block parent and in a flex
       one, without either needing to know about the other. Not fixed in the
       preview's PHP on purpose: any page builder or theme that wraps the
       shortcode in a flex column hits the identical bug on a customer's site. */
    width: 100%;
    border-left: 0;
    border-right: 0;
    border-radius: 0;
}

/* -- C12 . The last 12px of page scroll ----------------------------------
   Every non-Full size left the page scrollable by exactly 12px - measured at
   1600x900, 1280x720, 1024x768 and 1366x768, the same 12 every time, which is
   what says "a constant is short" rather than "a layout is wrong".

   Section 34's shared `--so-bw-desk-inset: 32px` is described as the root's 12px
   top padding PLUS the page gutter. On the embed route the body gutter alone is
   32px (16 top + 16 bottom), so the root's 12px is not in the number at all, and
   the card is sized 12px taller than the space it has.

   Corrected for Wide Canvas only, by adding the missing 12 to the inset rather
   than by capping the card somewhere else: the mechanism stays section 34's and
   the arithmetic becomes true. The same undercount applies to the other twenty-one
   architectures on this route and is worth a look at its own rule - not fixed here,
   because that number is shared and this change is not.

   Specificity matches section 34's (1,4,0) and sits later, the same requirement
   the Full Width rule above documents. */
#so-booking-widget[data-cwmin~="960"][data-template="wide"][data-template-id] .so-bw-container {
    --so-bw-desk-inset: 44px;
}

/* ==========================================================================
   39 · CORAL POP · THE REDESIGN
   ==========================================================================

   Scoped ENTIRELY to `[data-template-id="coral-pop"]`. Coral Pop shares its
   layout key ("soft") with other templates, so keying on the id is what keeps
   every rule below off them.

   THE REGISTER INVERTS. Coral Pop used to paint the SHELL blush and leave the
   cards white (~11815). That reads as a tinted box holding white boxes. The
   design this section implements does the opposite: the shell goes white and
   the coral is spent only where it means something — the active step, the
   selected card, the primary action. One accent, three placements, and the
   rest of the surface stays out of the way.

   COLOUR COMES FROM THE TOKEN, NOT FROM A LITERAL. Every coral below is
   `var(--so-bw-primary)`, every surface and ink a token with the design's
   value as its FALLBACK. Studio → Appearance can recolour this template, and a
   hardcoded hex would silently ignore that — leaving a recoloured widget with
   one stubbornly coral button. The blush tints are `color-mix` against the
   same token so they follow it too. The suite enforces this: see the
   "carries no hardcoded palette" test in staff-profile-popup.

   AND NEITHER DOES THE CORNER. Every radius here is a step on the ladder
   (`--so-bw-r-*`, declared on the widget root), not a px literal, for the same
   reason the colours are tokens: Studio → Appearance has a Border Radius
   slider, and a bare `border-radius: 16px` is a corner that stopped answering
   to it. The section was written before the ladder existed and carried ten
   such literals; they are 10/12/14/16 on the content scale and 24 on the shell
   scale, which is where the shell steps (18–28) live — the container, header,
   nav and footer take the shell one so the outer card rounds as one object.
   The suite enforces this too: "anchors the scale on the ladder, not on raw
   px" in widget-studio-settings-wiring fails on any literal that is not a
   999px pill or a 50% disc, both of which are shapes rather than corners.

   WHAT THIS SECTION CANNOT REACH. Four parts of the design need markup the
   widget does not emit, and no amount of CSS substitutes for them:
     • per-step ICONS — `.so-bw-stepbar-dot` renders a numeral, not a glyph
     • the Filter button beside the search field — no such element
     • the "Select" / "Selected" labels — only an icon circle exists
     • "Secure Booking" in the footer — `.so-bw-powered` has no such text
   They are listed here so the gap is recorded rather than rediscovered. What
   IS here is the whole visual language around them.
   ========================================================================== */

/* ── 1 · THE SHELL GOES WHITE ─────────────────────────────────────────────
   The blush moves off the surfaces and onto the accents. Kept as a variable
   so the three tints below cannot drift apart from each other. */
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-container {
    --so-bw-cp-tint: color-mix(in srgb, var(--so-bw-primary) 7%, var(--so-bw-card-bg, #ffffff));
    --so-bw-cp-tint-strong: color-mix(in srgb, var(--so-bw-primary) 12%, var(--so-bw-card-bg, #ffffff));
    --so-bw-cp-accent-grad: linear-gradient(135deg,
        color-mix(in srgb, var(--so-bw-primary) 82%, var(--so-bw-card-bg, #ffffff)),
        var(--so-bw-primary));
    border-radius: var(--so-bw-r-24);
    /* THE ELEVATION IS A DEFAULT, NOT A DECISION — same principle as the
       colours above and the corners beside them. This used to be a literal
       `box-shadow` here, which is a (1,3,0) declaration and beat the shell rule
       that reads `--so-bw-shadow-md`, so Studio → Appearance → Shadow could
       never reach this template whatever an admin picked.

       Seating the design value on `--so-bw-ds-shadow` instead puts it in the
       chain rather than across it: the design-system block maps
       `--so-bw-shadow-md: var(--so-bw-ds-shadow)` on this same element, so an
       untouched control still paints exactly this, and a set control overrides
       the token one level up and is honoured. The soft double-shadow — a hair
       of contact plus a wide, low-opacity lift — is what gives this template
       its floating-card register, and it survives as the default. */
    --so-bw-ds-shadow: 0 1px 3px rgba(24, 20, 30, 0.04), 0 12px 40px rgba(24, 20, 30, 0.08);
}

/* ── 1b · THE PANE IS TALL ENOUGH TO SHOW A WHOLE CARD ────────────────────
   Section 23 caps every template at `min(760px, calc(100dvh - 112px))`. That
   was set before this template's cards grew: Coral Pop's chrome runs ~390px
   (masthead ~200 with the icon stepper, search 68, commit bar 76, footer 46)
   and a row is up to ~136px, so 760 left a hair under three rows and the last
   one was always cut mid-card. A list whose bottom row is permanently sliced
   reads as broken rather than as scrollable.

   The height now follows the Studio "Widget Size" radio. Size sets the WIDTH
   (~7395), and width changes the row height with it — a 660px card wraps its
   description onto two lines where a 1000px one does not — so a single height
   cannot serve all four.

   THE RAMP IS RAISED BY 120px ACROSS THE BOARD, and the reason is section 38
   rather than a change of taste. The pane's head is now FIXED — masthead,
   stepper, search strip and chip row all sit outside the scroll — so the only
   thing the height buys is rows of cards. Every pixel of pane below the head
   goes to the list, where before some of it went to scrolling the head out of
   the way. A short pane spends most of itself on chrome and leaves a two-row
   porthole; the same chrome against a taller pane is a head over a list.

   These are not fitted to a whole number of rows any more, and that fitting is
   not recoverable: section 1c scales the tile with the size control and
   section 1e shrinks it again, so the row height moves with two settings and a
   height chosen to clear exactly three rows at one of them slices the third at
   another. The scroller handles a partial last row — the head's closing line
   is a visible edge for it to run to, which is what the fitting was standing
   in for.

   THE VIEWPORT ARM STAYS AS THE SECOND HALF OF EVERY min(), and it is what
   actually decides the height on most screens: 980px only binds above a
   ~1090px-tall display, so below that the pane is whatever the viewport
   leaves. Raising the first arm alone lets the widget use more of a TALL
   screen and does nothing at all on a laptop — which is why the reserve below
   matters more than the ramp above.

   THE EMBED ROUTE IS NOT AFFECTED by any of this. It pins
   `--so-bw-vph-reserve` to 112px with !important in its own inline stylesheet,
   which no rule here can outrank, so the no-second-scrollbar guarantee section
   23 documents survives.

   THE RESERVE IS 72px, NOT SECTION 23's 112. That 112 is the embed route's
   chrome — a ~56px identity banner, its 14px margin and a 16px body gutter —
   and none of it is on a customer's own page, where the widget sits in the
   theme's flow. What IS on every page is the widget root's own box: 32px of
   page gutter and top padding, plus the 40px `margin-bottom` section 2 puts on
   the root to separate it from whatever follows. 72 is those two, and it is
   section 34's measured number rather than a guess — this used to say 32,
   which ignored the bottom margin and asked the card to overhang it.

   AND WHEN NOTHING FOLLOWS, 34.5 HANDS THE 40 BACK. That rule zeroes both the
   margin and `--so-bw-desk-sep` on a widget that is `:last-child`, which is
   the case this reserve was originally trying to serve by guessing — solved
   there properly, by asking the DOM instead of assuming.

   SO THE RESERVE IS COMPOSED FROM 34's TWO TOKENS RATHER THAN RESTATING THEIR
   SUM, and that is what keeps this rule and 34.1 from disagreeing. Both are
   live at desktop width: 34.1 sets `min-height: var(--so-bw-desk-floor)` and
   this sets `max-height`. A flat 72 here would have gone on subtracting the
   separator at the exact moment 34.5 zeroed it, leaving a floor of
   `100dvh - 32` under a ceiling of `100dvh - 40` — a min-height ABOVE its own
   max-height. CSS resolves that by letting min-height win, so the rendered
   height happened to come out right, by the one rule overriding the other
   rather than by the two agreeing. Reading the same tokens, they agree.

   The fallbacks are what make it work below the 960 tier, where 34.1 never
   runs and neither token is declared: 32 + 40 is the same 72, stated as its
   two parts so it cannot drift from the model it came from. */
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-container {
    --so-bw-cp-maxh: 980px;
    --so-bw-vph-reserve: calc(var(--so-bw-desk-inset, 32px) + var(--so-bw-desk-sep, 40px));
}
/* THE HEIGHT ITSELF IS INLINE-ONLY, and the gate is not tidiness.
   `100dvh` means "the screen", which is the right measure for a card sitting
   in a page and the wrong one for a card sitting in a shell. A popup's
   container lives inside `.so-bw-popup__dialog` and a floating one inside
   `.so-bw-floating__panel`; both shells size themselves (90vh / 80vh) and then
   size the card to fill them — `.so-bw-floating__panel:has(> #so-booking-
   widget[data-template-id]) .so-bw-container { height: 100%; max-height: 100%
   !important }` at 35.3.

   That rule is (1,3,0) — a class, plus `:has()` scoring its most specific
   argument, plus a class — which is exactly what the ungated version of this
   rule scored, and this section is later in the file. So Coral Pop was winning
   it, and the 980px arm binds as soon as the shell is taller than 980: above a
   ~1225px viewport for the floating panel, ~1089px for the popup. The card
   stops short and a strip of shell shows underneath it — the precise fault
   section 35 was written to remove, reintroduced on one template.

   Gating on the position rather than raising the specificity is what the rest
   of the file does with "fill the available space" rules (see the note at the
   full-width gutter, ~14137, and the frame preview's own `[data-position=
   "inline"]` scope). Inside a shell this template now takes the shell's
   sizing, like the other twenty-two. */
#so-booking-widget[data-position="inline"][data-template-id="coral-pop"][data-template-id] .so-bw-container {
    max-height: min(var(--so-bw-cp-maxh), calc(100dvh - var(--so-bw-vph-reserve))) !important;
}
#so-booking-widget[data-size="small"][data-template-id="coral-pop"] .so-bw-container {
    --so-bw-cp-maxh: 880px;
}
#so-booking-widget[data-size="medium"][data-template-id="coral-pop"] .so-bw-container {
    --so-bw-cp-maxh: 980px;
}
#so-booking-widget[data-size="large"][data-template-id="coral-pop"] .so-bw-container {
    --so-bw-cp-maxh: 1040px;
}
#so-booking-widget[data-size="full"][data-template-id="coral-pop"] .so-bw-container {
    --so-bw-cp-maxh: 1100px;
}

/* ── 1b-ii · AND THE SAME RAMP AT DESKTOP WIDTH, WHERE 34.1 OWNS THE HEIGHT ─
   THE BUG THIS FIXES: everything above did nothing at all on a desktop.

   Section 34.1 gives every template a shared desktop band —
   `min-height: var(--so-bw-desk-floor); max-height: var(--so-bw-desk-ceil)
   !important` at `[data-cwmin~="960"][data-template][data-template-id]`, which
   is (1,4,0). The rule above is (1,3,0) — one attribute short. Both carry
   `!important`, so specificity decides and 34.1 wins outright at every
   container width of 960px and up, which is most desktops and is exactly where
   a size ramp is worth having. The Widget Size radio moved the pane's height
   on a phone and nowhere else, silently.

   34.2 documents this trap and is where Counter Sand and Frost restate their
   own ceilings for the same reason: "34.1's `!important` at (1,4,0) outranks
   the (1,3,0) `!important` those carry — the token is the only way their
   numbers survive." Coral Pop was simply missing from that list; this is its
   entry, kept here beside the ramp it restates rather than in 34.2, so the two
   cannot drift apart.

   THE TOKEN, NOT THE PROPERTY. Setting `max-height` here would fight 34.1
   forever; setting the tokens it READS lets its own rule compute the right
   number, and leaves 34.5 free to zero the separator at (1,5,0) when nothing
   follows the widget. That last part is why this must not restate
   `--so-bw-desk-sep`: a rule that spelled the whole expression out would
   silently flatten 34.5 back out again, which is the failure 34.2's closing
   note warns about for `--so-bw-desk-inset`.

   FLOOR EQUALS CEILING, and that is a real difference from the shared band's
   800/860. The gap upstream leaves is for templates whose steps vary in height
   — a calendar step is not a form step — so the card is allowed to settle
   somewhere inside it. This template's pane does not vary: section 38 fixes
   the head and hands every remaining pixel to a scrolling list, so there is no
   natural height for the card to find. Pinning both ends is what makes the
   card one size on every step, which is the whole point of section 34's title.

   `[data-template]` bare, NOT `[data-template="soft"]`: the layout key is
   shared with other templates and keying on it would resize them too. The bare
   presence selector buys the fourth attribute while the value match stays on
   the id — the same trick section 39's full-width rule documents.

   INLINE ONLY, for the same reason the rule above is: a shell sizes the card
   itself. A floating panel wide enough to stamp the 960 tier would otherwise
   get this ramp instead of the shell's height, and the card would stand short
   inside it. Gated, the template falls back to the shared band there, exactly
   like the other twenty-two. */
#so-booking-widget[data-cwmin~="960"][data-position="inline"][data-template][data-template-id="coral-pop"] .so-bw-container {
    --so-bw-desk-floor: min(var(--so-bw-cp-maxh), calc(100dvh - var(--so-bw-desk-inset) - var(--so-bw-desk-sep)));
    --so-bw-desk-ceil:  min(var(--so-bw-cp-maxh), calc(100dvh - var(--so-bw-desk-inset) - var(--so-bw-desk-sep)));
}

/* FULL WIDTH IS A WIDE CARD, NOT AN EDGE-TO-EDGE ONE.
   Section 23.2 reads "full" as a different INTENT from "large" — break out of
   the column entirely — and gives it `max-width: 100%`. On this template that
   is the wrong end of the scale: the service tiles below are laid out by
   auto-fill, so an unbounded card keeps adding columns until a row of six
   thin tiles crosses a 27-inch monitor and the step stops reading as a list.

   Coral Pop's natural width is 620px (~24590), so the size ramp is 496 / 620 /
   775. Full reaches well past that — 1280px, four tiles across on a wide
   monitor — but it is still a CARD, not a bleed.

   THE GAP IS THE POINT. `calc(100% - var(--so-bw-cp-full-gutter) * 2)` keeps a
   margin on both sides at every page width, so the widget grows into the page
   instead of running to its edges. The container is `margin: 0 auto`
   (section 2), so the two sides stay equal without anything being positioned.
   A card flush against the browser chrome reads as a layout failure rather
   than as a wide card, and on a page with its own padding it collides with
   whatever sits beside it.

   Both arms matter and neither alone is enough: the percentage is what holds
   the gap on a narrow page, the 1280px ceiling is what stops a 4K monitor
   stretching one row of tiles across two feet of glass.

   `min(…)` keeps the mobile behaviour section 23.2 relies on — the width is a
   ceiling, never a floor, so nothing pushes the card past its column.

   SPECIFICITY, and why the selector looks redundant. The rule being overridden
   is `[data-size="full"][data-template][data-template-id]` — (1,4,0). Matching
   it needs a fourth attribute, and the obvious one is `[data-template="soft"]`.
   That would be wrong: "soft" is the SHARED layout key, and a full-width rule
   keyed on it would resize every template on that key. The bare presence
   selectors `[data-template]` and `[data-template-id]` buy the same
   specificity while the value match stays on the template id alone. */
#so-booking-widget[data-size="full"][data-template][data-template-id][data-template-id="coral-pop"] .so-bw-container {
    /* The breathing room either side. One value so the two gaps can never
       drift apart, and so a host page that needs more can raise it in one
       place. Tightens on a phone, where 24px of each edge is a tenth of the
       screen and the cards need the width more than the margin does. */
    --so-bw-cp-full-gutter: 24px;
    max-width: min(calc(100% - var(--so-bw-cp-full-gutter) * 2), 1280px);
}
    #so-booking-widget:where([data-cw~="480"])[data-size="full"][data-template][data-template-id][data-template-id="coral-pop"] .so-bw-container {
        --so-bw-cp-full-gutter: 10px;
    }

/* ── 1c · THE CONTENT SCALES WITH THE SIZE CONTROL ────────────────────────
   Everything above sizes the SHELL. This sizes what is inside it, and the two
   are deliberately separate controls of one setting.

   THE PROBLEM. Every metric in sections 2–7 was an absolute px value, so the
   Widget Size radio moved the card's outer edges and nothing else: at "Small"
   a 496px pane still carried a 64px disc, a 44px search field, 36px stepper
   discs and 32px of side padding, so the chrome ate the width it had just
   been told to give up and one tile per row was all that fit. At "Full" the
   same furniture was adrift in 1280px. The control read as "crop the card"
   rather than "choose a size".

   THE FIX. One multiplier, `--so-bw-cp-d`, set on the container and inherited
   by everything below. Each interior metric is written as its design value
   times that multiplier, so the whole interior is one proportional system and
   a size change moves all of it together.

   WHY MEDIUM IS 0.88 AND NOT 1. The design values in sections 2–7 are the
   sizes this template shipped with, and they are what the redesign was drawn
   at — but drawn one-card-per-row, before section 5 turned the list into an
   auto-filling grid of 250px tiles. At tile scale the same furniture is too
   heavy: the disc, the field and the paddings are sized for a band that ran
   the pane's full width. 0.88 is the compaction that puts them back in
   proportion with the tile, and keeping the literals at their drawn values
   means the multiplier is the only thing to change if that judgement moves —
   nothing has to be re-derived from a scaled number.

   THE RAMP IS SUB-LINEAR ON PURPOSE. 0.8 / 0.88 / 0.96 / 1 against a shell
   ramp of 0.8 / 1 / 1.25 / ~2. Text and touch targets do not scale with the
   box: a 44px control at 0.8 is 35px, already at the floor of what a thumb
   can hit, and a 13px label at 0.8 is 10.4px. Matching the shell's ramp would
   put both under their limits at Small and make the interior of a Full-width
   card cartoonish. So the shell grows faster than its contents, which is also
   what makes the extra width buy an extra COLUMN of tiles rather than fatter
   ones — the point of the auto-fill grid.

   `--so-bw-cp-d` is only ever multiplied, never used bare, so a stylesheet
   that never sets it (or a browser that drops the declaration) falls back to
   the default below rather than to zero-sized furniture.

   TWO METRICS GET THEIR OWN TOKEN because they are shared across rules that
   must not drift: the side padding, which the header, body and nav all carry
   so the stepper, the search field and the cards share one left edge (see the
   grid note in section 2); and the icon disc, which the service tile and the
   location tile both draw and which must stay one size across the two
   consecutive steps.

   TWO PROPERTIES, NOT ONE, and the split is what makes section 1e possible.
   `--so-bw-cp-d-base` is what the size radio sets; `--so-bw-cp-d` is what
   every metric reads. Anything deeper in the tree can then re-derive
   `--so-bw-cp-d` from the base for its own subtree — a whole region compacts
   further with one declaration, and every metric inside it follows because
   they all already read the same property. Deriving it from ITSELF would be a
   cycle, which CSS resolves to guaranteed-invalid rather than to a nested
   scale, so the base has to be a separate property for this to work at all. */
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-container {
    --so-bw-cp-d-base: 0.88;
    --so-bw-cp-d: var(--so-bw-cp-d-base);
    --so-bw-cp-pad-x: calc(32px * var(--so-bw-cp-d));
    --so-bw-cp-disc: calc(64px * var(--so-bw-cp-d));
}
#so-booking-widget[data-size="small"][data-template-id="coral-pop"] .so-bw-container {
    --so-bw-cp-d-base: 0.8;
}
#so-booking-widget[data-size="medium"][data-template-id="coral-pop"] .so-bw-container {
    --so-bw-cp-d-base: 0.88;
}
#so-booking-widget[data-size="large"][data-template-id="coral-pop"] .so-bw-container {
    --so-bw-cp-d-base: 0.96;
}
#so-booking-widget[data-size="full"][data-template-id="coral-pop"] .so-bw-container {
    --so-bw-cp-d-base: 1;
}

/* ── 1d · THE STEPS THIS SECTION NEVER RESTYLED ───────────────────────────
   Sections 2–7 cover the service and location steps and the chrome around
   them. Date/Time, Staff, the form and the review pane are drawn by the base
   stylesheet, and compacting only the first two steps would give the widget
   two densities: tight tiles on step 1, then full-size slot pills and form
   fields on step 3. The size control has to move all of it or none of it.

   Nothing here restyles those steps — it re-states the SPACING TOKENS the
   base rules already read (~138) at this template's scale, so the compaction
   reaches them without this section acquiring an opinion about how a slot or
   a calendar looks. Set on the container, so the tokens resolve for every
   descendant and a step this template has never heard of gets it too.

   `--so-bw-input-font-size` IS DELIBERATELY ABSENT. The mobile block at ~4112
   raises it to 16px on purpose: iOS Safari zooms the page when a field with
   smaller text takes focus, and the zoom does not undo itself when the field
   blurs. That guard is set on the widget root and this rule is on the
   container BELOW it, so anything written here would shadow it on every
   descendant and hand phone users a booking form that zooms on focus and
   stays zoomed. A slightly large field on a small widget is the cheaper
   fault, so the token is left where it is. */
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-container {
    --so-bw-input-pad-y: calc(10px * var(--so-bw-cp-d));
    --so-bw-input-pad-x: calc(14px * var(--so-bw-cp-d));
    --so-bw-input-label-size: max(12px, calc(13px * var(--so-bw-cp-d)));
    --so-bw-slot-pad-y: calc(9px * var(--so-bw-cp-d));
    --so-bw-slot-pad-x: calc(10px * var(--so-bw-cp-d));
    --so-bw-slot-font-size: max(12px, calc(13px * var(--so-bw-cp-d)));
    --so-bw-slot-gap: calc(6px * var(--so-bw-cp-d));
    --so-bw-calendar-day-font-size: max(12px, calc(13px * var(--so-bw-cp-d)));
    /* Floored at 32px: this is a tap target, not a label. */
    --so-bw-calendar-nav-size: max(32px, calc(28px * var(--so-bw-cp-d)));
    --so-bw-summary-pad-y: calc(12px * var(--so-bw-cp-d));
    --so-bw-summary-pad-x: calc(20px * var(--so-bw-cp-d));
    /* 18 / 16 are this template's own values (~11806), not the root defaults —
       the point is to scale what Coral Pop chose, not to flatten it back to
       the base. */
    --so-bw-card-pad: calc(18px * var(--so-bw-cp-d));
    --so-bw-card-gap: calc(16px * var(--so-bw-cp-d));
    /* The tap floor, which the base rules give to every slot pill, input and
       select (~10070). 52px is the soft register's generous target and it is
       most of why the Date/Time step reads twice the weight of the compacted
       service tiles beside it. Floored at 44 — the accessibility minimum, and
       the same floor the controls in sections 4 and 5 carry. */
    --so-bw-ds-tap: max(44px, calc(52px * var(--so-bw-cp-d)));
    /* The staff step's avatars. The service and location discs have their own
       token (--so-bw-cp-disc) because this section pins them explicitly; this
       is the primitive's default, for the grids it does not touch. */
    --so-bw-icon-size: calc(48px * var(--so-bw-cp-d));
    --so-bw-icon-svg-size: calc(28px * var(--so-bw-cp-d));
    --so-bw-icon-text-size: calc(19px * var(--so-bw-cp-d));
}
/* The mobile block at ~4100 tightens the same four tokens, and it sets them on
   the widget ROOT while the rule above sets them on the container inside it —
   so the rule above silently wins on a phone and the tightening is lost. These
   are the mobile values, scaled, restated at the depth that beats it. Only the
   four the mobile block actually moves are here; the rest it leaves alone and
   so does this.

   THE DOUBLED `[data-template-id]` IS LOAD-BEARING, here and on every narrow
   rule in this section. `:where()` contributes NO specificity, so a narrow
   rule written as `:where([data-cw~="480"])[data-template-id="coral-pop"]
   .so-bw-container` is (1,2,0) — a point BELOW the (1,3,0) base rule it is
   trying to override, and it loses no matter where it sits in the file. Every
   narrow block in this section was written that way and none of them applied:
   the phone got the desktop disc, the desktop padding and the desktop
   masthead. Carrying the bare presence selector a second time buys the
   matching point without widening what the rule matches. */
    #so-booking-widget:where([data-cw~="480"])[data-template-id="coral-pop"][data-template-id] .so-bw-container {
        --so-bw-card-pad: calc(14px * var(--so-bw-cp-d));
        --so-bw-card-gap: calc(12px * var(--so-bw-cp-d));
        --so-bw-icon-size: calc(40px * var(--so-bw-cp-d));
        --so-bw-icon-svg-size: calc(24px * var(--so-bw-cp-d));
        --so-bw-icon-text-size: calc(16px * var(--so-bw-cp-d));
    }

/* ── 1e · THE TILES GO SMALLER STILL ──────────────────────────────────────
   The card grids get their own multiplier ON TOP of the pane's, because a tile
   and the pane it sits in do not want the same scale.

   WHY THEY DIVERGE. The pane's furniture is sized for reading — the masthead,
   the stepper labels, the search field, the commit button are each one thing
   the customer looks at once. A tile is the opposite: it is a repeating unit,
   and the number of them visible at a glance is most of what makes the step
   usable. Every pixel a tile spends is spent again on every other tile in the
   grid, so the same 12% that reads as "tidier" on the chrome reads as "one
   more row and one more column" here. Section 38 made this worth doing: the
   head is fixed now, so the whole pane below it is list, and a smaller tile
   turns directly into more of it.

   HOW IT COMPOSES. The grid re-derives `--so-bw-cp-d` from `--so-bw-cp-d-base`
   — the size radio's value — times its own factor. Every metric inside already
   reads `--so-bw-cp-d`, so all of them follow from this one declaration and
   nothing had to be rewritten to be shrunk twice.

   DERIVING FROM THE BASE, NOT FROM THE INHERITED VALUE, IS WHAT KEEPS IT
   SAFE. The two selectors below nest — a `.so-bw-card` sits inside a
   `.so-bw-card-grid` — and if this multiplied the inherited `--so-bw-cp-d` the
   factor would apply twice on the card and once on the grid around it, so the
   gap and the tiles would scale apart. Re-deriving from the base makes the
   rule idempotent: it lands on the same number however many times it is
   applied, which is why both selectors can carry it without coordination.

   `--so-bw-cp-disc` and the four base tokens are RESTATED rather than
   inherited. A custom property that reads another one is substituted where it
   is DECLARED, so section 1d's copies computed against the pane's multiplier
   and inherit down as fixed lengths. Re-declaring them here is what puts them
   back under the tile's. */
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-card-grid,
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-card {
    /* One number, on its own line, because it is the only thing here anyone
       will want to turn. Everything below is derived. */
    --so-bw-cp-card-d: 0.85;
    /* The fallback matters here in a way it does not on the container: these
       selectors match any `.so-bw-card` under the widget root, and the base is
       declared on `.so-bw-container`. Every card lives inside it today (the
       staff-profile panel says so explicitly at its markup), but a card
       rendered outside it would resolve the var to nothing, and an invalid var
       inside a calc() takes the WHOLE declaration out at computed-value time —
       so a stray card would lose every metric below, not just this one. */
    --so-bw-cp-d: calc(var(--so-bw-cp-d-base, 0.88) * var(--so-bw-cp-card-d));
    --so-bw-cp-disc: calc(64px * var(--so-bw-cp-d));
    --so-bw-card-pad: calc(18px * var(--so-bw-cp-d));
    --so-bw-card-gap: calc(16px * var(--so-bw-cp-d));
    --so-bw-icon-size: calc(48px * var(--so-bw-cp-d));
    --so-bw-icon-svg-size: calc(28px * var(--so-bw-cp-d));
    --so-bw-icon-text-size: calc(19px * var(--so-bw-cp-d));
}
/* The narrow-width tightening from 1d, restated at this depth for the same
   reason it was restated there: it is set higher up the tree and these rules
   are lower, so without this the phone silently gets the desktop values. */
    #so-booking-widget:where([data-cw~="480"])[data-template-id="coral-pop"][data-template-id] .so-bw-card-grid,
    #so-booking-widget:where([data-cw~="480"])[data-template-id="coral-pop"][data-template-id] .so-bw-card {
        --so-bw-card-pad: calc(14px * var(--so-bw-cp-d));
        --so-bw-card-gap: calc(12px * var(--so-bw-cp-d));
        --so-bw-icon-size: calc(40px * var(--so-bw-cp-d));
        --so-bw-icon-svg-size: calc(24px * var(--so-bw-cp-d));
        --so-bw-icon-text-size: calc(16px * var(--so-bw-cp-d));
    }

#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-header,
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-body,
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-nav {
    background: var(--so-bw-card-bg, #ffffff);
}
/* The rounded foot follows whichever element is last — `.so-bw-powered` is
   optional (Studio → Appearance), so neither can simply be given the corner. */
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-header {
    border-radius: var(--so-bw-r-24) var(--so-bw-r-24) 0 0;
}
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-nav {
    border-radius: 0 0 var(--so-bw-r-24) var(--so-bw-r-24);
    border-top: 0;
}
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-nav:not(:last-child) {
    border-radius: 0;
}
/* The footer is the one band that is NOT white: a hair of grey separates it
   from the action above without drawing a line. */
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-powered {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-wrap: wrap;
    gap: calc(6px * var(--so-bw-cp-d)) calc(14px * var(--so-bw-cp-d));
    padding: calc(14px * var(--so-bw-cp-d)) calc(24px * var(--so-bw-cp-d));
    background: var(--so-bw-bg-secondary, #fafafa);
    border-top: 1px solid color-mix(in srgb, var(--so-bw-primary) 8%, var(--so-bw-border, #e9e9ee));
    border-radius: 0 0 var(--so-bw-r-24) var(--so-bw-r-24);
    font-size: max(11px, calc(13px * var(--so-bw-cp-d)));
}
/* The rule between the two facts is a border on the trust line, not a glyph in
   the text: a literal "|" would be read aloud by a screen reader and would not
   disappear when the line wraps to two rows. */
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-powered__trust {
    display: inline-flex;
    align-items: center;
    gap: calc(7px * var(--so-bw-cp-d));
    padding-right: calc(14px * var(--so-bw-cp-d));
    border-right: 1px solid color-mix(in srgb, var(--so-bw-primary) 14%, var(--so-bw-border, #e9e9ee));
}
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-powered__trust svg {
    flex: 0 0 auto;
    opacity: 0.75;
}

/* ── 2 · THE HEADER IS A MASTHEAD ─────────────────────────────────────────
   Title and subtitle on the left, the step counter as a tinted pill on the
   right, aligned to the title's cap height rather than centred on the block —
   a counter that floats level with the subtitle reads as part of it.

   `.so-bw-step-label` carries the step's own name ("Choose a Service"), which
   in this design is already said by the active stepper label directly below
   it. Two headings for one thing is what made the old header feel stacked. */
/* A GRID, NOT A FLEX ROW. The header holds four children — brand, step
   indicator, stepbar, progress — and a flex row makes all four compete for one
   line: the counter lands beside the title instead of at the corner, and the
   stepbar is squeezed into whatever is left, which is what bunched six steps
   into the right-hand third.

   Two columns and explicit placement instead. Row 1 is the masthead: brand
   left, counter hard right. Row 2 is the stepbar, spanning BOTH columns so it
   runs the full width of the header.

   The stepbar then measures the same as the search field below it, because the
   header and the body carry the same 32px side padding — that is the alignment
   holding the two together, so the two paddings must be changed as a pair. */
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-header {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: start;
    column-gap: calc(20px * var(--so-bw-cp-d));
    row-gap: 4px;
    padding: calc(20px * var(--so-bw-cp-d)) var(--so-bw-cp-pad-x) 0;
}
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-brand {
    grid-column: 1;
    grid-row: 1;
    min-width: 0;
}
/* Full width, second row. `grid-column: 1 / -1` rather than `span 2` so it
   still spans everything if a third column is ever added above it. */
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-stepbar {
    grid-column: 1 / -1;
    grid-row: 2;
}
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-brand__title {
    font-size: calc(clamp(20px, 2vw, 26px) * var(--so-bw-cp-d));
    font-weight: 800;
    letter-spacing: -0.02em;
    line-height: 1.15;
}
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-brand__subtitle {
    margin-top: 3px;
    font-size: calc(14px * var(--so-bw-cp-d));
}
/* The counter goes to the top-right corner. `justify-self: end` puts it on the
   column's right edge; the column is `auto`, so it is only as wide as the pill
   and the brand keeps everything else. The indicator is only carrying the
   counter now (the step label below is hidden), so it stops being a row. */
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-step-indicator {
    grid-column: 2;
    grid-row: 1;
    justify-self: end;
    align-self: start;
    margin: 0;
    padding: 0;
}
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-step-label {
    display: none;
}
/* Below 480px of CONTAINER width the pill and the title cannot share a line
   without squeezing the title to two words. The masthead stacks: title, then
   counter on its own row at the left margin, then the stepbar. */
    #so-booking-widget:where([data-cw~="480"])[data-template-id="coral-pop"][data-template-id] .so-bw-header {
        grid-template-columns: minmax(0, 1fr);
        padding: calc(20px * var(--so-bw-cp-d)) calc(16px * var(--so-bw-cp-d)) 0;
    }
    #so-booking-widget:where([data-cw~="480"])[data-template-id="coral-pop"][data-template-id] .so-bw-step-indicator {
        grid-column: 1;
        grid-row: 2;
        justify-self: start;
        margin-top: 10px;
    }
    #so-booking-widget:where([data-cw~="480"])[data-template-id="coral-pop"][data-template-id] .so-bw-stepbar {
        grid-row: 3;
    }

#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-step-count {
    display: inline-block;
    padding: calc(7px * var(--so-bw-cp-d)) calc(15px * var(--so-bw-cp-d));
    border-radius: var(--so-bw-r-10);
    background: var(--so-bw-cp-tint-strong);
    color: color-mix(in srgb, var(--so-bw-primary) 75%, var(--so-bw-text, #2a1f1e));
    font-size: calc(15px * var(--so-bw-cp-d));
    font-weight: 700;
    white-space: nowrap;
}

/* ── 3 · THE STEPPER GETS ITS LABELS BACK ─────────────────────────────────
   Coral Pop hid them (~10561) and shrank the dot to 9px, which turned a named
   six-step journey into six anonymous pips. The customer could see how far
   along they were and not what any of it was.

   The dot is a 44px disc: inactive is a tinted circle with a quiet numeral,
   active is the accent gradient in white, done is a solid accent with a tick.
   `font-size` is restored explicitly — the soft rule zeroed it to hide the
   numeral inside a 9px dot, and that would otherwise persist into a 44px one. */
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-stepbar {
    align-items: flex-start;
    gap: 0;
    margin: calc(14px * var(--so-bw-cp-d)) 0 2px;
}
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-stepbar-label {
    display: block;
    margin-top: calc(7px * var(--so-bw-cp-d));
    /* The one metric with a FLOOR under it. 12.5 × 0.8 is 10px, which is under
       the size at which a 600-weight label is comfortably readable, so Small
       stops shrinking the step names a little before the rest of the ramp
       does. max() rather than a per-size override: the floor then holds at any
       multiplier, including one a host theme sets. */
    font-size: max(11px, calc(12.5px * var(--so-bw-cp-d)));
    font-weight: 600;
    color: var(--so-bw-text-tertiary);
}
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-stepbar-item--active .so-bw-stepbar-label {
    color: var(--so-bw-text, #2a1f1e);
    font-weight: 700;
}
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-stepbar-dot {
    /* One token for the disc, because the connector below has to sit on its
       centre line and would otherwise have to repeat the arithmetic. */
    --so-bw-cp-dot: calc(36px * var(--so-bw-cp-d));
    width: var(--so-bw-cp-dot);
    height: var(--so-bw-cp-dot);
    min-width: var(--so-bw-cp-dot);
    border-radius: 50%;
    border: 0;
    background: var(--so-bw-cp-tint);
    color: color-mix(in srgb, var(--so-bw-primary) 55%, var(--so-bw-text-tertiary, #8a7f7e));
    font-size: calc(15px * var(--so-bw-cp-d));
    line-height: 1;
    overflow: visible;
}
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-stepbar-dot svg {
    display: block;
    width: calc(18px * var(--so-bw-cp-d));
    height: calc(18px * var(--so-bw-cp-d));
}
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-stepbar-item--active .so-bw-stepbar-dot {
    background: var(--so-bw-cp-accent-grad);
    color: var(--so-bw-card-bg, #ffffff);
    box-shadow: 0 6px 16px color-mix(in srgb, var(--so-bw-primary) 35%, transparent);
}
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-stepbar-item--done .so-bw-stepbar-dot {
    background: var(--so-bw-primary);
    color: var(--so-bw-card-bg, #ffffff);
}
/* The connector sits on the dot's centre line. `top` is half the dot, so it
   tracks the 44px above rather than the 26px the base rule assumed. */
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-stepbar-item:not(:first-child)::before {
    /* Half the disc above. Written out rather than reusing --so-bw-cp-dot:
       that token is set ON the dot, and this pseudo-element hangs off the
       item, which is its PARENT — so it cannot inherit it. */
    top: calc(18px * var(--so-bw-cp-d));
    height: 3px;
    border-radius: 999px;
    background: color-mix(in srgb, var(--so-bw-primary) 10%, var(--so-bw-border, #e9e9ee));
}
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-stepbar-item--active:not(:first-child)::before,
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-stepbar-item--done:not(:first-child)::before {
    background: var(--so-bw-primary);
}
/* The thin bar under the stepper is a fallback for widths too narrow to label.
   With the labels back it is a second progress indicator saying the same thing. */
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-progress {
    display: none;
}

/* ── 4 · THE SEARCH FIELD IS A FIELD, NOT A PILL ──────────────────────────
   The template sets `--so-bw-input-radius: 999px` (~11805) and the design
   wants a 14px rounded rectangle here. Overridden on the search input alone:
   the token still governs every other control, so the round register survives
   where it belongs. */
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-search-input {
    border-radius: var(--so-bw-r-14);
    border: 1px solid color-mix(in srgb, var(--so-bw-primary) 10%, var(--so-bw-border, #e9e9ee));
    box-shadow: none;
    /* 44 is the touch-target minimum, so the floor is the interesting half of
       this: 44 × 0.8 is 35px, which is a control a thumb misses. The field
       shrinks with the rest of the interior down to 40px and no further. */
    min-height: max(40px, calc(44px * var(--so-bw-cp-d)));
    padding-left: calc(40px * var(--so-bw-cp-d));
    font-size: calc(14px * var(--so-bw-cp-d));
    max-width: calc(520px * var(--so-bw-cp-d));
}
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-search-input:focus {
    border-color: color-mix(in srgb, var(--so-bw-primary) 45%, var(--so-bw-card-bg, #ffffff));
    box-shadow: 0 0 0 4px color-mix(in srgb, var(--so-bw-primary) 12%, transparent);
}
/* THE FIELD NARROWS; THE STRIP DOES NOT — and the difference is load-bearing.

   The strip runs edge to edge, pulled out to the full pane width by the
   negative margins at ~11893. Cap the STRIP and it stops being a band and
   becomes a floating field with two channels of body either side of it, which
   is what breaks the join with the header below.

   So the 520px cap sits on `.so-bw-search-input` instead. The visible control
   gets smaller, the band stays edge to edge, and the Filter button still sits
   directly beside the field rather than stranded at the far right. */
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-search {
    /* The strip's own inset, restated so it tracks the size control and — more
       importantly — so it equals the body's side padding. The soft rule at
       ~11892 sets a flat 20px against this template's 32px body padding, which
       left the field's left edge 12px inboard of the cards it filters. One
       token for both is what puts them on a single spine. The strip is pulled
       out by this value and given it back as padding (~11893), so the band
       still runs edge to edge whatever the number is. */
    --so-bw-cp-strip-pad: var(--so-bw-cp-pad-x);
    background-color: var(--so-bw-card-bg, #ffffff);
    /* ── THE STRIP IS THE BOTTOM OF THE HEAD ──────────────────────────────
       The header, the stepper and this field are one fixed block; the cards
       are the only thing that moves. Three things make that read as one block
       rather than as a field that happens to sit still:

         • SAME SURFACE. The header and the body are both `--so-bw-card-bg`
           (section 1) and the strip restates it above, so there is no seam
           between the header's box and this one — they are two elements
           painting one continuous white band.
         • NO GAP. The header closes on `padding-bottom: 0` and the step's top
           padding is zeroed at ~11868, so the strip begins exactly where the
           stepper's labels end. The top padding here is the ONLY space between
           them, which is why it is a real value now rather than the 2px left
           over from when the strip was pinned and the strip's own inner band
           was doing this job.
         • ONE EDGE, AT THE BOTTOM. The head needs a single closing line, and
           it belongs under the LAST thing in the head — not under the header
           element. A rule drawn under the header would cut between the stepper
           and the search and make the field read as the top of the list
           instead of the bottom of the head, which is the exact opposite of
           what this rule is for.

       The line is also what the scrolling list needs: it is the edge the cards
       travel up to and stop at, so the scroll has a visible boundary instead
       of cards fading out against an unbroken white field. */
    padding-top: calc(12px * var(--so-bw-cp-d));
    padding-bottom: calc(12px * var(--so-bw-cp-d));
    border-bottom: 1px solid color-mix(in srgb, var(--so-bw-primary) 10%, var(--so-bw-border, #e9e9ee));
}
/* The chip row belongs to the head too — it is what the Filter button
   discloses — so the closing line moves down to IT whenever it is open.
   Without this the chips sit below the head's edge and read as the first row
   of the list, which is where the customer is looking for services. */
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-search:has(+ .so-bw-filter-chips:not([style*="display: none"])) {
    border-bottom-color: transparent;
}
/* — the chip row's own band is in part 4b, where the rest of its skin lives. */
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-search-icon {
    left: calc(var(--so-bw-cp-strip-pad, 20px) + calc(14px * var(--so-bw-cp-d)));
    color: color-mix(in srgb, var(--so-bw-primary) 30%, var(--so-bw-text-tertiary, #6b6470));
}

/* ── 4c · NO STEP BLURB ABOVE THE LIST ────────────────────────────────────
   `.so-bw-step-desc` is the sentence at the head of each step — "Select one or
   more services. They'll be booked back-to-back in the order you pick them.",
   "Pick a date and time for your appointment.", and six more.

   Removed for the same reason section 2 hides `.so-bw-step-label`: this
   template's masthead already names the step, the stepper repeats the name
   under an active dot, and the brand subtitle sits between them. A fourth line
   of prose before the customer reaches a single control is what made the head
   read as stacked, and it pushes the first card below the fold on the shorter
   pane sizes.

   `display: none` and not a markup change: the sentence is real information on
   the eighteen templates whose header does not already state it, and the
   multi-service ordering note in particular has nowhere else to live. This is a
   per-template chrome decision, so it belongs here rather than in the markup.
   Nothing is stranded by hiding it: it is a static <p>, not a control.

   WHAT IS ACTUALLY LOST, stated rather than glossed. The multi-service note is
   the only place the widget says the appointments run back-to-back IN PICK
   ORDER. `.so-bw-multi-summary` appears once two are chosen, but it carries a
   count and the combined duration and price — not the sequence. So on this
   template that ordering is discoverable from the CTAs reading "Selected" down
   the list and from the review step, not from a promise made up front. That is
   the trade this rule makes; if it proves wrong, the fix is a line in the
   summary bar rather than restoring a paragraph above the search. */
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-step-desc {
    display: none;
}

/* ── 4b · THE FILTER CONTROL ──────────────────────────────────────────────
   The button sits beside the field as a flex sibling, so the input has to stop
   claiming the whole row. `.so-bw-search-input` is `width: 100%` from the base
   (~7442); `flex: 1 1 auto` with `width: auto` lets it yield the button's width
   instead of overflowing the strip.

   THE CLEAR BUTTON IS THE CATCH. It is `position: absolute; right: 8px`
   against `.so-bw-search` (~7459), which is now wider than the input by the
   button plus the gap — so it would land ON the Filter button rather than
   inside the field. Both offsets are driven off one custom property so they
   cannot drift: change the button's width and the × follows it. */
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-search {
    --so-bw-cp-filter-w: calc(118px * var(--so-bw-cp-d));
    --so-bw-cp-filter-gap: calc(12px * var(--so-bw-cp-d));
    gap: var(--so-bw-cp-filter-gap);
}
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-search-input {
    flex: 1 1 auto;
    width: auto;
    min-width: 0;
}
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-search:has(.so-bw-search-filter:not([style*="display: none"])) .so-bw-search-clear {
    right: calc(var(--so-bw-cp-strip-pad, 20px) + var(--so-bw-cp-filter-w) + var(--so-bw-cp-filter-gap) + calc(10px * var(--so-bw-cp-d)));
}
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-search-filter {
    flex: 0 0 var(--so-bw-cp-filter-w);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: calc(9px * var(--so-bw-cp-d));
    /* Same 40px floor as the field beside it — the two share a row and a
       mismatch would show as a step in the strip, not just as a small button. */
    min-height: max(40px, calc(44px * var(--so-bw-cp-d)));
    border-radius: var(--so-bw-r-14);
    border: 1px solid color-mix(in srgb, var(--so-bw-primary) 10%, var(--so-bw-border, #e9e9ee));
    background: var(--so-bw-card-bg, #ffffff);
    color: var(--so-bw-text, #2a1f1e);
    font-family: inherit;
    font-size: calc(15px * var(--so-bw-cp-d));
    font-weight: 600;
    cursor: pointer;
    transition: border-color var(--so-bw-transition), background var(--so-bw-transition), color var(--so-bw-transition);
}
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-search-filter:hover {
    border-color: color-mix(in srgb, var(--so-bw-primary) 35%, var(--so-bw-card-bg, #ffffff));
}
/* Open OR narrowed. The button has to look engaged while a filter is applied
   even after the chip row is collapsed again — otherwise a customer who
   filtered, closed the row, and forgot has a short list and no visible reason. */
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-search-filter--on {
    border-color: transparent;
    background: var(--so-bw-cp-tint-strong);
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-filter-chips {
    display: flex;
    flex-wrap: wrap;
    gap: calc(8px * var(--so-bw-cp-d));
    /* PART OF THE HEAD, NOT THE FIRST ROW OF THE LIST. The row is what the
       Filter button discloses, so it has to sit inside the block that button
       belongs to. Full-bleed like the strip above it — same pull-out and
       give-back as ~11893 — and it carries the head's closing line while it is
       open, which the strip hands down to it (see the `:has(+ …)` rule beside
       the strip). A chip row below the head's edge reads as the top of the
       results, which is where the customer is looking for services. */
    margin: 0 calc(var(--so-bw-cp-pad-x) * -1);
    padding: 0 var(--so-bw-cp-pad-x) calc(12px * var(--so-bw-cp-d));
    border-bottom: 1px solid color-mix(in srgb, var(--so-bw-primary) 10%, var(--so-bw-border, #e9e9ee));
}
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-filter-chip {
    min-height: max(34px, calc(38px * var(--so-bw-cp-d)));
    padding: 0 calc(16px * var(--so-bw-cp-d));
    border-radius: 999px;
    border: 1px solid color-mix(in srgb, var(--so-bw-primary) 12%, var(--so-bw-border, #e9e9ee));
    background: var(--so-bw-card-bg, #ffffff);
    color: var(--so-bw-text-secondary, #4a4048);
    font-family: inherit;
    font-size: calc(14px * var(--so-bw-cp-d));
    font-weight: 600;
    cursor: pointer;
    transition: border-color var(--so-bw-transition), background var(--so-bw-transition), color var(--so-bw-transition);
}
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-filter-chip:hover {
    border-color: color-mix(in srgb, var(--so-bw-primary) 32%, var(--so-bw-card-bg, #ffffff));
}
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-filter-chip--on {
    border-color: transparent;
    background: var(--so-bw-cp-accent-grad);
    color: var(--so-bw-card-bg, #ffffff);
}

/* A narrow container cannot hold a 118px button beside a usable field. The
   button drops to its own row under the search, full width. */
    #so-booking-widget:where([data-cw~="480"])[data-template-id="coral-pop"][data-template-id] .so-bw-search {
        flex-wrap: wrap;
    }
    #so-booking-widget:where([data-cw~="480"])[data-template-id="coral-pop"][data-template-id] .so-bw-search-filter {
        flex: 1 1 100%;
    }
    #so-booking-widget:where([data-cw~="480"])[data-template-id="coral-pop"][data-template-id] .so-bw-search-clear {
        right: calc(var(--so-bw-cp-strip-pad, 20px) + calc(10px * var(--so-bw-cp-d)));
    }

/* ── 5 · THE SERVICE ROW ──────────────────────────────────────────────────
   Three columns: the gradient tile, the text, and the action. The text column
   is the only one that flexes, so the tile and the action hold the same x on
   every row and the list gets a spine down both edges.

   WHERE THE COLUMNS ACTUALLY LIVE. The tile (`.so-bw-card-media`) is a grid
   area on the CARD (~21503). The text and the action are inside
   `.so-bw-card-top`, which holds `.so-bw-card-body` and — for this template —
   `.so-bw-card-cta` beside it. So the row is one grid nested in another, and
   the action column is laid out by card-top, NOT by card-body.

   This is worth stating because getting it wrong is silent: an earlier pass
   put `grid-column: 2` on `.so-bw-card-check`, which is a child of card-top
   and not of the two-column card-body it was written against. The property
   applied to nothing and the circle landed correctly anyway, by flex order —
   a rule that appears to work and does nothing. */
/* THE GRID DECIDES HOW MANY FIT; NOTHING COUNTS COLUMNS.
   `auto-fill` with a 250px floor means the column count falls out of the
   available width instead of being declared per size — small lands on one,
   medium two, full three — and it keeps working at widths nobody enumerated,
   including a theme column this stylesheet has never seen.

   `minmax(250px, 1fr)` not a fixed track: 250 is the narrowest a tile can be
   and still hold "Full Body Massage" on one line above its meta row; `1fr`
   then shares the remainder so the last column is never a stub.

   The one-per-row rule this replaces is at ~11825 and ~19915 — both keyed on
   the layout key, so this must out-specify them, which it does by carrying
   the template id twice. */
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-card-grid {
    /* The tile floor scales too, and that is what turns the size control into
       a COLUMN COUNT rather than a zoom: a smaller widget carrying smaller
       furniture can still hold a tile that reads, so the track floor comes
       down with it and the grid keeps fitting two across where a fixed 250
       would have dropped to one. */
    grid-template-columns: repeat(auto-fill, minmax(calc(250px * var(--so-bw-cp-d)), 1fr));
    gap: calc(16px * var(--so-bw-cp-d));
}
/* THE CARD IS A TILE: media over body, centred.
   The shared rule at ~21484 lays this card out as `"media body"` — avatar
   BESIDE the text — with `display: grid !important`. The areas are restated
   here as a single column so the same grid stacks instead. `:not(.so-bw-sp__
   panel)` is carried for the reason section 23.3 documents: it keeps the
   staff-profile popup (which shares .so-bw-card) out, and it is what lifts
   this to (1,4,0) so it out-ranks the (1,3,0) rule it is overriding rather
   than relying on source order. */
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-card--service:not(.so-bw-sp__panel) {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas:
        "media"
        "body"
        "extras"
        "panel";
    justify-items: center;
}
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-card--service {
    border: 1px solid color-mix(in srgb, var(--so-bw-primary) 9%, var(--so-bw-border, #ececf1));
    border-radius: var(--so-bw-r-16);
    background: var(--so-bw-card-bg, #ffffff);
    box-shadow: none;
    transition: border-color var(--so-bw-transition), background var(--so-bw-transition), box-shadow var(--so-bw-transition);
}
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-card--service:hover {
    border-color: color-mix(in srgb, var(--so-bw-primary) 28%, var(--so-bw-card-bg, #ffffff));
    box-shadow: 0 4px 18px rgba(24, 20, 30, 0.06);
}
/* SELECTED: the tint and the accent border together. Neither alone is enough —
   the tint on its own is a wash a colour-blind eye can miss, and the border on
   its own is 1px of difference across a 1400px row. */
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-card--service.so-bw-card--selected {
    background: var(--so-bw-cp-tint);
    border-color: var(--so-bw-primary);
}
/* NO CORNER FLAG HERE — and this is a rule about the mechanism, not a taste
   about the ornament.

   `.so-bw-card--selected::after` is ALREADY TAKEN. The shared rule at ~14449
   draws the selection ring with it: an inset overlay carrying
   `border: 2px solid var(--so-bw-primary)` and `border-radius: inherit`. It is
   a pseudo-element, so there is exactly one per card — a template that styles
   `::after` for its own decoration does not add to the ring, it REPLACES it.

   A corner flag drawn here did precisely that: Coral Pop lost its selection
   ring, and the zero-size border-triangle that took its place would not clip to
   the card's 16px corner, so it rendered as a blob overhanging the radius. The
   card looked broken at the exact moment the customer picked it.

   The state is not under-communicated without a flag. A selected row carries
   the ring, the blush tint, the accent border AND the CTA reading "Selected".
   If a flag is ever wanted, it needs its own element in the markup — `::before`
   is spoken for on some templates too (Timeline's axis node), so neither
   pseudo-element is free to borrow. */

/* THE ICON IS A DISC AT THE HEAD OF THE TILE.
   It stretched to the row height while the card was a horizontal band, where
   the tile's job was to anchor a left spine down the list. In a stacked tile
   there is no row to measure and no spine to hold: the icon is a fixed 64px
   disc centred above the name, and every card in the grid carries the same one
   so the row scans as a set.

   THE STRETCH HAS TO BE UNDONE EXPLICITLY, not merely not-restated. The avatar
   primitive pins all six of width/height/min-/max- to --so-bw-icon-size
   (~14231), and the previous layout released every one of them to let the box
   grow. Leaving them released here would let a tall tile grow the disc again,
   so the size is pinned back on all four axes.

   Circular, not the rounded square the band used: `border-radius: 50%` is the
   primitive's own default, restored. The per-service colour still comes from
   `serviceIconTileStyle()`; the gradient over it keeps the lit-from-above
   quality the accent controls have. */
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-card--service .so-bw-card-media {
    grid-area: media;
    align-self: center;
    justify-self: center;
    display: flex;
    align-items: center;
    justify-content: center;
    margin: calc(24px * var(--so-bw-cp-d)) 0 0;
}
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-card-media-avatar,
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-card-media-img {
    align-self: center;
    width: var(--so-bw-cp-disc);
    height: var(--so-bw-cp-disc);
    min-width: var(--so-bw-cp-disc);
    max-width: var(--so-bw-cp-disc);
    min-height: var(--so-bw-cp-disc);
    max-height: var(--so-bw-cp-disc);
    aspect-ratio: 1 / 1;
    border-radius: 50%;
    --so-bw-icon-svg-size: calc(28px * var(--so-bw-cp-d));
    --so-bw-icon-text-size: calc(22px * var(--so-bw-cp-d));
}
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-card-media-avatar {
    background-image: linear-gradient(135deg, rgba(255, 255, 255, 0.28), rgba(255, 255, 255, 0));
    box-shadow: none;
}
/* A service WITH a photo renders `.so-bw-card-media-img` in the same slot, so
   it takes the same disc. `object-fit: cover` crops to fill rather than
   letting a landscape photo letterbox inside a circle. */
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-card-media-img {
    object-fit: cover;
}

/* card-top is the row: text takes the slack, the action sizes to itself.

   `align-items: flex-start` with the text block padded to the same 20px as the
   tile's margin. Centring was wrong once the tile stretched: on a card whose
   text runs shorter than the tile's 56px floor, centred text drifts off the
   tile's top edge and the title no longer starts where the tile does. Aligning
   both to the top pins the title's first line to the tile's top edge at every
   card height, which is the alignment the design reads by. */
/* The text block stacks and centres under the disc. `align-items: stretch`
   rather than `center`, so the CTA below can run the tile's full width — a
   centred column would shrink-wrap the button to its label. */
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-card--service .so-bw-card-top {
    grid-area: body;
    /* `align-self`, because the card carries `align-items: start !important`
       (~21492). Without this the text block only occupies its own content
       height, the CTA's `margin-top: auto` has no slack to consume, and the
       buttons stop lining up across a row of unequal descriptions. A
       different property, so it wins over the container's !important. */
    align-self: stretch;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    width: 100%;
    padding: calc(14px * var(--so-bw-cp-d)) calc(18px * var(--so-bw-cp-d)) calc(18px * var(--so-bw-cp-d));
    text-align: center;
}
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-card--service .so-bw-card-body {
    flex: 0 1 auto;
    min-width: 0;
    width: 100%;
}
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-card--service .so-bw-card-title {
    /* Floored at 13px. This metric now passes through TWO multipliers — the
       size radio's and the tile's (section 1e) — and the service name is the
       one string on the card the customer is actually reading. 16.5 at the
       small end of both ramps lands under 12px without this. */
    font-size: max(13px, calc(16.5px * var(--so-bw-cp-d)));
    font-weight: 700;
    letter-spacing: -0.01em;
}
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-card--service .so-bw-card-meta {
    margin-top: calc(5px * var(--so-bw-cp-d));
    /* 12px floor: the description is the smallest type on the tile already,
       and the ramp would take it to 10.8 at Small. */
    font-size: max(12px, calc(13.5px * var(--so-bw-cp-d)));
}
/* THE META ROW LOSES ITS PILLS.
   In the band layout the duration and price were two tinted pills at the left
   of a wide row, where they had space to read as chips. In a 250px tile they
   are two coloured lozenges stacked under a centred title, and they fight the
   price below for the same attention. They become one quiet centred line
   instead — the pill is the wrong pressure at this size, not the wrong text. */
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-card--service .so-bw-card-badges {
    justify-content: center;
    margin-top: calc(6px * var(--so-bw-cp-d));
    gap: calc(6px * var(--so-bw-cp-d));
}
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-badge--duration {
    padding: 0;
    background: transparent;
    color: var(--so-bw-text-tertiary, #8a7f7e);
    font-size: max(12px, calc(13px * var(--so-bw-cp-d)));
    font-weight: 500;
}
/* The price badge is dropped, not restyled: the tile already states the price
   once, at full size, directly above the button. Printing it twice in one
   250px column reads as two different numbers until you look twice. */
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-card--service .so-bw-card-badges .so-bw-badge--price {
    display: none;
}
/* `--so-bw-primary-ink`, not `--so-bw-primary`: the brand hue is chosen to
   carry a white glyph on a filled control, and the same value as TEXT on a
   pale tint can fall under 4.5:1. The ink token is the contrast-corrected
   variant, and the widget-wide rule (see the "brand hue through an ink token"
   test) is that text takes it. Falls back to the raw hue where no ink token
   is set, which is the pre-existing behaviour at ~19941. */
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-badge--price,
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-badge--free {
    padding: calc(7px * var(--so-bw-cp-d)) calc(14px * var(--so-bw-cp-d));
    background: var(--so-bw-cp-tint-strong);
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
    font-size: max(12px, calc(13.5px * var(--so-bw-cp-d)));
    font-weight: 700;
}

/* THE ACTION COLUMN — `.so-bw-card-cta`, rendered by the widget only when
   usesSoftChrome is on (see the template gate in booking-widget.js). It is a
   DIV, not a button, and aria-hidden: the whole card is already the control, so
   a real button here would be a second focus stop doing the same job and a
   screen reader would announce the service twice. The check circle beside it
   has always been a div for the same reason.

   The circle itself now has nothing to say — the CTA states the pick in words —
   so it is removed from the row rather than left as a second, smaller mark of
   the same fact. */
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-card--service .so-bw-card-check {
    display: none;
}
/* The CTA is the tile's foot: price, then a full-width control, separated from
   the text above by a rule. `margin-top: auto` pushes it to the bottom so the
   buttons line up across a row of tiles whose descriptions differ in length —
   without it a two-line card's button sits a line higher than its neighbour's
   and the row reads as ragged. */
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-card-cta {
    flex: 0 0 auto;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: calc(12px * var(--so-bw-cp-d));
    width: 100%;
    min-width: 0;
    margin-top: auto;
    padding-top: calc(14px * var(--so-bw-cp-d));
    border-top: 1px solid color-mix(in srgb, var(--so-bw-primary) 8%, var(--so-bw-border, #ececf1));
}
/* The price at full size. `--so-bw-primary-ink` rather than the raw hue: this
   is TEXT, and the widget-wide rule is that text takes the contrast-corrected
   token (see the "brand hue through an ink token" test). */
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-card-cta__price {
    font-size: calc(20px * var(--so-bw-cp-d));
    font-weight: 800;
    letter-spacing: -0.01em;
    line-height: 1.1;
    text-align: center;
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
    white-space: nowrap;
}
/* Unselected: an outline control that reads as available. Selected: the accent
   gradient, matching the Continue button — the same visual language for "this
   is chosen" and "this is the action", which is what makes the pair legible. */
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-card-cta__btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: calc(8px * var(--so-bw-cp-d));
    /* The 40px floor again — this is the tile's only control. */
    min-height: max(40px, calc(44px * var(--so-bw-cp-d)));
    padding: 0 calc(18px * var(--so-bw-cp-d));
    border-radius: var(--so-bw-r-12);
    border: 1px solid var(--so-bw-border, #e9e9ee);
    background: var(--so-bw-card-bg, #ffffff);
    color: var(--so-bw-text, #2a1f1e);
    font-size: calc(15px * var(--so-bw-cp-d));
    font-weight: 600;
    white-space: nowrap;
    transition: background var(--so-bw-transition), border-color var(--so-bw-transition), color var(--so-bw-transition);
}
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-card--service:hover .so-bw-card-cta__btn {
    border-color: color-mix(in srgb, var(--so-bw-primary) 35%, var(--so-bw-card-bg, #ffffff));
}
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-card-cta__btn--on {
    border-color: transparent;
    background: var(--so-bw-cp-accent-grad);
    color: var(--so-bw-card-bg, #ffffff);
    font-weight: 700;
    box-shadow: 0 6px 16px color-mix(in srgb, var(--so-bw-primary) 32%, transparent);
}

/* A narrow container needs no column override — `auto-fill` already collapses
   the grid to one track below 250px + gutters, which is the whole point of
   sizing it that way. What DOES need saying is that a single full-width tile
   should not carry a 64px disc and 18px of padding on a 360px phone: the
   proportions that make a three-across row breathe make one card look empty.

   Gated on CONTAINER width (not viewport — see the data-cw note repeated
   through this file), so a narrow theme column gets the same treatment as a
   narrow screen. */
    #so-booking-widget:where([data-cw~="480"])[data-template-id="coral-pop"][data-template-id] .so-bw-card-grid {
        gap: calc(12px * var(--so-bw-cp-d));
    }
    #so-booking-widget:where([data-cw~="480"])[data-template-id="coral-pop"][data-template-id] .so-bw-card--service .so-bw-card-media {
        margin: calc(18px * var(--so-bw-cp-d)) 0 0;
    }
    /* Shadowing --so-bw-cp-disc on the element itself, rather than restating
       six literals: the six pins already read the token, so one declaration
       narrows all of them and they cannot fall out of step. It does not reach
       the location tile — that icon is a different element and gets its own
       narrow rule at the end of section 5b. */
    #so-booking-widget:where([data-cw~="480"])[data-template-id="coral-pop"][data-template-id] .so-bw-card-media-avatar,
    #so-booking-widget:where([data-cw~="480"])[data-template-id="coral-pop"][data-template-id] .so-bw-card-media-img {
        --so-bw-cp-disc: calc(52px * var(--so-bw-cp-d));
        width: var(--so-bw-cp-disc);
        height: var(--so-bw-cp-disc);
        min-width: var(--so-bw-cp-disc);
        max-width: var(--so-bw-cp-disc);
        min-height: var(--so-bw-cp-disc);
        max-height: var(--so-bw-cp-disc);
        --so-bw-icon-svg-size: calc(24px * var(--so-bw-cp-d));
        --so-bw-icon-text-size: calc(18px * var(--so-bw-cp-d));
    }
    #so-booking-widget:where([data-cw~="480"])[data-template-id="coral-pop"][data-template-id] .so-bw-card--service .so-bw-card-top {
        padding: calc(12px * var(--so-bw-cp-d)) calc(14px * var(--so-bw-cp-d)) calc(14px * var(--so-bw-cp-d));
    }

/* ── 5b · THE LOCATION STEP TAKES THE SAME TILE ───────────────────────────
   The service step was redesigned and the location step was not, so the two
   consecutive steps looked like two different products: tiles with disc icons
   on one, then a flex row with the base border and radius on the next.

   Everything in part 5 is keyed on `.so-bw-card--service`, and location cards
   do not carry it — they were a bare `.so-bw-card`, which is also what the
   staff cards and the staff-profile popup use. The markup now tags them
   `.so-bw-card--location` (see the note at the card in booking-widget.js) so
   this can aim at them without hitting the other two.

   The grid needs nothing: `.so-bw-card-grid` already auto-fills, and the
   location list uses the same class, so it reflows by width exactly as the
   service list does.

   WHAT IS DELIBERATELY NOT COPIED. A location card has no `.so-bw-card-cta` —
   that element is service-step markup — so there is no price line and no
   "Select" button here. Rather than invent one, the pick is stated by the
   accent ring, the tint and the check disc, which is what the base card
   already does. Adding a labelled control would mean new markup on a step that
   does not need it: a location has nothing to state but its name and address. */
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-card--location {
    flex-direction: column;
    align-items: center;
    justify-content: flex-start;
    gap: calc(12px * var(--so-bw-cp-d));
    padding: calc(22px * var(--so-bw-cp-d)) calc(18px * var(--so-bw-cp-d));
    border: 1px solid color-mix(in srgb, var(--so-bw-primary) 9%, var(--so-bw-border, #ececf1));
    border-radius: var(--so-bw-r-16);
    background: var(--so-bw-card-bg, #ffffff);
    box-shadow: none;
    text-align: center;
    transition: border-color var(--so-bw-transition), background var(--so-bw-transition), box-shadow var(--so-bw-transition);
}
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-card--location:hover {
    border-color: color-mix(in srgb, var(--so-bw-primary) 28%, var(--so-bw-card-bg, #ffffff));
    box-shadow: 0 4px 18px rgba(24, 20, 30, 0.06);
}
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-card--location.so-bw-card--selected {
    background: var(--so-bw-cp-tint);
    border-color: var(--so-bw-primary);
}
/* The pin sits in the same 64px disc the service tile uses. This is
   `.so-bw-card-icon--avatar`, NOT `.so-bw-card-media-avatar` — the location
   card renders the icon directly rather than through the media band, so the
   part 5 rules never reached it. Same six pins released-and-repinned reasoning
   as there: the primitive drives all of them off --so-bw-icon-size. */
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-card--location .so-bw-card-icon--avatar {
    --so-bw-icon-size: var(--so-bw-cp-disc);
    --so-bw-icon-svg-size: calc(28px * var(--so-bw-cp-d));
    --so-bw-icon-text-size: calc(22px * var(--so-bw-cp-d));
    flex: 0 0 auto;
    border-radius: 50%;
    background-image: linear-gradient(135deg, rgba(255, 255, 255, 0.28), rgba(255, 255, 255, 0));
}
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-card--location .so-bw-card-body {
    width: 100%;
    min-width: 0;
}
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-card--location .so-bw-card-title {
    /* Same 13px floor as the service tile, and for the same reason — the two
       steps are consecutive and must not disagree about type size. */
    font-size: max(13px, calc(16.5px * var(--so-bw-cp-d)));
    font-weight: 700;
    letter-spacing: -0.01em;
}
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-card--location .so-bw-card-meta {
    margin-top: calc(5px * var(--so-bw-cp-d));
    font-size: max(12px, calc(13.5px * var(--so-bw-cp-d)));
    /* The address is the one long string on this card and it has nowhere to go
       in a 250px column, so it is allowed to wrap rather than being clamped:
       a truncated address is not an address. */
    overflow-wrap: anywhere;
}
/* The check keeps the base card's visibility contract — hidden until picked,
   but always in flow, so the tile does not change height on selection. Only
   its skin changes here, to the accent disc the rest of the template uses. */
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-card--location .so-bw-card-check {
    width: calc(30px * var(--so-bw-cp-d));
    height: calc(30px * var(--so-bw-cp-d));
    border-radius: 999px;
    background: var(--so-bw-cp-accent-grad);
    color: var(--so-bw-card-bg, #ffffff);
    box-shadow: 0 4px 12px color-mix(in srgb, var(--so-bw-primary) 30%, transparent);
}
    #so-booking-widget:where([data-cw~="480"])[data-template-id="coral-pop"][data-template-id] .so-bw-card--location {
        padding: calc(16px * var(--so-bw-cp-d)) calc(14px * var(--so-bw-cp-d));
        gap: calc(10px * var(--so-bw-cp-d));
    }
    #so-booking-widget:where([data-cw~="480"])[data-template-id="coral-pop"][data-template-id] .so-bw-card--location .so-bw-card-icon--avatar {
        --so-bw-icon-size: calc(52px * var(--so-bw-cp-d));
        --so-bw-icon-svg-size: calc(24px * var(--so-bw-cp-d));
        --so-bw-icon-text-size: calc(18px * var(--so-bw-cp-d));
    }

/* ── 6 · THE PRIMARY ACTION ───────────────────────────────────────────────
   The gradient and the lift are what mark this as the one thing to do on the
   step. The disabled state keeps the gradient at reduced opacity rather than
   flattening to grey: the button is the same object either way, and greying it
   out reads as a different control appearing once a service is picked. */
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-nav {
    padding: calc(18px * var(--so-bw-cp-d)) var(--so-bw-cp-pad-x);
}
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-btn--primary {
    padding: calc(14px * var(--so-bw-cp-d)) calc(30px * var(--so-bw-cp-d));
    background: var(--so-bw-cp-accent-grad);
    border: 0;
    color: var(--so-bw-card-bg, #ffffff);
    font-size: max(14px, calc(16px * var(--so-bw-cp-d)));
    font-weight: 700;
    box-shadow: 0 8px 22px color-mix(in srgb, var(--so-bw-primary) 32%, transparent);
}
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-btn--primary:hover:not(:disabled) {
    box-shadow: 0 10px 28px color-mix(in srgb, var(--so-bw-primary) 42%, transparent);
    transform: translateY(-1px);
}
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-btn--primary:disabled {
    opacity: 0.45;
    box-shadow: none;
}

/* ── 7 · THE LIST BREATHES AT BOTH ENDS ───────────────────────────────────
   Bottom: section 23 gives every template 16px so the last card is not flush
   against the commit bar; this template insets by 32px elsewhere, so 16 reads
   as a near-miss. Top: `scroll-padding-top` is the sticky strip's height — it
   does not change how the list looks, it changes where a SCROLL lands. Without
   it, focusing a card by keyboard parks it under the search field, which is
   the one place in the pane the customer cannot see it. */
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-body {
    padding: 0 var(--so-bw-cp-pad-x) 4px;
    /* NO `scroll-padding-top`. It was the sticky strip's height, so that a
       card focused by keyboard did not land underneath it. Section 38 part 3
       moved the strip out of the scroll box entirely, so there is nothing left
       overhanging the list to reserve space for — a scroll offset here would
       now push the first row down for no reason. */
}
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-step {
    padding-bottom: calc(24px * var(--so-bw-cp-d)) !important;
}

/* ── 8 · THE REVIEW STEP READS AS THE SAME PRODUCT ────────────────────────
   The final report — step 8, `.so-bw-summary` — was the one step this section
   never reached. It fell through to the rules keyed on the SHARED layout key
   `[data-template="soft"]` (~15680), which are sound in structure and were
   drawn for the OLD Coral Pop: the blush shell, 22px corners, a neutral card.
   This section inverted that register six steps earlier, so a customer who
   picked a service from white tiles with coral accents arrived at a review
   panel in the template's previous design.

   TWO THINGS WERE ACTUALLY WRONG, not merely unstyled.

   IT DID NOT SCALE. Every metric in those rules is a px literal — 11.5 label,
   16 value, 22 total, 13 section title — so the Studio "Widget Size" radio and
   the tile compaction of sections 1c–1e stopped at the review step. Every
   other step in this template answers to the size control; this one did not,
   which is the same fault as the radius literals and the desktop height, and
   the third place it has turned up. Everything below reads `--so-bw-cp-d`.

   IT DID NOT MATCH. The panel sat on `--so-bw-ds-n0` at `r-22` with no border,
   against tiles that are `--so-bw-card-bg` at `r-16` inside a 9%-primary
   hairline. Same surface, same corner and same hairline here, so the review
   panel reads as one more card in the set rather than as another template.

   WHAT IS DELIBERATELY LEFT ALONE. `.so-bw-itinerary` is gated on
   `usesItinerary`, true only for the agenda and timeline layouts — this
   template never renders it, and styling markup it cannot emit would be rules
   nobody can verify and nobody can safely delete later. Same for the
   membership block. The promo-code row and the form controls inside the panel
   take the input tokens section 1d already scales, so they need nothing here.

   THE TOTAL IS THE ONE SATURATED MARK, which is the colour policy this section
   opened with: one accent, three placements. On this step the placement is the
   amount the customer is about to be charged. */
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-summary {
    --so-bw-row-pad-y: calc(12px * var(--so-bw-cp-d));
    padding: calc(4px * var(--so-bw-cp-d)) calc(20px * var(--so-bw-cp-d));
    border: 1px solid color-mix(in srgb, var(--so-bw-primary) 9%, var(--so-bw-border, #ececf1));
    border-radius: var(--so-bw-r-16);
    background: var(--so-bw-card-bg, #ffffff);
    /* ── THE PANEL SCROLLS ITSELF ─────────────────────────────────────────
       `overflow: hidden` was here so the tint band — pulled out to the
       panel's edges by a negative inline margin — is clipped back to the
       rounded corner. It still is: any overflow value other than `visible`
       clips to the border radius, so `auto` keeps that job.

       What it was ALSO doing was hiding the review. `.so-bw-step` is a flex
       column (~13024), so this panel is a flex item, and a flex item is only
       protected from being shrunk below its content while its own overflow is
       `visible`. With `hidden` the automatic minimum size is zero, so the
       panel was crushed to whatever the pane had left and silently cut the
       rest off — measured at 372px of panel holding 704px of content on a
       1280x700 desktop, and 357px holding 934px on a 390px phone. Nothing
       could scroll to the remainder: the panel had no scrollbar, and
       `.so-bw-body` had nothing to scroll because the clipping had already
       absorbed the overflow. The total, the payment breakdown and the consent
       checkboxes were unreachable on the last step before paying.

       `auto` turns that same crush into a scroll. The panel still takes
       exactly the height the pane leaves it — which is what keeps the stepper
       above and the Back / Confirm bar below fixed in place, the arrangement
       this template is drawn for — and the detail rows now move inside it.

       overflow-X STAYS HIDDEN, and it has to be stated rather than left to
       the shorthand: a lone `overflow-y: auto` computes overflow-x to `auto`
       as well, and the tint band's negative inline margin is exactly the
       horizontal overflow that would then hang a second scrollbar under the
       panel. */
    overflow-y: auto;
    overflow-x: hidden;
    /* A floor, so a short pane leaves a panel worth scrolling rather than a
       two-row sliver. Past this the panel stops shrinking and the body scrolls
       instead — the fallback every other template already uses. */
    min-height: min(220px, 100%);
    /* Momentum on iOS, and the bar stays out of the neighbouring pane. */
    -webkit-overflow-scrolling: touch;
    overscroll-behavior: contain;
}
/* THE BAR HAS TO BE VISIBLE, not discovered by scrolling. This panel is the
   last screen before paying and the rows it hides are the price and the
   consents, so "there is more below" has to be stated by the interface rather
   than left to a customer to find.

   Which is why `scrollbar-width` / `scrollbar-color` are NOT set on the rule
   above. Setting either one switches Chromium to the standard scrollbar path,
   where it ignores every `::-webkit-scrollbar` rule below AND draws an overlay
   bar that fades out when idle — measured as 0px of layout width and an
   invisible bar at rest, which is the state this is fixing. Dropping them
   leaves the classic bar: always drawn while the content overflows, and it
   reserves its own 8px so no value ends up sitting underneath it.

   8px matches the list scrollers elsewhere in the file, so a customer meets
   one bar width across the whole widget. */
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-summary::-webkit-scrollbar {
    width: 8px;
}
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-summary::-webkit-scrollbar-track {
    background: transparent;
}
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-summary::-webkit-scrollbar-thumb {
    background: color-mix(in srgb, var(--so-bw-primary) 28%, var(--so-bw-border, #ececf1));
    border-radius: 999px;
}
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-summary::-webkit-scrollbar-thumb:hover {
    background: color-mix(in srgb, var(--so-bw-primary) 45%, var(--so-bw-border, #ececf1));
}
/* Firefox has no `::-webkit-scrollbar`, so it needs the standard properties —
   and it is the one engine where they cost nothing, because its `thin` bar is
   classic rather than an overlay that fades. Gated on the SELECTOR not being
   supported so Chromium and Safari never see these and keep the rules above;
   testing for the pseudo-element is what makes this a capability check rather
   than a browser sniff. */
@supports not selector(::-webkit-scrollbar) {
    #so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-summary {
        scrollbar-width: thin;
        scrollbar-color: color-mix(in srgb, var(--so-bw-primary) 28%, var(--so-bw-border, #ececf1)) transparent;
    }
}
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-summary-section-title {
    margin: calc(20px * var(--so-bw-cp-d)) 0 calc(6px * var(--so-bw-cp-d));
    font-size: max(11px, calc(13px * var(--so-bw-cp-d)));
    font-weight: 700;
    color: var(--so-bw-text-tertiary, #8a7f7e);
}
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-summary-section:first-child .so-bw-summary-section-title {
    margin-top: calc(14px * var(--so-bw-cp-d));
}
/* The label sits ABOVE its value — `order: -1` on a column row, which is the
   soft register's own arrangement and the reason a long address or service
   name has the panel's full width to wrap into rather than a right-hand half.
   Kept as it is; only the sizes move. */
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-summary-label {
    font-size: max(11px, calc(11.5px * var(--so-bw-cp-d)));
    font-weight: 600;
    color: var(--so-bw-text-secondary, #4a4048);
}
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-summary-value {
    font-size: max(13px, calc(16px * var(--so-bw-cp-d)));
    font-weight: 600;
    line-height: 1.35;
}
/* THE TOTAL: a blush band across the foot of the panel.
   Full-bleed by a negative inline margin equal to the panel's own padding, so
   the band meets the border on both sides instead of floating inside it — the
   same pull-out-and-give-back the search strip uses. The panel clips it to the
   16px corner (the `overflow: hidden` above), which is what stops the band's
   square bottom edge overhanging the rounded one. */
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-summary-row--total {
    margin: calc(10px * var(--so-bw-cp-d)) calc(20px * var(--so-bw-cp-d) * -1) 0;
    padding: calc(12px * var(--so-bw-cp-d)) calc(20px * var(--so-bw-cp-d));
    background: var(--so-bw-cp-tint);
    border-top: 1px solid color-mix(in srgb, var(--so-bw-primary) 12%, transparent);
}
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-summary-row--total .so-bw-summary-label {
    font-size: max(12px, calc(14px * var(--so-bw-cp-d)));
    color: var(--so-bw-text, #2a1f1e);
}
/* `--so-bw-primary-ink`, not the raw hue: this is TEXT on a pale tint, and the
   brand colour is chosen to carry white on a filled control. The ink token is
   the contrast-corrected variant — the same rule the tile price and the badges
   follow, enforced by the "brand hue through an ink token" test. */
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-summary-row--total .so-bw-summary-value {
    font-size: max(16px, calc(22px * var(--so-bw-cp-d)));
    font-weight: 800;
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}
/* The payment breakdown and any trailing block: label and value on one line,
   separated from the rows above by a tinted hairline rather than a grey one. */
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-summary-block {
    padding-top: calc(12px * var(--so-bw-cp-d));
    border-top-color: color-mix(in srgb, var(--so-bw-primary) 10%, var(--so-bw-border, #ececf1));
}
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-summary-block .so-bw-summary-label,
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-summary-pay .so-bw-summary-label {
    font-size: max(11px, calc(13px * var(--so-bw-cp-d)));
}
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-summary-block .so-bw-summary-value,
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-summary-pay .so-bw-summary-value {
    font-size: max(12px, calc(14px * var(--so-bw-cp-d)));
}
/* The timezone line and the notes beside a row: secondary information, so they
   take the ink the meta row uses on a tile. */
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-summary-tz,
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-summary-note {
    font-size: max(11px, calc(12.5px * var(--so-bw-cp-d)));
    color: var(--so-bw-text-tertiary, #8a7f7e);
}
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-summary-undo {
    font-size: max(11px, calc(12.5px * var(--so-bw-cp-d)));
    font-weight: 600;
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
}
/* A discount or credit chip: the same pale-tint pill the duration badge uses,
   so a second kind of chip does not arrive on the last step. */
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-summary-chip {
    padding: calc(4px * var(--so-bw-cp-d)) calc(10px * var(--so-bw-cp-d));
    border-radius: 999px;
    background: var(--so-bw-cp-tint-strong);
    color: var(--so-bw-primary-ink, var(--so-bw-primary));
    font-size: max(11px, calc(12px * var(--so-bw-cp-d)));
    font-weight: 700;
}
/* The policies list under the panel — consent lines and cancellation terms.
   Quiet by construction: it is a thing to have read, not a thing to read. */
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-policies--review {
    margin-top: calc(16px * var(--so-bw-cp-d));
    font-size: max(11px, calc(12.5px * var(--so-bw-cp-d)));
}
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-policies-title,
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-policies-group-title {
    font-size: max(11px, calc(12.5px * var(--so-bw-cp-d)));
    font-weight: 700;
    color: var(--so-bw-text-secondary, #4a4048);
}
/* The validation alert, when a required consent is missed. Coral rather than
   the stock red would be wrong here — an error is an error, and a state that
   means "stop" is the one place this section does not spend the accent. */
#so-booking-widget[data-template-id="coral-pop"][data-template-id] .so-bw-alert {
    border-radius: var(--so-bw-r-12);
    font-size: max(12px, calc(13.5px * var(--so-bw-cp-d)));
}
    #so-booking-widget:where([data-cw~="480"])[data-template-id="coral-pop"][data-template-id] .so-bw-summary {
        padding-inline: calc(16px * var(--so-bw-cp-d));
    }
    #so-booking-widget:where([data-cw~="480"])[data-template-id="coral-pop"][data-template-id] .so-bw-summary-row--total {
        margin-inline: calc(16px * var(--so-bw-cp-d) * -1);
        padding-inline: calc(16px * var(--so-bw-cp-d));
    }

/* -- C10 . Full Width is exactly the viewport ----------------------------
   The card measured 818 of a 900px viewport: 82px was still being subtracted for
   an identity banner that is no longer rendered above it, and the widget ROOT was
   contributing a 12px top padding of its own. Neither belongs at a size whose
   whole point is filling the screen, so the root's vertical padding goes here and
   the subtraction goes to zero on the route that knows nothing is above the card.

   Scoped to Full Width. Every other size keeps the root padding, which is what
   separates the card from the page around it. */
#so-booking-widget[data-size="full"][data-template="wide"] {
    padding-block: 0;
    /* And the 40px separator below it. It exists to hold the widget off whatever
       follows on the page; at Full Width the widget IS the page, and left in place
       it pushes 40px past the fold and puts a scrollbar on a screen the card was
       just sized to fill exactly. */
    margin-bottom: 0;
}

/* -- C11 . Full screen feeds the desktop ceiling, rather than fighting it -
   Section 34 owns the desktop height ceiling: it computes
   `--so-bw-desk-ceil: min(860px, calc(100dvh - inset - sep))` and applies it as
   `max-height: … !important` at a higher specificity than anything here. So the
   full-screen height was being clamped to 828px on a 900px viewport - measured -
   no matter what this section asked for.

   Overriding the INPUTS instead of out-shouting the rule. `inset` and `sep`
   describe the root's 12px padding and 40px bottom margin, both of which are
   zeroed just above at this size, so they are genuinely 0 here rather than being
   lied about. And the 860px pixel term goes, because it is the one thing in that
   expression that cannot be right for a card asked to be the viewport.

   The floor moves with the ceiling: 34.1 sets `min-height: var(--so-bw-desk-floor)`,
   and a floor left at `min(800px, …)` would fight the ceiling on any viewport
   shorter than 800px. Both point at the same value, which is what "exactly the
   viewport" means. */
/* `[data-template-id]` is here for SPECIFICITY, not for filtering. Section 34's
   rule is
     #so-booking-widget[data-cwmin~="960"][data-template][data-template-id] .so-bw-container
   which scores (1,4,0). Without the third attribute this rule scores (1,3,0) and
   loses, so the variables kept their section-34 values and the card stayed at
   828px on a 900px viewport (measured). Matching (1,4,0) and sitting later in the
   file is what carries it. Any install that renders the widget has both
   attributes, so nothing is excluded by adding it. */
#so-booking-widget[data-size="full"][data-template="wide"][data-template-id] .so-bw-container {
    --so-bw-desk-inset: 0px;
    --so-bw-desk-sep: 0px;
    --so-bw-desk-floor: max(180px, calc(100dvh - var(--so-bw-vchrome, 112px)));
    --so-bw-desk-ceil: max(180px, calc(100dvh - var(--so-bw-vchrome, 112px)));
}

