/* ==========================================================================
   OSCAR Booking — patient booking surface
   Register: Stripe / Linear / Apple. Legible, neutral, branded not decorated.
   Light is the default for every viewer, including system-dark.
   Dark applies only when the reader explicitly stamps data-theme="dark".
   ========================================================================== */

:root {
  --ground: var(--color-ground);
  --surface: var(--color-surface);
  --surface-2: var(--color-surface-2);
  --ink: var(--color-ink);
  --ink-soft: var(--color-ink-soft);
  --ink-faint: var(--color-ink-faint);
  --line: var(--color-line);
  --line-strong: var(--color-line-strong);
  --accent: var(--color-primary);
  --accent-soft: var(--color-primary-soft);
  --accent-ink: var(--color-primary-ink);
  --on-accent: var(--color-on-primary);
  /* Text colour for anything painted on --accent-soft (2026-09-01, dark-mode
     hover/selected fix). Paired to the TINT, not the accent hue like
     --accent-ink is — public/app.js's applyClinicLook() sets this inline
     alongside --accent-soft, computed from --accent-soft's own relative
     luminance, so it stays legible no matter which theme (light/dark) the
     clinic's tint happens to read as. Default here just inherits --ink,
     which already flips correctly for the two built-in themes below. */
  --accent-soft-ink: var(--ink);
  --warn: var(--color-warn);
  --warn-soft: var(--color-warn-soft);
  --danger: var(--color-danger);
  --danger-soft: var(--color-danger-soft);
  --band-limited: var(--color-band-limited);
  --band-full: var(--color-band-full);
  /* OPENS2 (2026-09-19) — desk Schedule status swatches, owner-overridable
     from the fixed accessible palette (Settings > Appearance). Aliases so
     an untouched clinic renders byte-identical (they default to whichever
     semantic token each status already painted with). DESKG2 comp A: a
     Booked visit's default tone is the neutral ink-faint grey (Arrived
     keeps the green); an owner's own pick still wins. */
  --status-booked: var(--ink-faint);
  --status-noshow: var(--warn);
  --status-cancelled: var(--ink-faint);
  --status-nowline: var(--danger);
  --shadow: 0 1px 2px rgba(16, 19, 26, 0.05);
  --shadow-lift: 0 1px 3px rgba(16, 19, 26, 0.08), 0 6px 16px rgba(16, 19, 26, 0.06);
  --radius: var(--rounded-card);
  --radius-sm: var(--rounded-control);
  /* Corner "shape" (patient booking page only, per-clinic `look.corners`,
     2026-08-31) — cards and buttons/inputs are two distinct radius tiers so
     `pill` can round controls all the way (999px) without turning a card
     into a stadium shape. Default = today's actual values on every existing
     selector, so an untouched clinic is byte-identical. */
  --radius-control: var(--radius-sm);
  --radius-card: var(--radius);
  /* Body typeface (patient booking page only, per-clinic `look.font`,
     2026-08-31). `--font-control` is what buttons/inputs actually render in
     — it inherits `--font-body` by default (today's behavior: one typeface
     everywhere) and only splits from it in serif mode (see the `data-font`
     overrides below), where controls stay on a plain system-UI face. */
  --font-body: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
  --font-control: var(--font-body);
}

/* Per-clinic look overrides (2026-08-31) — set via `data-font`/`data-corners`
   on <html> by public/app.js's `applyClinicLook()`, driven by `GET
   /api/clinic`'s `look.font`/`look.corners` (schema.md `settings.look`).
   Absent/empty `look` means neither attribute is ever set, so these blocks
   never match and every existing clinic renders identically to before this
   feature. */
:root[data-font='serif'] {
  --font-body: Georgia, 'Times New Roman', serif;
  --font-control: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
}
:root[data-font='rounded'] {
  --font-body: ui-rounded, 'SF Pro Rounded', 'Nunito', system-ui, sans-serif;
}
:root[data-corners='square'] {
  --radius: var(--rounded-square);
  --radius-sm: var(--rounded-square);
  --radius-control: var(--rounded-square);
  --radius-card: var(--rounded-square);
}
:root[data-corners='pill'] {
  --radius-control: var(--rounded-pill);
  --radius-card: var(--rounded-pill-card);
}

:root[data-theme='dark'] {
  --ground: var(--color-ground-dark);
  --surface: var(--color-surface-dark);
  --surface-2: var(--color-surface-2-dark);
  --ink: var(--color-ink-dark);
  --ink-soft: var(--color-ink-soft-dark);
  --ink-faint: var(--color-ink-faint-dark);
  --line: var(--color-line-dark);
  --line-strong: var(--color-line-strong-dark);
  --accent: var(--color-primary-dark);
  --accent-soft: var(--color-primary-soft-dark);
  --accent-ink: var(--color-primary-ink-dark);
  --on-accent: var(--color-on-primary-dark);
  --accent-soft-ink: var(--ink);
  --warn: var(--color-warn-dark);
  --warn-soft: var(--color-warn-soft-dark);
  --danger: var(--color-danger-dark);
  --danger-soft: var(--color-danger-soft-dark);
  --band-limited: var(--color-band-limited-dark);
  --band-full: var(--color-band-full-dark);
  --status-booked: var(--ink-faint);
  --status-noshow: var(--warn);
  --status-cancelled: var(--ink-faint);
  --status-nowline: var(--danger);
  --shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
  --shadow-lift: 0 1px 3px rgba(0, 0, 0, 0.6), 0 6px 16px rgba(0, 0, 0, 0.45);
}

* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }

/* Load-bearing (2026-08-27, measured live): the UA stylesheet only hides
   [hidden] via `[hidden] { display: none }`, and ANY component rule that
   sets `display` on that same element — even one with lower specificity,
   even one written for the visible state — silently outranks the UA rule
   the instant both apply. `.pf-fill { display: block }` did exactly this:
   `$('pf-fill-code').hidden = true` correctly set the attribute, and the
   element rendered anyway with no click handler attached (app.js only
   wires the handler when a demoCode exists), because the UA's `[hidden]`
   rule and `.pf-fill`'s `display:block` have equal specificity and the
   component rule won on source order. `!important` here is the fix, not a
   style crutch — it makes `hidden` an ABSOLUTE property no later
   `display` declaration can defeat, for every element in this app, not
   just the one that got caught. Never remove this to "simplify" a single
   component's CSS. */
[hidden] { display: none !important; }

body {
  margin: 0;
  background: var(--ground);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: 15px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3 { margin: 0; text-wrap: balance; letter-spacing: -0.021em; }
p { margin: 0; }
button { font: inherit; font-family: var(--font-control); color: inherit; }

/* ------------------------------------------------------------------ shell */

.shell {
  max-width: 620px;
  margin: 0 auto;
  /* Flow layout, not a viewport-height frame (2026-09-01 fix): the footer
     sits directly under whatever content the current step actually has, so
     a short step (e.g. a small practitioner list) never leaves a void
     between the content and a footer that used to be pinned to the bottom
     of the viewport regardless of content length. */
  display: flex;
  flex-direction: column;
  background: var(--ground);
}

/* B167 (2026-09-27): the bar is a fixed 57px at every width and step — 6px
   padding around the 44px back and theme targets, and a two-line clinic
   name (38px) still fits inside that 44px row — so `.steps` below can pin
   to one known offset and the top of the page never jumps between steps. */
.topbar {
  position: sticky;
  top: 0;
  z-index: 20;
  background: var(--ground);
  border-bottom: 1px solid var(--line);
  padding: 6px 24px;
  display: flex;
  align-items: center;
  gap: 12px;
}

.brand {
  font-weight: 600;
  font-size: 15px;
  letter-spacing: -0.015em;
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* 2026-09-01 fix: a long clinic name ellipsised in the slim top bar at
   narrow widths ("Fit for Life Wellness & Rehab Ce…") — the name never
   truncates, it wraps to two lines and the bar grows to fit instead. */
@media (max-width: 600px) {
  .brand {
    overflow: visible;
    text-overflow: clip;
    white-space: normal;
    line-height: 1.25;
  }
}

/* B260: the top-bar clinic logo (no banner set). Height-led: its box is the
   38px two-line name block from B167. A wide wordmark is capped by width
   instead and scales down whole (contain, no round clip): 24% of the bar,
   never under 82px (a 360 phone's 24% would draw a wordmark's letters too
   small to read) and never over 120px (it would shout past the name on
   desktop). The logo shows at this size or not at all — fitBrandLogo
   (app.js) hides it when the clinic name needs the room, so the 57px bar
   never grows. No `display` here on purpose: a flex item is
   already a block, and the `hidden` attribute must still be able to win.
   A square emblem fills the full 38px. */
#brand-logo {
  flex: none;
  height: 38px;
  width: auto;
  max-width: clamp(82px, 24%, 120px);
  object-fit: contain;
}

/* B292: a light-ink logo (white lettering — a navy disc with a white
   wordmark) drew white on the white bar, a blank gap before the name.
   app.js reads the logo's own pixels once it loads and hangs a light-ink
   one on the banner plate's sign (B281, `.clinic-plate:has(>
   .clinic-plate-logo)` and `.clinic-plate-logo` above): the same white
   paper stock in every theme, hairline border, card corner and
   one-device-pixel keyline, sized to the `#brand-logo` box — 38px tall
   overall, so the logo inside draws 28px (1px border + 4px padding top and
   bottom), and the 82–120px width cap moves from the logo to the plate.
   The banner plate's shadow stays the banner's: it lifts that plate off
   the photo it overlaps, and nothing overlaps here. Any other logo stays
   bare under the rule above. The `hidden` attribute still wins over this
   rule's display (the global [hidden] guard near the top of this file).
   B301: on the dark bar the same plate carries a dark-ink logo instead (a
   near-black wordmark drew on the dark top bar, a blank gap again); the light tokens
   below are theme-proof on purpose — that wordmark reads 17.7:1 on them. */
.brand-logo-plate {
  flex: none;
  display: flex;
  align-items: center;
  height: 38px;
  max-width: clamp(82px, 24%, 120px);
  padding: 4px 8px;
  border-radius: var(--radius-card);
  background: var(--color-ground);
  border: 1px solid var(--color-line);
}
.brand-logo-plate > #brand-logo {
  height: 28px;
  max-width: 100%;
  filter: url(#brand-logo-keyline-1x);
}
@media (min-resolution: 1.5dppx), (-webkit-min-device-pixel-ratio: 1.5) {
  .brand-logo-plate > #brand-logo { filter: url(#brand-logo-keyline-2x); }
}
@media (min-resolution: 2.5dppx), (-webkit-min-device-pixel-ratio: 2.5) {
  .brand-logo-plate > #brand-logo { filter: url(#brand-logo-keyline-3x); }
}
/* The keyline filters' holder: 0x0 and out of the plate's flow; its colour
   is the keyline's ink, as on the banner plate. */
.brand-logo-keyline {
  position: absolute;
  width: 0;
  height: 0;
  color: var(--color-ink-soft);
}

/* B167: the back chevron's slot is reserved on every step (hidden, not
   removed, on step 1) so the clinic name keeps one width and never
   rewraps the moment the chevron appears. 44x44 target; the -12px left
   margin puts the glyph on the same 24px edge as the page content. */
.backbtn {
  border: 0;
  background: transparent;
  cursor: pointer;
  width: 44px;
  height: 44px;
  padding: 0;
  margin: 0 0 0 -12px;
  border-radius: var(--radius-control);
  color: var(--ink-soft);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  visibility: hidden;
  line-height: 0;
}
.backbtn.on { visibility: visible; }
.backbtn:hover { background: var(--surface-2); color: var(--ink); }
.backbtn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

.themetoggle {
  border: 1px solid var(--line);
  background: var(--ground);
  color: var(--ink-soft);
  cursor: pointer;
  height: 44px;
  padding: 0 12px;
  border-radius: var(--radius-control);
  font-size: 12px;
  font-weight: 500;
  flex: none;
}
.themetoggle:hover { background: var(--surface-2); color: var(--ink); }
.themetoggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* ------------------------------------------------------------- progress */

.steps {
  display: flex;
  gap: 4px;
  padding: 0 24px 14px;
  background: var(--ground);
  border-bottom: 1px solid var(--line);
  position: sticky;
  top: 57px; /* B167: the topbar's fixed height */
  z-index: 19;
}
.steps .bar {
  height: 2px;
  flex: 1;
  background: var(--line);
}
/* B169: the done bars read the theme token, and dark swaps it to
   primary-dark — `--accent` is pinned inline by applyClinicLook() to the
   clinic's light accent, so it never followed [data-theme=dark]. */
.steps .bar.done { background: var(--color-primary); }
:root[data-theme='dark'] .steps .bar.done { background: var(--color-primary-dark); }

/* ---------------------------------------------------------------- panes */

main { flex: 1; padding: 22px 24px 32px; }

.pane { display: none; }
.pane.on { display: block; }

.eyebrow {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin-bottom: 8px;
}
.title { font-size: 26px; font-weight: 640; margin-bottom: 6px; line-height: 1.2; }
.sub { color: var(--ink-soft); font-size: 14px; margin-bottom: 28px; max-width: 46ch; }

/* ------------------------------------------------------------- services */

.group-label {
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin: 28px 0 10px;
}
.group-label:first-of-type { margin-top: 0; }

.card-list { display: flex; flex-direction: column; gap: 6px; }

/* Practitioner grid — the entry, step 1 of the funnel (2026-08-27). Row
   cards (avatar-left, text-right) instead of stacked photo-on-top tiles:
   with no headshot files existing for any demo tenant, a full-width square
   image slot was dead weight that made a 17-practitioner clinic
   (?clinic=demo) a long scroll of near-empty tiles. A row card's height is
   driven by two short text lines, not a reserved photo box, so it scans at
   speed with or without a future photo (object-fit: cover keeps a real
   photo looking correct in the same 44px slot).

   Fixed 2026-08-27 (measured live, 1440x900): the previous breakout
   (position:relative; left:50%; translateX(-50%)) recentered the grid on
   the FULL VIEWPORT, not on the .shell/main content column the heading,
   step label, and simband live in — main measured left:411 right:1029
   width:618 while the grid measured left:110 right:1330 width:1220, a
   ~300px overhang on both sides that made the cards visually disagree
   with the text introducing them. Fix stays inside the existing 618px
   column (no breakout, no width/position override) and adds columns by
   narrowing cards instead of widening the grid — still clears 17
   practitioners well inside one screen at every width.

   Fixed-geometry rebuild (2026-08-31, Chris rejecting the row-card build:
   "i dont like the spacing here on this page with the cards and the
   services they offer. its not unison across the board with a fixed
   locaion for profile pic and name and service"). The row-card's
   `align-items: center` vertically centred the 44px avatar against
   whatever the 2-line name box's actual content height was — a 1-line
   name centred the photo higher than a 2-line name did, and the role
   line inherited that same drift underneath it. Every element now sits
   on an explicit grid slot instead of being centred against its sibling:
   photo fixed at `align-self: start` in column 1, name/role stacked in
   column 2 with their own reserved 2-line boxes (unchanged from the prior
   pass — see the comment on `.prac-name` below). Because every slot's
   height is now fixed by construction (not by content), every card in
   the grid lands on the identical total height without needing a
   hard-coded row px that would fight the clinic's own font-size overrides
   (`--font-serif`/`--font-rounded` swap family only, never size). Column
   breakpoints: 1-up below 640, 2-up >=640 — all inside the same 618px
   `.shell` frame (unchanged; see the desktop-framed-card comment further
   down), so the per-card width narrows as columns add, never the frame.

   MEASURED, not assumed (agent-browser, 1440x900, fit-for-life with two
   providers carrying real long titles set via the settings overlay):
   a THIRD column was tried first per the original 3-up->=1100 spec, and
   at that width each card's text column is ~93px (572px content width /
   3 cards, minus 24px card padding and the 56+12px avatar+gap). Even the
   SHORTER required string, "Registered Massage Therapist" (Alex Kidd),
   clipped after "Registered / Massage..." — 1.5 lines of content, not 2 —
   and the longer "Registered Massage Therapist & Acupuncturist" (Rui
   Tian) clipped the same way. The 620px `.shell` frame this app commits
   to elsewhere (see the desktop-framed-card comment) makes a 3-up text
   column too narrow for these titles at ANY viewport, so there is no
   min-width where 3-up both fits the frame and clears real title
   lengths — the fix is capping at 2 columns, never a wider breakpoint
   trigger. 2 columns gives ~190px per text column, which clears both
   strings in 2 lines (re-measured, same method, both cards). */
.prac-grid { display: grid; grid-template-columns: 1fr; gap: 8px; align-items: stretch; }
@media (min-width: 640px) {
  .prac-grid { grid-template-columns: repeat(2, 1fr); }
}
/* Desktop columns (2026-08-31, `.shell` widens to 1120px at >=1024 — see
   the desktop section at the bottom of this file). 3-up at >=1024, 4-up at
   >=1360, both still inside the same 1120px `.shell` cap (the shell does
   NOT grow past 1360 — the extra viewport just gives 4 narrower cards the
   same total 1120px frame). `minmax(240px, 1fr)` is a measured floor, not
   a guess: at >=1360 with the 1120px cap, 4 columns average ~262px/card
   (~170px text column after the 56px avatar+12px gap+24px padding), which
   is tighter than the 2-column ~190px text column that was the original
   fixed-geometry measurement — MEASURED via agent-browser against
   fit-for-life's two longest real titles ("Registered Massage Therapist &
   Osteopathic Manual Practitioner" et al.) before shipping; the floor
   stops a card from shrinking past what 2-line clamp needs if a future
   clinic's title runs longer than today's fixtures. */
/* B167 (2026-09-27, WALK-booking): the 4-up grid inside the 1120px shell
   left the 4th column dead in 6 of 8 groups. At >=1024 every step now
   sits in the one 640px column (desktop section below), and the grid is
   3-up inside it — no 4-up at any width. */
@media (min-width: 1024px) {
  .prac-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

.prac {
  display: grid;
  grid-template-columns: 56px 1fr;
  column-gap: 12px;
  align-items: start;
  text-align: left;
  padding: 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  background: var(--ground);
  cursor: pointer;
  transition: border-color 0.12s ease, background-color 0.12s ease;
}
.prac:hover { border-color: var(--accent); background: var(--accent-soft); color: var(--accent-soft-ink); }
.prac:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

.prac-avatar {
  width: 56px;
  height: 56px;
  align-self: start;
  border-radius: var(--radius-card);
  object-fit: cover;
  background: var(--surface-2);
  display: block;
}
.prac-meta { min-width: 0; width: 100%; align-self: start; }
/* Every card the SAME height regardless of name length or a long combined
   role string ("RMT & Osteopathic Manual Practitioner" — Firouzeh Nassiri,
   fit-for-life). `min-height` reserves the full 2-line box for both name
   and role whether or not the actual text wraps that far, so a
   1-line-name/1-line-role card and a 2-line-name/2-line-role card commit
   to the identical reserved height — text past 2 lines clips with an
   ellipsis, full string always available via the element's `title` attr
   (set in public/app.js). A 1-line name leaves its own second line empty
   rather than growing/shrinking, which is what pins the role slot below
   it to the same y-position on every card (the fixed-geometry ask). */
/* 2026-08-31 (Chris: "i dont like the whitespace between the dr name and
   their title"). The 2-line reserved name box left a full empty line
   above the role whenever a name was one line — the name is now a
   SINGLE line (ellipsis + `title` attr carries the full string for
   anything that clips), sitting a fixed 3px gap above the role. The role
   keeps its own fixed 2-line box unchanged, so a 1-line role's leftover
   whitespace lands at the BOTTOM of the card, never between name and
   role. Cards stay equal height across the grid: name's line-height is
   now fixed (single line, not a reserved 2-line box), and the role box
   was already fixed — nothing content-driven is left in the meta column. */
.prac-name {
  font-weight: 600;
  font-size: 14.5px;
  line-height: 1.3;
  letter-spacing: -0.01em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.prac-disc {
  font-size: 12.5px;
  line-height: 1.3;
  color: var(--ink-soft);
  margin-top: 3px;
}

/* Practitioner profile — identity block (avatar next to name/discipline),
   an OPTIONAL media-only cover band above it, bio, then services (or the
   degrade browse into the pre-existing discipline tiles below).

   Rebuilt 2026-09-01 (Chris, looking at a flat grey box with the clinic's
   name in it: "every page keeps getting messed up and out of whack. I
   want the 'fit for life header' to be a video / placeholder if we dont
   have the video"). Every profile now renders the SAME banner-shaped band
   (`.prof-banner`) regardless of what fills it — video, image, or a
   designed placeholder, resolved by `headerBandNode()` in app.js. This
   retires the old split between a click-to-play `.prof-cover` media band
   and a name-beside-avatar `.prof-heading` for providers without media —
   that split is exactly what let the flat "clinic name in a box"
   typographic fallback exist; deleting the split deletes the fallback.

   `.has-cover` on #pane-service hides the pane's own eyebrow/H1/sub — the
   heading block below renders the SAME name/discipline itself (with or
   without a cover band above it), so leaving the static title visible
   would double the identity AND its reserved margins would eat into the
   fit-to-screen budget even emptied to ''. */
.has-cover .eyebrow,
.has-cover #t-service,
.has-cover #service-sub {
  display: none;
}

/* The band breaks out of main's 24px SIDE padding only — symmetric,
   self-contained inside main's own box (never viewport-relative). This is
   deliberately NOT the 100vw/left:50%/translateX(-50%) trick: that one
   recentered on the full viewport instead of the .shell content column
   and overflowed asymmetrically (see the .prac-grid comment above — the
   bug Chris caught once already on this same funnel). A margin/width
   pair scoped to main's own padding can never disagree with anything
   outside main, so nothing below the band is at risk of the same defect.
   The -22px top margin only cancels main's own 22px top padding — it's
   neutralized below when the "simulated demonstration" `.simband` is
   prepended ahead of `#pane-service`, so the band respects simband's own
   margin instead of reaching past it for main's edge. */
.prof-name-wrap { min-width: 0; }
.prof-name { font-size: 21px; font-weight: 640; letter-spacing: -0.018em; line-height: 1.2; }
.prof-disc { font-size: 13px; color: var(--ink-soft); margin-top: 3px; }

.prof-bio { color: var(--ink-soft); font-size: 14px; line-height: 1.5; max-width: 52ch; margin: 0 0 18px; }

/* Header band (rebuilt 2026-09-01, `headerBandNode()` in app.js) — video,
   image, or designed placeholder all render into this same shape.
   `overflow: visible` so the headshot can overlap the band's own bottom
   edge instead of sitting inside it. 16:9 is the mobile ratio. On the
   single-column desktop layout the band now spans the FULL shell width
   (up to ~1120px), so 16:9 would run ~630px tall and push the name below
   the fold (Chris, 2026-09-01, looking at the 1440px screenshot) — the
   >=1024 override below trades that for a wide, short hero band instead:
   16:5 capped at 320px tall. */
.prof-banner {
  position: relative;
  margin: -22px -24px 40px;
  width: calc(100% + 48px);
  aspect-ratio: 16 / 9;
  background: var(--surface-2);
  overflow: visible;
}
@media (min-width: 1024px) {
  .prof-banner {
    aspect-ratio: 16 / 5;
    max-height: 320px;
  }
}
.simband + #pane-service.has-cover #service-list .prof-banner:first-child {
  margin-top: 0;
}
/* look.sectionOrder = ['welcome','banner'] (P4 live editor, 2026-09-14) —
   the clinic banner band renders AFTER #service-welcome instead of as
   #pane-service's first child (app.js's syncClinicBannerBand). The -22px
   top margin above only makes sense cancelling main's own top padding
   when the band IS the pane's first paint; in this position it sits below
   real text instead, so it gets a normal positive gap under that text —
   same full-bleed side margins/bottom margin either way. */
#service-welcome ~ #clinic-banner-band.prof-banner {
  margin-top: 24px;
}
/* Video/iframe fill: object-fit: cover — unlike the retired `.prof-cover`
   (contain, to protect a practitioner's own uncontrolled source), the
   media reaching this band is either a clinic-composed banner/video or
   the designed placeholder, so a full-bleed fill reads as intentional
   rather than a crop risk. Visible focus ring since both are keyboard-
   reachable interactive media (video controls / iframe). */
.prof-banner-media {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  border: 0;
  border-bottom: 1px solid var(--line);
}
.prof-banner-media:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}
/* No-header identity row (2026-09-08, Chris to the clinic: "if there is no
   video it can be no header at all") — a provider with neither a video nor
   a `bannerUrl` gets NO band: no gradient, no monogram placeholder, no
   reserved height (that old designed placeholder is retired along with
   its fallback path in app.js). This is the layout that runs instead: a
   plain avatar-beside-name row, no overlap trick, no absolute positioning. */
.prof-identity-row {
  display: flex;
  align-items: center;
  gap: 14px;
  margin: 0 0 18px;
}
.prof-identity-avatar {
  width: 56px;
  height: 56px;
  border-radius: 50%;
  object-fit: cover;
  background: var(--surface-2);
  display: block;
}
.prof-identity-avatar.avatar-mono { font-size: 19px; }

/* Single-column profile, desktop included (2026-09-01 — Chris, looking at
   the old 58/42 split: "the services shouldnt be listed beside a HEADER.
   They should be listed below in a list."). `.prof-cols`/`.prof-col-main`/
   `.prof-col-side` are still built by renderPractitionerProfile() in
   app.js, but stay `display: contents` at every width now — no grid, no
   sticky side column. DOM order (identity -> quote -> bio -> facts ->
   focus -> services) IS the render order at every breakpoint: header band
   full width, then identity/bio, then "Services they offer" full width
   below, one row per service (the existing `.svc` rows). The former
   >=1024px 58/42 grid + 16:6 desktop banner override is retired along
   with it — the band keeps its mobile 16:9 full-bleed shape at every
   width, since it now spans the whole content column regardless of
   viewport. */
.prof-cols,
.prof-col-main,
.prof-col-side {
  display: contents;
}
.prof-banner-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-bottom: 1px solid var(--line);
}
.prof-banner-avatar {
  position: absolute;
  left: 24px;
  bottom: -36px;
  z-index: 1;
  width: 72px;
  height: 72px;
  border-radius: 50%;
  object-fit: cover;
  background: var(--surface-2);
  border: 3px solid var(--ground);
  box-shadow: var(--shadow-lift);
  display: block;
}
.prof-banner-avatar.avatar-mono { font-size: 24px; }

/* Clinic banner plate (P1 storefront, 2026-09-14, Control 1) — the clinic
   logo (or a colour-dot + name fallback when no logo is uploaded, same
   "monogram/initials fallback" discipline `.prof-banner-avatar.avatar-mono`
   already uses) on a white plate overlapping the CLINIC banner's own
   bottom-left edge, the same overlap trick `.prof-banner-avatar` uses on a
   practitioner's own banner. A pill, not a circle — this is a name+mark
   identity chip, not a headshot. */
.clinic-plate {
  position: absolute;
  left: 24px;
  bottom: -22px;
  z-index: 1;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 16px 8px 10px;
  border-radius: 999px;
  background: var(--ground);
  border: 1px solid var(--line);
  box-shadow: var(--shadow-lift);
  max-width: calc(100% - 48px);
}
/* B281 (round 2, comp A "the keyline sign"): an uploaded logo hangs on the
   plate alone, at sign size in its own shape — 40px tall, width from its
   aspect, never a round crop (a 300x68 wordmark in the old 28px circle was
   a speck). The plate squares off to the card radius around it; app.js
   leaves the typed name off (the logo's alt carries it). No logo = the pill
   above, as is. A printed sign stays printed: ONE white paper stock in
   every theme (the theme-following --ground turned a dark-ink wordmark
   invisible in dark mode), never chosen from the logo's pixels. */
.clinic-plate:has(> .clinic-plate-logo) {
  padding: 8px 12px;
  border-radius: var(--radius-card);
  background: var(--color-ground);
  border-color: var(--color-line);
}
/* The keyline (filters built in app.js beside the logo): a sharp hairline,
   one device pixel, in --color-ink-soft just outside the logo's thin ink,
   so a white wordmark reads as outlined letters on the white paper. No
   blur, so the logo is as crisp as the top bar's at any density; and no
   edge round a solid ground (card, tile, opaque PNG/JPG, photo), so no box
   inside the plate's box. The logo's colours are never altered. One filter
   per density; the media queries below pick the one whose hairline is one
   device pixel on this screen (they re-match live on a zoom or monitor
   change). */
.clinic-plate-logo {
  width: auto;
  height: 40px;
  max-width: 100%;
  object-fit: contain;
  display: block;
  flex: none;
  filter: url(#clinic-plate-keyline-1x);
}
@media (min-resolution: 1.5dppx), (-webkit-min-device-pixel-ratio: 1.5) {
  .clinic-plate-logo { filter: url(#clinic-plate-keyline-2x); }
}
@media (min-resolution: 2.5dppx), (-webkit-min-device-pixel-ratio: 2.5) {
  .clinic-plate-logo { filter: url(#clinic-plate-keyline-3x); }
}
/* The filters' holder: 0x0 and out of the plate's flow (no gap, no size);
   its colour is the keyline's ink. */
.clinic-plate-keyline {
  position: absolute;
  width: 0;
  height: 0;
  color: var(--color-ink-soft);
}
.clinic-plate-dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--accent);
  flex: none;
}
.clinic-plate-name {
  font-size: 13.5px;
  font-weight: 650;
  color: var(--ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.prof-name-wrap-banner { margin: 0 0 12px; }
.prof-name-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.prof-credentials { font-size: 12.5px; color: var(--ink-faint); margin-top: 2px; }

/* One-line pull quote (2026-08-31, `provider.quote`) — accent-ink italic,
   the one spot on this page allowed to carry the clinic's accent as text
   color rather than only borders/backgrounds. */
.prof-quote { font-style: italic; color: var(--accent-ink); font-size: 14.5px; line-height: 1.5; margin: 0 0 14px; }

/* Compact facts row (2026-08-31) — years in practice / languages /
   education, joined with a middle dot, only the facts actually present. */
.prof-facts { font-size: 13px; color: var(--ink-soft); margin: 0 0 14px; font-variant-numeric: tabular-nums; }

/* Focus-area chips (2026-08-31) — reuses `.tag`, the SAME classification-
   pill class the pronouns tag and the calendar band legend already use;
   this app has no separate chip/badge component and none is needed. */
.prof-focus { display: flex; flex-wrap: wrap; gap: 6px; margin: 0 0 18px; }

/* Social links row (2026-09-08, approved comp card-socials-v1) — the kit's
   own quiet row: monochrome `--ink-soft` 20px glyphs in 44x44 tap targets,
   no chrome, no brand colour, no labels. Sits last in the profile text
   block. Absent entirely when the provider has no `socials` — no
   placeholder, no reserved height. Tokens/rules copied verbatim from the
   comp: `--surface-2` hover tile, `.backbtn:focus-visible`'s focus ring. */
/* Spacing fix (2026-09-08, review of dbce6e1 — sat too close to the
   SERVICES label). Margin collapse math against the fixed neighbours
   (`.prof-bio`'s 18px bottom margin above, `.group-label`'s 28px top
   margin below): top -2px collapses with bio's 18px to the comp's
   measured 16px bio->glyph gap; bottom 30px (a positive margin always
   wins the collapse over the label's 28px) hits the comp's measured 30px
   glyph->label gap exactly. */
.prof-links { display: flex; flex-wrap: wrap; gap: 2px; margin: -2px 0 30px -12px; }
.prof-link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  color: var(--ink-soft);
  border-radius: var(--radius-sm);
  transition: color 0.12s ease, background-color 0.12s ease;
}
.prof-link svg { width: 20px; height: 20px; display: block; }
.prof-link:hover { color: var(--ink); background: var(--surface-2); }
.prof-link:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; color: var(--ink); }
@media (prefers-reduced-motion: reduce) { .prof-link { transition: none; } }

/* Media -> headshot -> monogram (avatarNode, public/app.js) — used by the
   practitioner grid (`.prac-avatar`) and the profile header's overlapping
   headshot (`.prof-banner-avatar`). Declared AFTER those classes on
   purpose: same specificity, so source order is what makes this
   fallback's background/display/alignment win over the sizing rules
   above it, while still inheriting their width/height/margin (never
   redeclared here). */
.avatar-mono {
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 640;
  letter-spacing: 0.02em;
  color: var(--accent-soft-ink);
  background: var(--accent-soft);
  border: 1px solid var(--line);
}
.prac-avatar.avatar-mono { font-size: 15px; }

/* Discipline tiles — the funnel's degrade path now (2026-08-27; was step 1
   before practitioner-first). Tap-first: the whole tile advances. */
.cat-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
@media (max-width: 380px) { .cat-grid { grid-template-columns: 1fr; } }

.cat {
  text-align: left;
  padding: 16px;
  min-height: 84px;
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  background: var(--ground);
  cursor: pointer;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: 6px;
  transition: border-color 0.12s ease, background-color 0.12s ease;
}
.cat:hover { border-color: var(--accent); background: var(--accent-soft); color: var(--accent-soft-ink); }
.cat:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.cat-name { font-weight: 600; font-size: 15px; line-height: 1.3; letter-spacing: -0.01em; }
.cat-meta { font-size: 12.5px; color: var(--ink-soft); font-variant-numeric: tabular-nums; }

.svc {
  display: flex;
  align-items: center;
  gap: 16px;
  width: 100%;
  text-align: left;
  padding: 14px 16px;
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  background: var(--ground);
  cursor: pointer;
  transition: border-color 0.12s ease, background-color 0.12s ease;
}
.svc:hover { border-color: var(--line-strong); background: var(--surface); }
.svc:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.svc.on { border-color: var(--accent); background: var(--accent-soft); color: var(--accent-soft-ink); }

.svc-main { flex: 1; min-width: 0; }
.svc-name { font-weight: 550; font-size: 15px; line-height: 1.35; }
.svc-meta { font-size: 13px; color: var(--ink-soft); margin-top: 2px; font-variant-numeric: tabular-nums; }
/* CFGSVC1 judge fix (sheet gap): the price segment of the service card's
   meta and the time step's line never breaks, so "$140" stays on the
   "30 minutes" line (app.js svcCard / paintTimeSub). */
.price-seg { white-space: nowrap; }
.svc-chev { color: var(--ink-faint); flex: none; line-height: 0; align-self: center; }

/* A card with a description grows taller than its chevron, so it can no
   longer center-align the row (the chevron would float mid-paragraph) —
   `.has-desc` switches the row to top-align instead; the chevron's own
   align-self keeps it visually centered against the name/meta line only
   when no description is present (the common case, unchanged). Mirrors
   `.prof-bio`'s restrained typography scale (styles.css ~L467) — same
   font-size/line-height/color, so a service description reads as the same
   register as a practitioner bio. */
.svc.has-desc { align-items: flex-start; }
.svc-desc { font-size: 13px; color: var(--ink-soft); line-height: 1.5; margin: 6px 0 0; white-space: pre-line; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }

/* Classification tag — carries a fact, never decoration. */
.tag {
  display: inline-block;
  font-size: 12px;
  font-weight: 550;
  padding: 2px 7px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm);
  color: var(--ink-soft);
  background: var(--ground);
  white-space: nowrap;
}
.tag.warn { color: var(--warn); border-color: color-mix(in srgb, var(--warn) 35%, transparent); background: var(--warn-soft); }

/* ------------------------------------------------------------- calendar */

.cal { margin-bottom: 18px; }

/* Step 2 must fit calendar + times on one screen; its header is tighter. */
#pane-time .sub { margin-bottom: 14px; }

.cal-head {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 10px;
}
.cal-month {
  flex: 1;
  font-size: 16px;
  font-weight: 600;
  letter-spacing: -0.015em;
}
.cal-nav {
  border: 1px solid var(--line-strong);
  background: var(--ground);
  color: var(--ink-soft);
  border-radius: var(--radius-control);
  width: 34px;
  height: 34px;
  display: grid;
  place-items: center;
  cursor: pointer;
  flex: none;
  line-height: 0;
}
.cal-nav:hover:not(:disabled) { background: var(--surface-2); color: var(--ink); }
.cal-nav:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.cal-nav:disabled { opacity: 0.3; cursor: not-allowed; }

.cal-dow, .cal-grid {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 3px;
}
.cal-dow {
  margin-bottom: 4px;
}
.cal-dow span {
  text-align: center;
  font-size: 11px;
  font-weight: 550;
  color: var(--ink-faint);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.cal-pad { display: block; }

.cal-day {
  position: relative;
  /* Booksy-compact: fixed short rows so the whole month AND the day's times
     share one screen. Square tiles pushed the times below the fold. */
  height: 38px;
  width: 100%;
  border: 1px solid transparent;
  border-radius: var(--radius-control);
  background: transparent;
  color: var(--ink);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  padding: 0;
  font: inherit;
  transition: border-color 0.12s ease, background-color 0.12s ease;
}
.cal-num { font-size: 13.5px; font-weight: 550; font-variant-numeric: tabular-nums; line-height: 1; }

/* The availability band. Colour is never the only signal — a day that cannot
   be picked is also not a button and carries aria-disabled. */
.cal-bar, .cal-legend-swatch {
  width: 16px;
  height: 3px;
  border-radius: 2px;
}
.cal-bar { background: var(--accent); }
.cal-day.limited .cal-bar { background: var(--band-limited); }
.cal-day.full .cal-bar { background: var(--band-full); }

/* Legend swatches (CALLEGEND1, 2026-09-15) — each one pulls its colour from
   the exact same custom property as the band it stands for, so `.cal-key`
   below can never say a colour word that drifts from what the grid paints.
   `openings` carries no modifier: it's the same unmodified `--accent` the
   default (open) `.cal-bar` above already uses. `away` has no `.cal-bar` at
   all (see `.cal-day.away` below), so its swatch derives from that rule's
   own background token instead of a band token. */
.cal-legend-swatch {
  display: inline-block;
  vertical-align: middle;
  margin-right: 4px;
  background: var(--accent);
}
.cal-legend-swatch--limited { background: var(--band-limited); }
.cal-legend-swatch--full { background: var(--band-full); }
.cal-legend-swatch--away { background: var(--surface-2); }
.cal-legend-item { white-space: nowrap; }

button.cal-day { cursor: pointer; background: var(--surface); border-color: var(--line); }
button.cal-day:hover { border-color: var(--accent); background: var(--accent-soft); color: var(--accent-soft-ink); }
button.cal-day:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* A full day is a door to the waitlist, so it stays a button — but its number
   is muted so the eye still reads the month by where the room is. */
button.cal-day.full { color: var(--ink-soft); }
button.cal-day.full:hover { border-color: var(--band-full); background: var(--danger-soft); }

.cal-day.on { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
.cal-day.on .cal-bar { background: var(--on-accent); opacity: 0.8; }
/* B310 — a picked full day reads like every other picked day. `button.cal-day.full`
   (0,2,1) outranks `.cal-day.on` (0,2,0), so without this its number stayed
   --ink-soft on the green, and its hover repainted to --danger-soft. */
button.cal-day.full.on { color: var(--on-accent); }
button.cal-day.full.on:hover { border-color: var(--accent); background: var(--accent); }

/* Closed, past, beyond — visible for orientation, plainly not choosable. */
.cal-day.closed { color: var(--ink-faint); }
.cal-day.past, .cal-day.beyond { color: var(--ink-faint); opacity: 0.4; }

/* Away — a pinned practitioner on time off (or every confirmed practitioner
   away). Distinct from closed: closed means the clinic doesn't run that day,
   away means people just aren't in — so it gets its own filled grey tile
   rather than reading as identical to "clinic shut". Not a button (see
   drawMonth): there is no waitlist door for a day nobody works. */
.cal-day.away { color: var(--ink-faint); background: var(--surface-2); }

.cal-day.loading .cal-num { opacity: 0.25; }
.cal-day.loading { background: var(--surface); border-color: var(--line); }

.cal-key {
  font-size: 12px;
  color: var(--ink-faint);
  margin-top: 8px;
  line-height: 1.45;
}

/* PW audit item 5 — the clinic's cancellation policy, under the slot
   picker and on the review step. Same quiet register as .cal-key/.hint
   (never a colored notice — this isn't a warning, it's fine print), set
   apart with a hairline divider so it reads as its own line, not a run-on
   of whatever sits above it. Empty text collapses the divider too, so a
   not-yet-booted page never flashes a bare rule with nothing under it. */
.policy-note {
  font-size: 12px;
  color: var(--ink-faint);
  line-height: 1.45;
  margin-top: 14px;
  padding-top: 14px;
  border-top: 1px solid var(--line);
}
.policy-note:empty { display: none; }

/* --------------------------------------------------------------- slots */

.prov-block { margin-bottom: 26px; }
.prov-block:last-child { margin-bottom: 0; }
/* Defect 4(c) (2026-09-10) — name, discipline, and count used to compete
   in one flex row with no wrap plan: a long name ("Luciano Di Loreto")
   crushed against the count and wrapped mid-name, with the discipline text
   wrapping under both. The discipline line now always takes its own row
   (flex-basis: 100%, pushed to `order: 3`) so name+count share a short,
   uncontested first row instead. */
.prov-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  column-gap: 10px;
  row-gap: 4px;
  margin-bottom: 10px;
  padding-bottom: 8px;
  border-bottom: 1px solid var(--line);
}
.prov-name {
  font-weight: 570;
  font-size: 14px;
  flex: 1 1 auto;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.prov-disc { font-size: 13px; color: var(--ink-faint); flex-basis: 100%; order: 3; }
.prov-count { margin-left: auto; font-size: 12px; color: var(--ink-faint); font-variant-numeric: tabular-nums; flex: 0 0 auto; white-space: nowrap; }
/* CFGSVC1 judge fix (T-37): a practitioner's own price, when it differs
   from the clinic price, in the count's style just before it. */
.prov-price { margin-left: auto; font-size: 12px; color: var(--ink-faint); font-variant-numeric: tabular-nums; flex: 0 0 auto; white-space: nowrap; }
.prov-price + .prov-count { margin-left: 0; }

.slots { display: flex; flex-wrap: wrap; gap: 6px; }
.slot {
  padding: 8px 13px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-control);
  background: var(--ground);
  font-size: 14px;
  font-weight: 500;
  cursor: pointer;
  font-variant-numeric: tabular-nums;
  transition: border-color 0.12s ease, background-color 0.12s ease;
}
.slot:hover { border-color: var(--accent); background: var(--accent-soft); color: var(--accent-soft-ink); }
.slot:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.slot.on { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }

/* --------------------------------------------------------------- forms */

.field { margin-bottom: 16px; }
.field label {
  display: block;
  font-size: 13px;
  font-weight: 550;
  margin-bottom: 6px;
  color: var(--ink);
}
.field input, .field textarea, .field select {
  width: 100%;
  padding: 10px 12px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-control);
  background: var(--ground);
  color: var(--ink);
  font: inherit;
  font-family: var(--font-control);
  font-size: 15px;
}
.field input::placeholder, .field textarea::placeholder { color: var(--ink-faint); }
.field input:focus, .field textarea:focus, .field select:focus {
  outline: 2px solid var(--accent);
  outline-offset: -1px;
  border-color: var(--accent);
}
.field textarea { min-height: 72px; resize: vertical; }
.field .hint { font-size: 12px; color: var(--ink-faint); margin-top: 5px; }
.field.err input, .field.err textarea, .field.err select {
  border-color: var(--danger);
}
.field.err input:focus, .field.err textarea:focus, .field.err select:focus {
  outline-color: var(--danger);
  border-color: var(--danger);
}
.field .field-error {
  font-size: 12px;
  font-weight: 550;
  color: var(--danger);
  margin-top: 5px;
}
/* Money-row affix layout (2026-09-03, "Value of one visit") — a `$` prefix
 * and a read-only currency `.tag` bracket the editable number input, so the
 * currency reads as identity metadata (derived, never desk-writable) rather
 * than a second input. Reuses `.field`'s existing input/hint/error chrome —
 * this only replaces the plain `width:100%` input with a flex row. */
.field .money-row { display: flex; align-items: center; gap: 8px; }
.field .money-row .money-affix { font-size: 15px; color: var(--ink-soft); flex: none; }
.field .money-row input { width: auto; flex: 1; min-width: 0; }
.field .money-row .tag { flex: none; }
.row2 { display: grid; grid-template-columns: 1fr 1fr; gap: 0 14px; }
@media (max-width: 440px) { .row2 { grid-template-columns: 1fr; } }

/* Step-3 vertical rhythm: the patient-type toggle, the identity block, and
   the visit-details note are three distinct sections, not one undifferentiated
   stack of fields — a consistent, generous gap between them (not just one
   accidental gap before the footer) reads as intentional at both a 430px
   phone and a 1440px desktop frame. Each section keeps its own tight internal
   .field spacing; only the SPACE BETWEEN sections grows. */
.details-body { display: flex; flex-direction: column; gap: 30px; }
.details-body > .seg { margin-bottom: 0; }
.details-section { padding-top: 2px; }
.details-section .group-label { margin: 0 0 12px; }
@media (min-width: 720px) {
  .details-body { gap: 36px; padding-top: 4px; }
}

.seg {
  display: flex;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-control);
  overflow: hidden;
  margin-bottom: 22px;
}
.seg button {
  flex: 1;
  padding: 10px 8px;
  border: 0;
  background: var(--ground);
  cursor: pointer;
  font-size: 14px;
  font-weight: 530;
  color: var(--ink-soft);
}
.seg button + button { border-left: 1px solid var(--line-strong); }
/* B168: a selected segment is a chip-selected state (primary-soft ground,
   primary-ink text), never the primary's own fill — green fill belongs to
   the one primary action on the screen. */
.seg button.on { background: var(--color-primary-soft); color: var(--color-primary-ink); }
:root[data-theme='dark'] .seg button.on { background: var(--color-primary-soft-dark); color: var(--color-primary-ink-dark); }
.seg button:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

/* --------------------------------------------------------- simulated sms */
/* Demo-only "your phone" panel: shows the code/confirmation texts arriving
   as received SMS bubbles, reusing the exact dryrun message bodies the
   server actually generated (never a second, invented copy). Deliberately
   plain — a quiet phone-frame card, not a decorative device mockup. */
.pf-sim {
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  background: var(--surface);
  padding: 14px;
  margin-bottom: 16px;
}
.pf-sim-head {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin-bottom: 12px;
}
.pf-sim-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--accent);
  flex: none;
}
.pf-sim-thread { display: flex; flex-direction: column; gap: 8px; }
.pf-sim-msg {
  align-self: flex-start;
  max-width: 88%;
  padding: 9px 12px;
  border-radius: 14px;
  border-bottom-left-radius: 4px;
  background: var(--surface-2);
  color: var(--ink);
  font-size: 13.5px;
  line-height: 1.45;
  animation: pf-sim-in 0.18s ease-out;
}
.pf-sim-msg .pf-sim-time {
  display: block;
  margin-top: 4px;
  font-size: 10.5px;
  font-weight: 500;
  color: var(--ink-faint);
}
@keyframes pf-sim-in {
  from { opacity: 0; transform: translateY(3px); }
  to { opacity: 1; transform: translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
  .pf-sim-msg { animation: none; }
}
.pf-fill {
  display: block;
  margin-top: 8px;
}

/* -------------------------------------------------------------- review */

.summary {
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  background: var(--surface);
  padding: 4px 16px;
  margin-bottom: 20px;
}
.summary .r {
  display: flex;
  justify-content: space-between;
  gap: 20px;
  padding: 11px 0;
  font-size: 14px;
}
.summary .r + .r { border-top: 1px solid var(--line); }
.summary .k { color: var(--ink-soft); flex: none; }
.summary .v { font-weight: 550; text-align: right; }
/* P2 named variants (2026-09-14) — the Service row's parent-OSCAR-type
   sub-line, review + done screens (public/app.js's parentServiceLabel). */
.summary .v-parent { display: block; font-size: 12.5px; font-weight: 400; color: var(--ink-soft); text-align: right; margin-top: 2px; }

.notice {
  border: 1px solid var(--line);
  background: var(--surface);
  border-radius: var(--radius-sm);
  padding: 12px 14px;
  font-size: 13px;
  color: var(--ink-soft);
  margin-bottom: 18px;
}
.notice.warn { background: var(--warn-soft); border-color: color-mix(in srgb, var(--warn) 28%, transparent); color: var(--warn); }
.notice.err { background: var(--danger-soft); border-color: color-mix(in srgb, var(--danger) 28%, transparent); color: var(--danger); }
.notice strong { font-weight: 640; }
.notice .rb-btn { border: 0; background: none; padding: 0; color: inherit; font: inherit;
  font-weight: 640; text-decoration: underline; cursor: pointer; flex: none; }

/* Simulation tint band — quiet, not a warning. Sits above the booking card
   for a demo tenant. Uses the tenant's own accent-soft/accent-ink tokens so
   it never reads as an alarm and holds contrast in both themes. */
/* B167: banner text on the booking scale (14px, the .sub stop) and
   "← Back to overview" as its own 44px-tall row under it (15/600, the
   booking-label stop) — the old inline link was a 15px-tall tap box. */
.simband {
  background: var(--accent-soft);
  color: var(--accent-soft-ink);
  border-radius: var(--radius-sm);
  padding: 10px 14px 0;
  font-size: 14px;
  font-weight: 400;
  line-height: 1.5;
  margin-bottom: 18px;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}
.simband-back {
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 2px;
  font-size: 15px;
  font-weight: 600;
  white-space: nowrap;
  display: inline-flex;
  align-items: center;
  min-height: 44px;
}
/* B169: dark is its own palette — the banner was a light primary-soft slab on
   the dark page (the clinic's inline --accent-soft never swaps). */
:root[data-theme='dark'] .simband { background: var(--color-primary-soft-dark); color: var(--color-ink-dark); }
.simband-back:hover,
.simband-back:focus-visible {
  text-decoration-thickness: 2px;
}
.simband-back:focus-visible {
  outline: 2px solid currentColor;
  outline-offset: 2px;
  border-radius: 2px;
}

/* ---------------------------------------------------------------- cta */

/* In-flow, not pinned (2026-09-01 fix): the bar sits directly under the
   content of the current step with normal bottom padding, never floating
   fixed over a viewport taller than the content — see the .shell comment
   above. Visual style (background/border/padding) is unchanged. */
.footer {
  background: var(--ground);
  border-top: 1px solid var(--line);
  padding: 14px 24px calc(14px + env(safe-area-inset-bottom));
}
.legalfoot {
  max-width: 620px;
  margin: 0 auto;
  padding: 0 24px env(safe-area-inset-bottom);
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 8px;
  font-size: 12.5px;
  color: var(--ink-faint);
}
/* B167: each legal link is a 44px tap box (was 19px tall). */
.legalfoot a {
  color: var(--ink-faint);
  text-decoration: underline;
  text-underline-offset: 2px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  min-width: 44px;
}
.legalfoot a:hover, .legalfoot a:focus-visible { color: var(--ink-soft); }
/* FOOTER1 (2026-10-06): the owner line takes its own row under the legal
   links, same muted text; its bottom margin matches the space under the
   links' text inside their 44px boxes. */
.legalfoot { flex-wrap: wrap; }
.legalfoot .legalfoot-owner { flex-basis: 100%; text-align: center; margin-bottom: 12px; }
.footer-inner {
  max-width: 620px;
  margin: 0 auto;
  display: flex;
  gap: 12px;
  align-items: center;
}

.cta {
  flex: 1;
  padding: 12px 20px;
  border: 1px solid var(--accent);
  border-radius: var(--radius-control);
  background: var(--accent);
  color: var(--on-accent);
  font-size: 15px;
  font-weight: 570;
  cursor: pointer;
  transition: background-color 0.12s ease;
}
.cta:hover:not(:disabled) { background: var(--accent-ink); border-color: var(--accent-ink); }
.cta:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.cta:disabled { opacity: 0.38; cursor: not-allowed; }
/* B168: the footer's disabled primary keeps full ink on a quiet grey
   (was white on a 38%-opacity green, ~1.9:1), and #foot-hint under it
   says what unlocks it. */
#btn-primary:disabled { opacity: 1; background: var(--surface-2); border-color: var(--line-strong); color: var(--ink); }
.cta.ghost {
  background: var(--ground);
  color: var(--ink);
  border-color: var(--line-strong);
  flex: none;
  padding: 12px 16px;
}
.cta.ghost:hover:not(:disabled) { background: var(--surface-2); border-color: var(--line-strong); }

/* Inline text action inside a .hint line (e.g. "Use my email instead") — no
   button chrome, just the accent underline the rest of the product already
   uses for links. */
.linklike {
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  color: var(--accent);
  text-decoration: underline;
  cursor: pointer;
}
.linklike:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.footmeta { font-size: 13px; color: var(--ink-soft); line-height: 1.35; }
.footmeta b { display: block; color: var(--ink); font-weight: 570; }
/* HELDWAIT1 — the "still booking" line under the Confirm bar; same type
   and colour as .footmeta, nothing new. Empty = no box, no margin. */
.book-pending { max-width: 620px; margin: 10px auto 0; font-size: 13px; color: var(--ink-soft); line-height: 1.35; }
.book-pending:empty { display: none; }
.foot-hint { max-width: 620px; margin: 8px auto 0; font-size: 13px; color: var(--ink-soft); line-height: 1.35; text-align: center; }
.foot-hint:empty { display: none; }

/* ------------------------------------------------------------- states */

.empty {
  padding: 28px 20px;
  color: var(--ink-soft);
  font-size: 14px;
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  background: var(--surface);
}
.empty b { display: block; color: var(--ink); font-weight: 570; margin-bottom: 4px; font-size: 15px; }
.empty p { margin: 0; line-height: 1.5; }
/* CLINIC CONFIRMED ONLY empty states (2026-09-01) — an optional secondary
   action ("See all services") sits inside the same card, on its own row,
   never full-width-stretched like the footer's `.cta` (no flex parent
   here to give that rule meaning). CFGTIME1 (2026-10-04): `flex` +
   `fit-content`, not `inline-flex` — inline, the button sat on the body's
   last line, touching a short owner line ("…front desk.Join waitlist"). */
.empty .cta { display: flex; width: fit-content; margin-top: 14px; }

.skel {
  height: 58px;
  border-radius: var(--radius-card);
  background: var(--surface);
  border: 1px solid var(--line);
  margin-bottom: 6px;
}

.center { text-align: center; }
.stack { display: flex; flex-direction: column; gap: 10px; }

.done-mark {
  width: 44px;
  height: 44px;
  border: 1px solid var(--accent);
  border-radius: 50%;
  color: var(--accent);
  display: grid;
  place-items: center;
  margin: 8px auto 20px;
  font-size: 20px;
  line-height: 0;
}

/* Defect 1 fix (2026-08-29): an unarmed-real tenant (writeEnabled:false,
   no appointmentNo came back) must never show the success checkmark — a
   real patient reading a green check + "You're booked" believes a slot is
   held when OSCAR holds nothing. Reuses the existing --warn token, same
   discipline as invariant 34's OTP-kill-switch box (no invented tint). */
.done-mark.pending {
  border-color: var(--warn);
  color: var(--warn);
}

/* OPENS3 item 1 (2026-09-19) — add-to-calendar / directions / intake-form
   row on a genuinely confirmed booking's done screen. `.cta.ghost` is the
   existing secondary-button style (see "See all services"); this row just
   wraps and spaces them, ≥44px touch targets already come from `.cta`'s
   own padding. */
.done-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: 16px 0;
}
.done-actions .done-action {
  flex: none;
  text-decoration: none;
  text-align: center;
}
/* B168: "Book another" — a text link, 44px tall, beside the one primary. */
.done-another { display: inline-flex; align-items: center; min-height: 44px; padding: 0 4px; }

code {
  font-family: ui-monospace, 'SF Mono', Menlo, monospace;
  font-size: 12px;
  background: var(--surface-2);
  padding: 1px 5px;
  border-radius: 3px;
}

.payload {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: 13px 14px;
  font-family: ui-monospace, 'SF Mono', Menlo, monospace;
  font-size: 11.5px;
  line-height: 1.6;
  overflow-x: auto;
  white-space: pre;
  color: var(--ink-soft);
  margin-top: 12px;
}

.disclose {
  border-top: 1px solid var(--line);
  margin-top: 18px;
  padding-top: 14px;
}
.disclose summary {
  cursor: pointer;
  font-size: 13px;
  font-weight: 550;
  color: var(--ink-soft);
  list-style: none;
}
.disclose summary::-webkit-details-marker { display: none; }
.disclose summary:hover { color: var(--ink); }
.disclose summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* B171: the record opens as the same label / value sheet as review. */
.disclose .summary { margin: 12px 0 0; }

/* ------------------------------------------------------------- desktop */
/* The flow is one column by design (Booksy register); on a wide screen the
   column becomes a framed card on a tinted page instead of floating loose
   in white space. The in-flow footer keeps the same 620px frame. */
@media (min-width: 720px) {
  body { background: var(--surface); }
  /* B167: the sheet is at least one viewport tall — main's flex:1 takes
     the slack, so the footer sits at the bottom of the screen and no grey
     ground shows under a short step (the profile ended at 560 of 900). */
  .shell {
    border-left: 1px solid var(--line);
    border-right: 1px solid var(--line);
    min-height: 100vh;
    min-height: 100dvh;
  }
  /* .footer is already inside the framed .shell (in-flow, see the .footer
     rule above) so it inherits the shell's left/right border for free —
     no separate fixed-positioning override needed here anymore. */
}

/* --------------------------------------------------------- desktop wide */
/* 2026-08-31, Chris on the 1440-wide live site: "why is this a mobile look
   on a desktop site?" — the 618/620px column above stayed the frame for
   phone AND desktop alike, so a 1440px screen was one narrow ribbon lost
   in empty margins. This block only fires >=1024px; the 620px-frame
   `@media (min-width: 720px)` block above (720-1023, a tablet/narrow-
   desktop band) is untouched, and everything below 720 is untouched.

   Embedded/iframe note: this product has no live iframe embed today —
   `/embed.js` (server.js) renders a plain link-out `<a>` styled with the
   clinic's accent that top-level-navigates to this page; it does not embed
   this page inside an <iframe> on the clinic's own site. That makes the
   "don't fire inside a narrow iframe" requirement automatic rather than
   something to code defensively: `@media` reads the frame's OWN viewport,
   and there is no frame narrower than the real one to misread. If a real
   iframe embed is ever added, nothing here needs to change — a clinic
   site embedding this page at, say, 620px wide would still never cross
   these >=1024 queries, exactly like a narrow browser window today. */
@media (min-width: 1024px) {
  .shell { max-width: 1120px; }

  /* The footer stayed pinned to the old 618px frame (`.footer-inner`'s
     own `max-width: 620px`, set in the 720px band above) even after the
     shell widened — a narrow bar floating centred under a 1120px page reads
     as misaligned, not centred-on-purpose. Matching left/right edges to the
     shell fixes that; letting `.cta`'s existing `flex: 1` stretch to fill
     the new 1120px gap would just trade "too narrow" for "absurdly wide,"
     so button SIZE stays exactly what it is below 1024 (`flex: none` here
     overrides the base `.cta{flex:1}`) and `.footmeta` absorbs the freed
     space instead, right-aligning the button group — the Stripe/Linear
     checkout register TASTE.md anchors on (§1: "same register as Stripe/
     Apple/Tesla/Amazon"): primary action fixed-size and anchored to the
     trailing edge, context copy filling the leading space, never a CTA
     stretched to the width of the page. Applies to every step, including
     details/review — their form is capped at 640 (see above), but the
     footer bar itself still spans the full shell, not the form. */
  /* B167 (2026-09-27, WALK-booking): the footer bar keeps to the same
     640px column as the step above it, so the picked slot and the one
     Continue sit together instead of ~1000px apart across the sheet. */
  .footer-inner { max-width: 640px; }
  .book-pending,
  .foot-hint { max-width: 640px; text-align: right; }
  .footer-inner .footmeta { flex: 1; }
  .footer-inner .cta { flex: none; min-width: 200px; }

  /* B167 (2026-09-27, WALK-booking's one move): EVERY step — the
     practitioner grid, a profile and its service rows, the calendar,
     details, review, done, waitlist — renders in the one 640px column
     steps 3–4 already used, centred inside the wide sheet; the demo banner
     keeps to the same column. The calendar and that day's times now stack
     (the old 1fr/380px split left a ~100px-tall times column top-aligned
     beside a 292px calendar), so both share one centre line. `.on` stays
     in the selector: `.pane { display: none }` must keep winning for the
     panes that are not the current step. */
  main > .pane.on,
  main > .simband {
    max-width: 640px;
    margin-left: auto;
    margin-right: auto;
  }
}

/* B142 D1 (2026-09-26) — the Appearance settings page's frame is 720 wide,
   like every other settings page; two classes outrank desk.html's
   `.settwrap { max-width: 640px }` without touching the other settwrap pages. */
.settwrap.ap-page { max-width: 720px; }

/* ==========================================================================
   Schedule room (2026-09-02) — the front-desk's read-only OSCAR appointment
   book. Ported verbatim for the grid/tile/drawer mechanics; every class below is new
   (sched-*, appt, provcol, drawer, etc. don't exist elsewhere in this
   file) so nothing here redefines an existing token or component rule —
   `.chip`/`.badge`/`.btn`/`.widget`/`.overlay` all already live in
   public/desk.html's own inline <style> and are reused as-is by the room's
   toolbar; the one place this room's chips need a DIFFERENT selected style
   than desk.html's global `.chip[aria-pressed]` (accent-soft fill here vs
   that black fill, reserved for the Day|Week toggle) is scoped under
   `.sched-chips`/`.sched-chips-scroll` so it never leaks onto any other
   chip in the app. Runs on the SAME light/dark tokens as the rest of the
   desk (--accent/--ground/--ink/--line/... from the :root blocks above) —
   dark mode needs no separate appt/tile rules, only the grid-line and
   status-tint color-mixes below, which already resolve off those tokens.
   ========================================================================== */

:root {
  /* DESKG2 comp A: hour rules are the theme hairline (--line flips with
     the theme, so one block serves light and dark); the 5-minute scale's
     finer rules fade from it. */
  --gridline-hour: var(--line);
  --gridline-half: color-mix(in srgb, var(--line) 70%, transparent);
  --gridline-quarter: color-mix(in srgb, var(--line) 45%, transparent);
  --gridline-five: color-mix(in srgb, var(--line) 25%, transparent);
}

/* ------------------------------------------------------------- toolbar */
.datenav { display: flex; align-items: stretch; border: 1px solid var(--line);
           border-radius: var(--radius-sm); overflow: hidden; height: 36px; box-sizing: border-box; }
/* DESKG2 comp A: ‹ | Today | › — three text-only cells, 13/500 ink, hairline
   dividers (the kit's .g1-datenav look, restated here so this room reads
   the same whatever order the two sheets load in). */
.datenav button { border: 0; background: var(--ground); color: var(--ink); padding: 0 12px; min-width: 34px;
                   font-size: 13px; line-height: 18px; font-weight: 500; cursor: pointer;
                   display: flex; align-items: center; justify-content: center; }
.datenav button:hover { background: var(--surface-2); }
.datenav button:disabled { opacity: .45; cursor: default; }
.datenav button:disabled:hover { background: var(--ground); }
/* Today is a plain segment of the same outer frame, not its own boxed
   button — the hairlines either side of it are the ONLY separators in
   the row (DESKG1-schedule r2). */
.datenav .datepick { border-left: 1px solid var(--line); border-right: 1px solid var(--line);
                      font-weight: 500; color: var(--ink); font-variant-numeric: tabular-nums; padding: 0 12px;
                      background: var(--ground); cursor: pointer; }
.viewtoggle { display: flex; align-items: stretch; border: 1px solid var(--line);
              border-radius: var(--radius-sm); overflow: hidden; height: 36px; box-sizing: border-box; }
.viewtoggle button { border: 0; border-right: 1px solid var(--line); background: var(--ground); color: var(--ink-soft); padding: 0 12px;
                      font-size: 13px; line-height: 18px; font-weight: 500; cursor: pointer;
                      display: flex; align-items: center; justify-content: center; }
.viewtoggle button:last-child { border-right: 0; }
/* DESKG1-schedule r2 — the selected segment takes the G1 kit's own
   `.g1-segmented button.selected` look (primary-soft fill, ink text, 600)
   instead of a black ink invert; `--accent-soft`/`--ink` already resolve
   to the kit's light/dark pair, so no separate [data-theme='dark'] rule
   is needed here. */
.viewtoggle button[aria-pressed="true"] { background: var(--accent-soft); color: var(--ink); font-weight: 600; }
.sched-chips { display: flex; gap: 6px; flex-wrap: wrap; }
.sched-chips-scroll { display: flex; gap: 6px; overflow-x: auto; padding-bottom: 2px; }
/* Selected = accent-soft fill + accent text here (never the black fill
   `.chip[aria-pressed]` uses globally, reserved for the Day|Week toggle). */
.sched-chips .chip[aria-pressed="true"],
.sched-chips-scroll .chip[aria-pressed="true"] {
  background: var(--accent-soft); border-color: var(--accent); color: var(--accent-ink);
}
/* One-line practitioner chips (2026-09-02 fix) — a real roster like Fit
   for Life's (18, several with long OSCAR-supplied role text baked into
   the raw name) was wrapping the global `.chip`'s text to 3-4 lines
   because `.sched-chips-scroll` is an `overflow-x:auto` flex row, which
   sets each child's automatic minimum width to 0 and lets the browser
   shrink-and-wrap the label instead of scrolling past it. `flex: none` +
   `white-space: nowrap` restores the intended "scroll past it" behavior;
   height is pinned so every chip in the row reads as the same control
   regardless of whether it carries a role suffix. */
.sched-chips-scroll .chip {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  height: 36px;
  white-space: nowrap;
  box-sizing: border-box;
}
.sched-chips-scroll .chip .chip-role {
  color: var(--ink-faint);
  font-weight: 500;
}
.sched-chips-scroll .chip[aria-pressed="true"] .chip-role { color: var(--accent-ink); opacity: .8; }

/* ---------------------------------------------------- consolidated header
   (2026-09-11, Chris live: "I don't like how this is a box sitting within
   a box, and it's scrolling. Just make it fit to the screen and fix the
   schedule in the top-left corner... Look at all the white space to the
   top right." — then, on the consolidated row: "Just remove the schedule
   word and other words. just toggles that matter.") Replaces four stacked
   full-width rows (title row / .sched-toolbar / .freshline /
   .sched-legend2) with ONE row of bare controls on the same `.viewhead`
   pattern every other desk room already uses — no new box, border, or
   background; sits directly on the page. No title, no prose: ‹ date ›
   nav · Day/Week toggle · live dot + icon-only refresh, and nothing else.
   `.sched-header` overrides the shared `.viewhead`'s
   `align-items:baseline` to `center` (there is no text baseline left to
   align to — every child is a bordered control) and trims the two
   bordered controls to 32px so the whole row costs exactly one control's
   height plus the shared `.viewhead` margin. */
.viewhead.sched-header { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin: 0; }
/* DESKG1-schedule r2 — one height for every toolbar control on this row
   (date nav, both segmented groups, the practitioner select, Expand
   schedule, View options), so the row reads as one control strip, not a
   32px pair beside 36/auto-height siblings. */
.sched-header .datenav, .sched-header .viewtoggle, .sched-header .sched-provpick,
.sched-header .sched-expand, .sched-header .sched-options > summary,
.sched-head-actions .sched-expand, .sched-head-actions .sched-options > summary { height: 36px; box-sizing: border-box; }
/* DESKG2 comp A: Expand schedule and View options are the kit's secondary
   button — 13/500 ink on ground, hairline, control radius. On desktop they
   sit on the title row, right side (judge defect 2): `.sched-head-actions`
   inside #room-header, shown only while Schedule is the room. */
.sched-header .sched-expand, .sched-header .sched-options > summary,
.sched-head-actions .sched-expand, .sched-head-actions .sched-options > summary {
  display: inline-flex; align-items: center; justify-content: center; padding: 0 12px;
  border: 1px solid var(--line); border-radius: var(--radius-sm); background: var(--ground); color: var(--ink);
  font-size: 13px; line-height: 18px; font-weight: 500; }
.sched-header .sched-expand:hover, .sched-header .sched-options > summary:hover,
.sched-head-actions .sched-expand:hover, .sched-head-actions .sched-options > summary:hover { background: var(--surface); }
.sched-head-actions { flex: none; display: flex; align-items: center; gap: 8px; }
.sched-head-actions:empty, body:not(.schedule-page) .sched-head-actions { display: none; }

/* Far-right toggles-only legend — DESK10 (2026-09-16) pared this down to
   FOUR entries (Open · Pending · Not working · Now, decided on top
   of the approved comp), collapsed again by DESK14 (2026-09-16, Chris
   22:16) to Open · Booked · Not working · Now — Pending and Busy merge
   into the one grey "Booked" entry (a taken tile only ever says "Patient
   pending" for its own first hour, see schedule.js's buildLegend()): a
   visit's own status now shows as a word directly on its tile (schedule.
   js's tall-tile format) and "est." rides the tile's own time text, so
   "Known visit"/"Estimated length" no longer need a swatch to decode.
   Lived in a second header row until SCHED1 (2026-09-17, Chris live: "the
   buttons above calendar are too close to one another") moved it into
   `.sched-rail`, a column beside the grid — see schedule.js's buildRail().
   Hidden on mobile (comp 06a carries no legend row at that width — the
   provider picker takes its place in the header row, see .sched-provpick
   below); the rail is hidden there too, same guard. */
.sched-header-wrap { display: flex; flex-direction: column; gap: 8px; width: 100%; margin-bottom: 8px; }
.sched-legend { display: flex; align-items: center; gap: 8px; font-size: 12px; line-height: 16px; font-weight: 500; color: var(--ink-soft); flex-wrap: wrap; }
.legend-item { display: inline-flex; align-items: center; gap: 4px; }
.legend-sep { color: var(--ink-faint); }
.legend-swatch { width: 13px; height: 10px; border-radius: 2px; flex: none; box-sizing: border-box; }
.legend-swatch.legend-sw-booked { background: var(--accent-soft); border-left: 3px solid var(--accent); }
.legend-swatch.legend-sw-open { border: 1px solid var(--accent); background: var(--ground); }
.legend-swatch.legend-sw-estimated { background: var(--accent-soft); border-left: 3px dashed var(--accent); }
/* Reuses the exact fill `.appt.st-busy` already carries (below) — one
   colour formula, never a second swatch-only definition of "taken". Labels
   the legend's "Booked" entry (DESK14, 2026-09-16 — Pending's own dotted/
   italic swatch is retired with the separate pending tile it described). */
.legend-swatch.legend-sw-busy { background: var(--surface-2); border-left: 3px solid var(--line-strong); }
/* DESKSTRIPE1 (2026-09-23) — mirrors the grid: the "Not working" key
   swatch reads flat, the same `var(--surface-2)` fill the grid itself now
   paints for not-working time and closed days (DESKGRAMMAR1 + this row),
   never its own texture the grid no longer carries anywhere. */
.legend-swatch.legend-sw-notworking { border: 1px solid var(--line);
  background-image: none; background-color: var(--surface-2); }
.legend-swatch.legend-sw-now { height: 2px; width: 13px; border-radius: 0; background: var(--danger); align-self: center; }
/* The Open entry's "i" tooltip (DESK10) — same disclosure
   pattern as `.info` conventions elsewhere in this app: a plain title
   attribute, no JS popover. */
.legend-info { display: inline-flex; width: 16px; height: 16px; border-radius: 50%; border: 1px solid var(--line-strong);
  color: var(--ink-faint); font-size: 12px; line-height: 16px; align-items: center; justify-content: center; font-weight: 500; cursor: help; flex: none; }
@media (max-width: 760px) { .sched-legend, .sched-rail { display: none; } }

/* "+N not working today/this week" chip + "N cancelled" toggle + "Later ›"/
   "‹ Earlier" (DESK10 contract items 1, 2, 5) — all plain `.chip`s (desk.
   html's own shared control) inside the rail; `.ghost` is the one
   dashed-border variant this room needs (an informational, non-clickable
   chip reads differently from an actionable one). */
/* ROOMSFIX2 B209 — the idle count is information, not a control: a plain
   line in the caption's own type (no dashed box, no tap height), at every
   width. */
.sched-rail .chip.ghost, .sched-options .sched-rail .chip.ghost { border: 0; background: none; border-radius: 0; padding: 0;
  min-height: 0; font-size: 12px; line-height: 16px; font-weight: 500; color: var(--ink-faint); cursor: default; }
.cnt-toggle { display: inline-flex; align-items: center; gap: 8px; }
/* B408: desk.html's shared .chip pads 6px 13px; on the 4px grid this
   36-tall toggle pads 8px 12px with an 8px gap — same height, same width,
   its text 1px right of where it was. */
.chip.cnt-toggle { padding: 8px 12px; }
.cnt-toggle-sw { width: 10px; height: 10px; border-radius: 2px; border: 1px solid var(--line-strong);
  background: repeating-linear-gradient(0deg, transparent 0 4px, var(--line-strong) 4px 5px); }
.cnt-toggle[aria-pressed="true"] .cnt-toggle-sw { border-color: var(--ground);
  background: repeating-linear-gradient(0deg, transparent 0 4px, var(--ground) 4px 5px); }
.sched-rail .caption { font-size: 12px; line-height: 16px; font-weight: 500; color: var(--ink-faint); }
/* DESKG2: the View options chips take the control radius, never the
   shared .chip's 999 pill (TASTE §4). */
.sched-options .chip { border-radius: var(--radius-sm); font-size: 13px; line-height: 18px; font-weight: 500; }

/* Mobile practitioner picker
   — replaces the Day/Week toggle at mobile width (Week is hidden there;
   render() forces day mode). Styled like the segmented control it
   replaces: same 32px height and border-strong frame as .datenav/
   .viewtoggle in this same header row, a plain native <select> rather
   than a custom listbox (real OS picker UI, full keyboard/VoiceOver
   support for free). */
.sched-provpick-wrap { display: flex; align-items: stretch; min-width: 0; flex: 0 1 auto; max-width: 260px; }
.sched-provpick { flex: 1 1 auto; min-width: 0; height: 36px; border: 1px solid var(--line); border-radius: var(--radius-sm);
                  background-color: var(--ground); color: var(--ink); font-size: 13px; line-height: 18px; font-weight: 500; padding: 0 32px 0 12px; }
.sched-provpick:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* Appointments/Bea Connection cards (2026-09-12, room-settings-b3 change) —
   both cards moved from tile grids + a bare `<details>` to the shared
   `.disc`/`.disc-body` disclosure rows (desk.html owns those base rules);
   only each card's own non-disc chrome (state pill, action toolbar, the
   booking-checks sub-list) stays here. */
.bea-connection { border: 1px solid var(--line); border-radius: 12px; background: var(--surface); padding: 24px; margin: 24px 0 32px; }
.bea-connection-state { font-size: 12px; font-weight: 600; border-radius: 6px; padding: 6px 9px; background: var(--surface-2); color: var(--ink-soft); }
.bea-connection-state.is-on { background: var(--accent-soft); color: var(--accent-ink); }
.bea-connection-summary { font-size: 13px; line-height: 1.6; color: var(--ink); }
.bea-authorization .owner-field { max-width: 440px; }
.bea-actions { display: flex; flex-wrap: wrap; gap: 8px; margin: 14px 0 10px; }
.bea-connection .tgsub { line-height: 1.6; }
.bea-connection button { white-space: normal; }
@media (max-width: 640px) { .bea-connection { padding: 16px; } .bea-actions { flex-direction: column; align-items: stretch; } }
.owner-booking-checks { padding-left: 18px; margin: 12px 0 0; }
.owner-booking-checks li { margin: 10px 0; font-size: 12px; line-height: 1.5; }
.owner-booking-checks li strong { font-size: 12px; }
.owner-booking-checks li span { display: block; color: var(--ink-soft); margin-top: 3px; }

/* ---------------------------------------------------------------- grid */
:root {
  /* Fit-to-screen column floor (2026-09-12 round 2 — production defect:
     Fit for Life's 14-practitioner Day grid measured scrollWidth 2856 vs
     clientWidth 1146, Week measured 14756 vs 1146; owner's standing
     correction: "fit-to-screen — every practitioner visible, greyed when
     not working"). The ONLY knob — probe or override it at runtime
     (`document.documentElement.style.setProperty('--sched-col-min', ...)`)
     with no code change; schedule.js's ResizeObserver reacts to the
     resulting layout change on its own.
     Round 3 (2026-09-12, lead's decision, production defect):
     80px was illegible — "Marcu s (." style broken
     header abbreviations and "Odalys …" chip truncation. Floor raised to
     120px and HARD-CLAMPED below (max(120px, ...)) in both places that
     read this var, so a runtime override can raise the floor but never
     lower it past 120px. */
  --sched-col-min: 168px; /* DESKG2 comp A: First Last + the status word read whole at 168 */
}
/* SCHED1 (2026-09-17, Chris live: "make the calendar fit end to end
   horizontally instead of floating in its own box") round 1 dropped this
   rule's border+radius. Round 2 (Chris, live on production, real
   screenshot: "its not fititng to screen! it should not be sitting in a
   box with white boarders around") — border/radius were never the whole
   story: --ground (#fff) is close enough to --surface (#f7f8fa) that they
   look identical on a neutral display, but a warm-shifted one (Night
   Shift/f.lux, common) pushes near-white noticeably lighter than
   near-grey, so the plain white panel still read as a box with no border
   drawn at all. Fixed at the actual source: `.sched-gutter`/`.provcol`
   below (the sticky time column and every practitioner column) also
   painted --ground; all three now paint --surface, the page's own
   background, so there is no color boundary left to read as an edge
   under any color temperature. Individual tiles (booked/open/not-working)
   stay visually distinct on their own colors, never on a white backdrop
   they don't need. */
.sched-shell { background: var(--surface); overflow: hidden; }
/* DESKG2 comp A (2026-09-26, supersedes SCHED1's frameless
   grid): the day grid is the room's ONE surface — a single 8px hairline
   frame, the practitioner header as its surface-tinted strip, the day on
   the white ground, off-hours as the surface tint. Nothing boxed inside. */
.sched-shell.daybox { background: var(--ground); border: 1px solid var(--line); border-radius: var(--radius-card); }
.sched-main { width: 100%; min-width: 0; }
.sched-options { position: relative; flex: none; }
/* DESKG1-schedule r2 — same label size/weight as Expand schedule
   (14px/500), centered on both axes; height comes from `.sched-header
   .sched-options > summary` above. */
.sched-options > summary { list-style: none; border: 1px solid var(--line-strong); border-radius: var(--radius-sm);
  padding: 0 10px; font-size: 14px; font-weight: 500; cursor: pointer;
  display: flex; align-items: center; justify-content: center; box-sizing: border-box; }
.sched-options > summary::-webkit-details-marker { display: none; }
.sched-options > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.sched-options[open] > summary { background: var(--surface-2); }
.sched-options .sched-rail { position: absolute; right: 0; top: calc(100% + 6px); z-index: 12;
  display: flex; flex-direction: column; align-items: flex-start; width: 240px; max-width: calc(100vw - 32px);
  padding: 14px; gap: 12px; border: 1px solid var(--line-strong); border-radius: var(--radius-sm); background: var(--ground); box-shadow: var(--shadow-lift); }
.sched-options .sched-rail .sched-legend { display: flex; flex-direction: column; align-items: flex-start; }
.sched-options .sched-rail .sched-legend .legend-sep { display: none; }
.sched-options .chip { min-height: 36px; }
.sched-expand { white-space: nowrap; }
.schedule-page .viewhead.sched-header { margin-bottom: 0; }
/* Real rosters run past what any viewport fits side by side (Fit for
   Life: 18 practitioners) — every column shares the available width
   equally (see `.sched-colgroup-grid` below) down to `--sched-col-min`,
   never squeezed further; only past that floor does the room scroll
   HORIZONTALLY. The gutter and header both stay sticky against that
   scroll (2026-09-02 fix). Scrollbar is ALWAYS a visible affordance when
   present (2026-09-12 round 2) — never the OS's auto-hiding thin overlay
   — `scrollbar-gutter: stable` reserves its track space so the grid never
   jumps when a scrollbar appears/disappears across a day/week toggle. */
.sched-shell.daybox { max-height: calc(100vh - 260px - var(--chrome-banner-h, 0px)); overflow-y: auto; overflow-x: auto; scrollbar-gutter: stable;
                       scroll-snap-type: x proximity; scroll-padding-left: 56px; }
.sched-shell.daybox::-webkit-scrollbar { height: 8px; }
.sched-shell.daybox::-webkit-scrollbar-track { background: var(--surface); }
.sched-shell.daybox::-webkit-scrollbar-thumb { background: var(--line-strong); border-radius: 4px; }
.sched-shell.daybox { scrollbar-width: thin; scrollbar-color: var(--line-strong) var(--surface); }
.sched-shell.stale { opacity: 0.55; filter: grayscale(0.35); pointer-events: none; }
.sched-cols-head { display: flex; border-bottom: 1px solid var(--line); background: var(--surface);
                    position: sticky; top: 0; z-index: 5; width: 100%; }
/* B190 (2026-09-27, JUDGE-roomsfix-3 NEW-1) — the header, day-label and
   body rows were each the shell's width while their practitioner columns
   overflowed them (17 practitioners). Past the first screen the header had
   no ground (visits painted over names), and at the far end the sticky
   time gutter and the ‹ › pager left with their one-screen box. Each row
   is now as wide as its columns: min-content is exactly the columns' own
   floors (a flexible track sizes to its minimum under min-content, the
   same widths the overflowing columns already had), never less than the
   shell. The header is solid over the whole scroll width at every
   scrollLeft, and the gutter sticks to the end. */
.sched-shell > :is(.sched-cols-head, .sched-daygroup-head, .sched-body) { width: min-content; min-width: 100%; }
.sched-gutter-head { width: 56px; flex: none; position: sticky; left: 0; z-index: 7; background: var(--surface);
                      display: flex; align-items: center; justify-content: center; }
/* Header-gutter ‹ › pager (2026-09-12 round 2) — schedule.js only ever
   appends this when the grid genuinely overflows (`pagerVisible()`), so
   there is no hidden/disabled state to style — present means usable. */
.sched-pager { display: flex; gap: 2px; }
.sched-pager-btn { width: 24px; height: 24px; border: 1px solid var(--line); border-radius: var(--radius-sm);
                    background: var(--ground); color: var(--ink-soft); font-size: 13px; line-height: 18px; font-weight: 500;
                    cursor: pointer; display: flex; align-items: center; justify-content: center; padding: 0; }
.sched-pager-btn:hover { background: var(--surface-2); color: var(--ink); }
.sched-pager-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* One flex row per header/body layer, holding one `.sched-colgroup` per
   day-grid (Day view: exactly one; Week: up to 7) — see schedule.js's
   `renderGrid()` header comment for the full nested-layout rationale.
   This is the row that actually overflows `.sched-shell.daybox` when the
   floor is hit; schedule.js measures ITS width (never the shell's) for
   the pager decision. */
.sched-colgroups-row { display: flex; align-items: stretch; flex: 1 1 auto; min-width: 0; }
/* Every day-grid — Day's one, each of Week's up to 7 — is IDENTICAL: a
   flex child sharing the row's width with its siblings, but never
   shrinking below its own N * floor pixel minimum (`--sched-daycount` is
   set per group by schedule.js to that day's practitioner count). Past
   that combined minimum, the whole `.sched-colgroups-row` overflows and
   `.sched-shell.daybox` scrolls — never a silent squeeze past the floor. */
.sched-colgroup { flex: 1 1 auto; min-width: calc(var(--sched-daycount, 1) * max(120px, var(--sched-col-min, 120px))); box-sizing: border-box; }
/* The two rows that carry actual practitioner columns (head names, body
   tiles) additionally lay their day-group out as a CSS grid — N columns
   sharing whatever width `.sched-colgroup`'s flex sizing gave that day,
   equally, down to the SAME floor. This is the literal fit-to-screen
   rule: `minmax(floor, 1fr)` grows every column evenly while there is
   room, and holds at the floor once there isn't. */
.sched-colgroup-grid { display: grid; grid-template-columns: repeat(var(--sched-daycount, 1), minmax(max(120px, var(--sched-col-min, 120px)), 1fr)); }
/* Week carries short date headings; the practitioner is named once in
   its selector. Seven dated columns fit at ordinary desktop widths. */
.weekbox .sched-colgroup { min-width: calc(var(--sched-daycount, 1) * 96px); }
.weekbox .sched-colgroup-grid { grid-template-columns: repeat(var(--sched-daycount, 1), minmax(96px, 1fr)); }
.weekbox .provcol-head { padding: 12px 12px; }
.provcol-head { min-width: 0; padding: 12px 16px; border-left: 1px solid var(--line); scroll-snap-align: start;
                display: flex; align-items: flex-start; gap: 8px; box-sizing: border-box; }
.provcol-head > div { min-width: 0; }
/* Round 3 (2026-09-12, lead's decision): the header floor is now
   hard-clamped to >=120px, so the practitioner's FULL "First Last" name
   always fits — the header never abbreviates to an initial anymore (that
   was round 2's failed compromise: "Marcu s (." came from abbreviating
   AFTER a stray "(Chiropractor)" token had already been mistaken for the
   surname). `overflow-wrap: normal` + `hyphens: none` — NEVER break
   inside a word, even at the 120px floor; a name that can't fit on 2
   lines ellipsizes at the (rare) 3rd rather than splitting a word. */
/* DESKG2 comp A: name 13/18 500 ink, never cut — it wraps whole words
   onto a second line before it would ever clip (columns are >=168). */
.provcol-head .pname { font-size: 13px; font-weight: 500; line-height: 18px; color: var(--ink); overflow-wrap: normal;
                        hyphens: none; word-break: normal; white-space: normal; min-width: 0; }
/* The stripped "(Chiropractor)"/"(RMT)" role suffix (stripTrailingSuffix())
   becomes its own small faint 3rd line — never folded back into `.pname`,
   never re-parenthesized, one ellipsized line since a role name is never
   long enough to need wrapping. */
.provcol-head .pname-suffix { font-size: 12px; font-weight: 500; color: var(--ink-soft); line-height: 16px;
                               white-space: normal; margin-top: 2px; }
.provcol-head .pmeta { font-size: 12px; line-height: 16px; font-weight: 500; color: var(--ink-soft); margin-top: 2px; }
/* FILLDAY3 — "Fill this day", one per practitioner column, only when
   that column's own date is in the future and this page's own
   availability read already shows an open start. 44px minimum touch
   target (TASTE.md/standards/frontend-done.md) even though the visual
   row is compact — padding carries the height, not a taller box. */
/* DESKG2 comp A: a quiet text action under the role (no box inside the
   header strip); 44px tall on a phone (media rule below). */
.provcol-fillday {
  display: inline-flex; align-items: center; justify-content: flex-start;
  min-height: 24px; margin-top: 4px; padding: 0;
  border: 0; border-radius: var(--radius-sm); background: none; color: var(--accent-ink); cursor: pointer;
  font: inherit; font-size: 12px; line-height: 16px; font-weight: 500;
}
.provcol-fillday:hover { text-decoration: underline; }
.provcol-fillday:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.sched-body { display: flex; position: relative; }
.sched-gutter { width: 56px; flex: none; position: sticky; left: 0; z-index: 4; background: var(--ground); }
/* DESKG2 comp A: 12/16 ink-soft, centred on its hour rule. */
.gutter-label { position: absolute; right: 8px; left: 0; text-align: right; transform: translateY(-50%);
                font-size: 12px; font-weight: 500; line-height: 16px; color: var(--ink-soft); font-variant-numeric: tabular-nums; }
/* The now bubble replaces the hour label it would collide with, same as
   Google Calendar's red time readout — the label stays in the DOM. */
.gutter-label.is-under-now { visibility: hidden; }
.gutter-sublabel { position: absolute; right: 8px; left: 0; text-align: right; transform: translateY(-50%);
                    font-size: 12px; line-height: 16px; color: var(--ink-faint); font-variant-numeric: tabular-nums; font-weight: 500; }
.fivemin-line { position: absolute; left: 0; right: 0; border-top: 1px solid var(--line); pointer-events: none; }
.book-search-retry { display: block; margin-top: 8px; }
/* Half-hour tick (DESK10 contract item 4) — no text, the hour label above
   already carries the reading; a finer-grained gutter without a second
   label competing with it. */
.gutter-tick { position: absolute; right: 0; width: 9px; border-top: 1px solid var(--line-strong); }
/* The one gutter time bubble (DESK10 contract item 4) — buildGutter()'s
   single reader of `nowMin`; every per-column `.nowline-seg` still marks
   the line itself, this is just the shared time readout instead of
   repeating it per column. */
/* DESKG2 comp A: the now time is plain 12/16 600 text in the now-line
   colour on the gutter's own ground (no pill, no fill), centred on the line. */
.gutter-nowbubble { position: absolute; left: 0; right: 8px; transform: translateY(-50%);
  z-index: 4; background: var(--ground); color: var(--status-nowline); font-size: 12px; line-height: 16px; font-weight: 600;
  text-align: right; font-variant-numeric: tabular-nums; }

/* Week's collapsed off-day column (DESK10 contract item 6, Chris live: "if
   people are not working just leave them off the schedule for the day /
   week") — a fixed 44px hatched placeholder carrying only the weekday
   label, replacing the old full-width "Not working" column. `.off` here
   is scoped to a BARE `.provcol-head`/`.provcol` (no `.sched-colgroup`
   wrapper — see schedule.js's renderGrid()), so its fixed width is never
   fought by the grid-floor rules those wrapped cells carry. */
/* DESKSTRIPE1 (2026-09-23) — same flat quiet grey DESKGRAMMAR1 gave
   not-working time inside a working day (`.hatched`, below): a closed day
   is the SAME "not working" fact at column granularity, so it reads with
   the identical fill, never its own texture. `var(--surface-2)`, the exact
   token DESKGRAMMAR1 used, light and dark alike (that token itself already
   resolves differently per theme — no separate dark override needed). */
.provcol-head.off { flex: none; width: 44px; box-sizing: border-box; padding: 10px 4px;
  display: flex; align-items: center; justify-content: center; text-align: center;
  font-size: 12px; line-height: 16px; font-weight: 500; color: var(--ink-faint);
  background-image: none; background-color: var(--surface); }
.provcol.off { flex: none; width: 44px; min-width: 44px; box-sizing: border-box;
  background-image: none; background-color: var(--surface); }

/* DESK12 (2026-09-16) — every tile/badge stays inside its own column at
   every width (Chris, live: an Open tile + a small badge overflowing a
   column's right edge at 9 columns/1440). `overflow: hidden` clips
   anything that would otherwise escape past the column's own box,
   whatever the exact cause — the tiles/badges themselves are already
   positioned `left/right` relative to THIS box (see `.appt`/
   `.sched-opening` below), so nothing legitimate needs to bleed past it. */
.provcol { min-width: 0; position: relative; border-left: 1px solid var(--line); background: var(--ground); box-sizing: border-box; overflow: hidden; scroll-snap-align: start; }
/* Day-group header row (2026-09-11; column rule replaced 2026-09-12 round
   2) — one label cell per day, spanning exactly that day's own
   `.sched-colgroup` (same `--sched-daycount`/floor formula, so its width
   matches the columns-head/body rows for that date pixel for pixel — see
   schedule.js's renderGrid() header comment). Sits above `.sched-cols-
   head`'s existing per-column practitioner-name row; both stay sticky,
   stacked (28px tall day-group row at top:0, the practitioner-name row
   pinned at top:28px via `.has-daygroups` below) so both remain visible
   while the grid body scrolls vertically. */
.sched-daygroup-head { display: flex; border-bottom: 1px solid var(--gridline-hour); background: var(--surface);
                        position: sticky; top: 0; z-index: 6; width: 100%; height: 28px; }
/* Left-aligned, not centered (2026-09-11 walk fix, caught live): a day
   worked by a full roster spans a wide cluster — a centered label sits
   deep inside that cluster, off the initial horizontal-scroll position
   entirely, so the FIRST thing a visitor sees scrolling into a new day is
   an unlabelled column. Left-aligned with the same 14px inset the
   practitioner-name header already uses (`.provcol-head`), the label
   appears the instant that day's first column scrolls into view —
   matching every other label in this room, none of which are centered. */
.daygroup-cell { box-sizing: border-box; border-left: 1px solid var(--line); overflow: hidden;
                  display: flex; align-items: center; justify-content: flex-start; padding-left: 16px;
                  font-size: 12px; line-height: 16px; font-weight: 500; color: var(--ink-soft); white-space: nowrap; }
.sched-cols-head.has-daygroups { top: 28px; }
.hourline { position: absolute; left: 0; right: 0; border-top: 1px solid var(--gridline-hour); }
.halfline { position: absolute; left: 0; right: 0; border-top: 1px solid var(--gridline-half); }
.quarterline { position: absolute; left: 0; right: 0; border-top: 1px solid var(--gridline-quarter); }
/* DESKGRAMMAR1 SCHEDULE (2026-09-23), tightened by DESKSTRIPE1 fix-round
   (2026-09-23) — comp grammar item 5: not-working time is a flat quiet
   grey, not a diagonal hatch (the comp has no texture on
   its off-hours band). The diagonal `repeating-linear-gradient` this class
   carried is retired outright, light and dark, rather than painted over
   by a scoped override: `.hatched` is only ever created by schedule.js
   (renderGrid()'s holiday/off-hours bands), so there is no other caller
   to preserve the texture for, and a future caller outside the Schedule
   room would otherwise silently bring the diagonal hatch back. Same
   `var(--surface-2)` token DESKGRAMMAR1 used elsewhere; that token itself
   already resolves differently per theme, so no `[data-theme='dark']`
   twin is needed. */
/* DESKG2 comp A: time off is the flat surface tint on the white day. */
.hatched { position: absolute; left: 0; right: 0; z-index: 1; background: var(--surface); }
.hatched-label { position: absolute; top: 8px; left: 8px; font-size: 12px; line-height: 16px; font-weight: 500;
                  color: var(--ink-faint); }
.away-band { position: absolute; left: 0; right: 0; background: var(--surface); z-index: 1; }
.away-band span { position: absolute; top: 8px; left: 8px;
                   font-size: 12px; line-height: 16px; font-weight: 500; color: var(--ink-faint); white-space: nowrap; }

/* Positive returned starts only; blank grid space makes no availability claim. */
.sched-availability-note { display: flex; align-items: center; gap: 12px; margin: 0 0 16px; font-size: 12px; line-height: 16px; font-weight: 500; color: var(--ink-soft); }
.sched-availability-note > span { flex: 1; min-width: 0; }
.sched-availability-note > button { flex: none; }
/* DESKG2 comp A: an Open time is a quiet primary tint with its word at
   right — no border, so nothing reads as a box inside the frame. */
.sched-opening { position: absolute; left: 4px; right: 4px; z-index: 1; border: 0; background: color-mix(in srgb, var(--accent) 6%, var(--ground)); border-radius: var(--radius-sm); padding: 0 8px; overflow: hidden; text-align: right; color: var(--accent-ink); cursor: pointer; }
.sched-opening-label { font-size: 12px; line-height: 16px; font-weight: 500; }
.sched-opening.is-tiny { padding: 0; }
.sched-opening.is-tiny .sched-opening-label { display: none; }
.sched-opening:hover, .sched-opening:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; background: var(--accent-soft); }
/* SCHED1 (2026-09-17, Chris live production walk: "those bubbles are still
   touching the calendar too") — inherited the base rule's `right: 2px`,
   which reads as touching the column divider at normal zoom. Its own
   `right: 6px` gives it real breathing room from the border, same as any
   other tile already has via the base rule's `left/right: 2px` pair. */
.sched-opening.has-conflict { left: auto; right: 6px; width: 18px; z-index: 3; padding: 0; color: var(--warn); background: var(--warn-soft); text-align: center; }
.sched-opening-option { display: flex; align-items: baseline; gap: 16px; padding: 12px 0; border-top: 1px solid var(--line); font-size: 13px; }
.sched-opening-option > strong { flex: 0 0 72px; }
.sched-opening-option > span { flex: 1; min-width: 0; line-height: 1.5; }
.modal:has(.sched-opening-details) { max-width: 640px; }
/* DESKBOOK2 (2026-09-18) — the "Book a patient" action added to each
   open-times row; the row's own flex layout (baseline-aligned time +
   description) is untouched, the button just gets its own alignment so
   it doesn't stretch or baseline-shift against the text beside it. */
.sched-opening-option { flex-wrap: wrap; }
.sched-opening-option .btn { flex: none; align-self: center; margin-left: auto; }
.appt.is-stale-evidence { opacity: .7; }

/* DESKBOOK2 (2026-09-18) — the booking panel opened from an open-times
   row, inside the ONE shared #modal dialog (never a second dialog
   implementation, DESIGN.md hard ban). Reuses `.field`/`.notice`/`.btn`
   wholesale; only the patient combobox's results list, the selected-
   patient summary row, and the service radio list are new component
   shapes — no native <select> anywhere. */
.modal:has(.book-panel) { max-width: 480px; }
.book-panel .stcaption { margin-bottom: 18px; }
.book-results { border: 1px solid var(--line-strong); border-radius: var(--radius-control);
  margin-top: 6px; max-height: 240px; overflow-y: auto; background: var(--ground); }
.book-result-row { display: block; width: 100%; text-align: left; padding: 9px 12px; border: 0;
  border-top: 1px solid var(--line); background: none; cursor: pointer; font: inherit; color: var(--ink);
  min-height: 44px; }
.book-result-row:first-child { border-top: 0; }
.book-result-row:hover, .book-result-row.is-active { background: var(--surface-2); }
.book-result-empty { padding: 12px; font-size: 13px; color: var(--ink-faint); }
.book-result-name { font-size: 14px; font-weight: 550; color: var(--ink); }
.book-result-meta { font-size: 12px; color: var(--ink-soft); margin-top: 1px; }
.book-result-note { font-size: 11.5px; color: var(--ink-faint); margin-top: 1px; }
.book-selected-patient { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px;
  border: 1px solid var(--line-strong); border-radius: var(--radius-control); padding: 10px 12px; margin-top: 6px; }
.book-service-list { border-top: 1px solid var(--line); }
/* DESKBOOK2 (2026-09-18) — MISSING_FIELDS field-highlighting: the
   pre-existing `.field.err input/textarea/select` rule (this file, forms
   section) has nothing to grab onto for the service field (a button list,
   not a native control), so its label carries the same danger colour as a
   generic fallback that works regardless of what's inside a given field. */
.field.err > label { color: var(--danger); }
.book-service-row { display: flex; align-items: center; gap: 10px; width: 100%; text-align: left;
  padding: 11px 2px; border: 0; border-bottom: 1px solid var(--line); background: none; cursor: pointer;
  font: inherit; font-size: 14px; color: var(--ink); min-height: 44px; }
.book-service-row:last-child { border-bottom: 0; }
.book-service-row:hover { background: var(--surface-2); }
.book-service-row .radio-dot { flex: none; width: 16px; height: 16px; border-radius: 50%;
  border: 1.5px solid var(--line-strong); position: relative; }
.book-service-row[aria-checked="true"] .radio-dot { border-color: var(--accent); }
.book-service-row[aria-checked="true"] .radio-dot::after { content: ''; position: absolute; inset: 3px;
  border-radius: 50%; background: var(--accent); }
.book-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 20px; }
/* DESKBOOK2 round 4 (2026-09-18) — the NOT_LINKED secondary action:
   sits right under the error sentence it answers, own field (DOB, shown
   only when needed) plus one plain (never primary-accent) button. */
.book-new-patient { display: flex; flex-direction: column; gap: 10px; margin: -8px 0 18px; }
.book-new-patient .field { margin-bottom: 0; }
.book-new-patient .btn { align-self: flex-start; }
@media (max-width: 760px) {
  .sched-availability-note { flex-wrap: wrap; font-size: 11px; margin-bottom: 8px; }
  .book-actions .btn, .book-selected-patient .btn, .sched-opening-option .btn { min-height: 44px; }
  .book-result-row, .book-service-row { min-height: 44px; }
}

/* DESKG1-schedule: every toolbar control (date nav, Day/Week and scale
   toggles, practitioner picker, Expand schedule, View options) is a real
   44px touch target on a phone, matching the kit's own phone floor. */
@media (max-width: 767px) {
  .datenav, .datenav button, .viewtoggle, .viewtoggle button,
  .sched-provpick, .sched-expand, .sched-options > summary { min-height: 44px; height: auto; }
  /* DESKG2 comp A @390: the joined ‹ Today › and the scale segment each
     take the full row with equal cells, every cell a 44x44 tap (the kit's
     own .g1-datenav rule sits the cells at 42 inside a 44 frame). */
  .sched-header .datenav, .sched-header .viewtoggle { flex: 1 1 100%; height: auto; }
  .sched-header .datenav .g1-datenav-btn, .sched-header .viewtoggle button { flex: 1 1 0; min-height: 44px; }
  /* r4 (2026-09-26): the option now reads "name, hours" —
     the desktop cap above truncated that mid-word on a phone, so drop it
     here and let the picker take the full toolbar row instead. */
  .sched-provpick-wrap { max-width: none; flex-basis: 100%; }
  .sched-provpick { height: 44px; }
  .provcol-fillday { min-height: 44px; }
  .sched-header > .spacer { display: none; } /* a 0-tall wrapped row would double the 8px gap */
}

/* DESKG2 comp A: a 1px rule BEHIND the visit cards —
   schedule.js appends it before any tile at the Open tiles' level, so a
   card always paints over it and never reads as crossed out. */
.nowline-seg { position: absolute; left: 0; right: 0; height: 0; border-top: 1px solid var(--status-nowline); z-index: 1; pointer-events: none; }
.nowdot-wrap { position: absolute; left: 0; width: 56px; display: flex; align-items: center;
               justify-content: flex-end; gap: 4px; transform: translateY(-50%); z-index: 4; }
.nowdot-wrap .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--danger); flex: none; }
.nowdot-wrap .lbl { font-size: 11px; font-weight: 600; color: var(--danger); font-variant-numeric: tabular-nums; }

/* ------------------------------------------------------------- tiles */
/* DESKG2 comp A chip: "First Last" left and the status word right on one
   line, a 2px tone bar, the tone's tint as the fill, the control radius.
   A word that does not fit wraps under the name and the chip's own height
   clips it — the name keeps the line; a tall chip's range sits under.
   B408 (4px grid): 4px inset + 8px name-to-word gap — measured on the
   demo at 1440, 8px inset cut 3 names; 4/8 changes no name or wrap. */
.appt {
  position: absolute; left: 2px; right: 2px; border-radius: var(--radius-sm); border: 0;
  border-left: 2px solid var(--ink-faint); padding: 0 4px; overflow: hidden;
  cursor: pointer; z-index: 2; box-sizing: border-box; color: var(--ink);
  display: flex; flex-wrap: wrap; align-content: flex-start; align-items: baseline; column-gap: 8px; text-align: left;
  font: inherit; font-size: 12px; line-height: 14px; font-weight: 500;
}
.appt:hover { filter: brightness(0.97); }
.appt--draggable { cursor: grab; touch-action: pan-y; }
.appt--dragging { cursor: grabbing; z-index: 6; box-shadow: 0 6px 18px rgba(0,0,0,.28); opacity: .92; transition: none; touch-action: none; }
.appt-drop-ghost { position: absolute; left: 2px; right: 2px; z-index: 1; border: 2px dashed var(--accent); border-radius: 4px; pointer-events: none; box-sizing: border-box; }
.sched-drop-note { position: absolute; left: 4px; right: 4px; top: 4px; z-index: 7; padding: 6px 8px; border-radius: 6px;
  background: var(--ink); color: var(--bg, #fff); font-size: 12px; font-weight: 600; pointer-events: none; }
@media (prefers-reduced-motion: no-preference) { .appt--draggable:not(.appt--dragging) { transition: top .18s ease; } }
.appt:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* One-line chip — name +
   an estimated-duration tag, nothing else. The old second "Appointment ·
   Other" line and the raw start–end time line are gone from the chip
   (both still live in full in the drawer). */
.appt .a-name { flex: 0 1 auto; font-size: 12px; font-weight: 500; line-height: 14px; color: var(--ink); overflow: hidden;
                white-space: nowrap; text-overflow: ellipsis; min-width: 0; }
.appt .a-status { flex: none; margin-left: auto; font-size: 12px; line-height: 14px; font-weight: 500; color: var(--ink-soft); white-space: nowrap; }
.appt .a-unverified { flex: none; align-self: center; width: 6px; height: 6px; border-radius: 50%; background: var(--warn); }
.appt .a-dur { flex: none; font-size: 10.5px; font-weight: 550; color: var(--ink-soft); }
/* DESKG2 comp A (judge defect 4): a chip draws name + status word only —
   the "Booked online" dot is not shown; the fact stays in the chip's
   title/aria-label and the visit window. */
.appt .a-dot { display: none; }
.appt.tiny { padding: 0 4px; }
.appt.tiny .a-name { line-height: 12px; }
.appt.tiny .a-dur { display: none; }
/* Compact vs tall tile format (DESK10 contract item 3) — compact is the
   one-line "Name · start[· est.]" form (`.a-time`, same slot `.a-dur` used
   to sit in); tall adds the second `.a-sub` line with the real time range
   and the status word. Which form a given tile gets is scale/height math
   in schedule.js (buildProvCol) — these two classes are ONLY the paint. */
.appt .a-time { flex: none; font-size: 10.5px; font-weight: 500; color: var(--ink-soft); font-variant-numeric: tabular-nums; }
/* DESKG1-schedule r2 — the status tag echoes the chip's own 4px corner
   instead of the kit's default pill radius; a flat block, not a tinted
   cap glued onto the end of the chip. */
.appt.tall { padding: 3px 6px; line-height: 16px; }
.appt.tall .a-name, .appt.tall .a-status { line-height: 16px; }
.appt .a-sub { flex: 1 0 100%; font-size: 12px; font-weight: 500; color: var(--ink-soft); line-height: 16px;
               white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.appt .a-sub[hidden] { display: none; }
.appt .a-sub b { font-weight: 600; }
.appt.st-cancelled .a-sub, .appt.st-cancelled .a-time, .appt.st-cancelled .a-status { color: var(--ink-faint); }
/* DESKZOOM1 contract item 5 — Whole day drops the tile-height floor so a
   short visit becomes its true-duration slim bar; below the readable-text
   floor it carries no visible label (its aria-label + title still carry
   patient/time/status, and it still opens on tap), just the accent bar. */
.appt.is-slim { padding: 0; display: block; }

/* OPENS2 (2026-09-19) — booked/no-show/cancelled now paint from the
   `--status-*` aliases above (owner-overridable from the fixed accessible
   palette) instead of the raw semantic token directly; every DEFAULT value
   is identical to before this feature, so an untouched clinic is
   byte-identical. `st-inroom`/`st-done`/`st-requested` are untouched —
   not named in the map's "desk status colours" scope. */
/* DESKG2 comp A tones — Booked grey (the --status-booked default), Arrived
   and In room green, No-show amber, Cancelled faint; each word in its tone. */
.appt.st-booked { background: color-mix(in srgb, var(--status-booked) 12%, var(--ground)); border-left-color: var(--status-booked); }
.appt.st-arrived, .appt.st-inroom { background: color-mix(in srgb, var(--accent) 12%, var(--ground)); border-left-color: var(--accent); }
.appt.st-arrived .a-status, .appt.st-inroom .a-status { color: var(--accent-ink); }
.appt.st-done { background: color-mix(in srgb, var(--ink-faint) 8%, var(--ground)); border-left-color: var(--ink-faint); }
.appt.st-done .a-name, .appt.st-done .a-dur { color: var(--ink-soft); }
.appt.st-noshow { background: color-mix(in srgb, var(--status-noshow) 12%, var(--ground)); border-left-color: var(--status-noshow); }
.appt.st-noshow .a-status { color: color-mix(in srgb, var(--status-noshow) 85%, var(--ink)); }
.appt.st-cancelled { background: color-mix(in srgb, var(--status-cancelled) 6%, var(--ground)); border-left-color: var(--status-cancelled); }
.appt.st-cancelled .a-name { text-decoration: line-through; color: var(--ink-soft); }
.appt.st-cancelled .a-dur { color: var(--ink-faint); }
/* st-requested (2026-09-04, NO-EMR DESK change) — a no-EMR booking request the
   desk hasn't confirmed/declined yet (see STATUS_LABEL in schedule.js). */
.appt.st-requested { background: color-mix(in srgb, var(--warn) 10%, var(--ground)); border-left-color: var(--warn); border-left-style: dashed; }
.appt.st-requested .a-status { color: var(--warn); }
/* st-pending RETIRED (DESK14, 2026-09-16) — the separate "pending" tile it
   styled is gone; a taken tile always says "Booked" now (DESK14, amended
   23:07 — "we dont need booked AND patient pending... booked w patient
   info, empty slot, or booked (without patient info) that's all"), the
   SAME grey `.st-busy` tile below, never italic. Zero remaining callers
   (grepped public/, test/). */

/* st-busy (2026-09-16, DESK13b, Chris 20:34 — "the only time something
   should say patient pending is the gap between the system seeing a
   booking...the undescribed time is time OSCAR is NOT offering"; DESK14,
   2026-09-16, Chris 22:16, amended 23:07 — Pending and Busy collapse into
   this ONE "taken" tile, always "Booked", never a separate "Patient
   pending" state) — every working-hours minute OSCAR shows taken with no
   visible visit claiming it: a snapshot remainder, or this page's own live
   check finding it no longer offered. Deliberately the QUIETEST fill on
   the grid — one neutral, `--surface-2` (this app's existing flat surface
   token, no new colour), no accent hue, no dashed border — plainly
   distinct from the diagonal "Not working" hatch and an Open tile's
   accent-bordered pill. Never a `<button>` (schedule.js renders it as a
   plain `<div>`) — not an interactive element, so no `:hover`/
   `:focus-visible` treatment applies here. */
.appt.st-busy { background: var(--surface-2); border-left-color: var(--line-strong); cursor: default; }
.appt.st-busy:hover { filter: none; }
/* `--ink-soft`, not `--ink-faint` — measured contrast against `--surface-2`
   (2026-09-16, agent-browser eval on the live walk): ink-faint reads
   2.9:1, under the 4.5:1 text floor; ink-soft reads 6.1:1 — still visibly
   quieter than a real visit's bold `--ink` name, just legible. */
.appt.st-busy .a-name { color: var(--ink-soft); }

/* DESK11b — the ring on the tile whose drawer is currently open (applyOpenTileRing() in
   schedule.js). Existing tokens only (--ground/--ink), no new colour. */
.appt.is-drawer-open { box-shadow: 0 0 0 2px var(--ground), 0 0 0 4px var(--ink); z-index: 3; }

/* ------------------------------------------------------------ day list
   DESKG2 comp A @390, Whole day (schedule.js buildDayList): ONE frame of
   56px hairline rows — time 12/16 ink-soft | name 13/18 over "10:00 am,
   15 min" 12/16 | the status tag at right; "Open" in ink-faint. */
.sched-daylist { border: 1px solid var(--line); border-radius: var(--radius-card); background: var(--ground); overflow: hidden; }
.sched-dayrow { display: flex; align-items: center; gap: 12px; width: 100%; min-height: 56px; margin: 0; padding: 8px 16px;
  box-sizing: border-box; border: 0; border-bottom: 1px solid var(--line); background: var(--ground); color: var(--ink);
  text-align: left; font: inherit; font-size: 13px; line-height: 18px; font-weight: 500; }
.sched-dayrow:last-child { border-bottom: 0; }
button.sched-dayrow { cursor: pointer; }
button.sched-dayrow:hover { background: var(--surface); }
button.sched-dayrow:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.sched-dayrow.is-drawer-open { background: var(--accent-soft); }
.sched-dayrow-time { flex: 0 0 64px; font-size: 12px; line-height: 16px; font-weight: 500; color: var(--ink-soft); font-variant-numeric: tabular-nums; }
.sched-dayrow-text { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; }
.sched-dayrow-name { font-size: 13px; line-height: 18px; font-weight: 500; color: var(--ink); overflow-wrap: anywhere; }
.sched-dayrow-meta { margin-top: 2px; font-size: 12px; line-height: 16px; font-weight: 500; color: var(--ink-soft); }
.sched-dayrow-open, .sched-dayrow-quiet { color: var(--ink-faint); }
.sched-dayrow.is-cancelled .sched-dayrow-name { text-decoration: line-through; color: var(--ink-soft); }
.sched-dayrow .g1-status-tag { flex: none; }
.sched-dayrow-fill { color: var(--accent-ink); }
.sched-dayrow.is-empty { color: var(--ink-soft); }

/* -------------------------------------------------------------- drawer
   DESK11b (2026-09-16, approved comps) — the drawer is now a 420px auto-height CARD,
   top-right over the dimmed grid, never a full-height right-edge panel;
   own overlay class kept (never `.overlay`/`#modal`, the existing CENTERED
   modal desk.html uses elsewhere — a different shape). */
.sched-overlay { position: fixed; inset: 0; background: rgba(10, 12, 16, 0.44); z-index: 50;
                 display: flex; justify-content: flex-end; align-items: flex-start; padding: 16px; }
.sched-overlay[hidden] { display: none; }
.sched-drawer { width: min(420px, 100%); height: auto; max-height: calc(100% - 32px); background: var(--ground);
                border: 1px solid var(--line); border-radius: var(--radius-card);
                box-shadow: var(--shadow-lift); display: flex; flex-direction: column; overflow: hidden; }
.sched-drawer-head { display: flex; align-items: flex-start; gap: 12px; padding: 18px 20px 14px; border-bottom: 1px solid var(--line); }
.sched-drawer-head .dh-name { font-size: 17px; font-weight: 640; }
/* Status pill + evidence line (DESK11b header, contract item 1) — replaces
   the retired standalone verified-status badge (TASTE §1: the collapsed
   disclosure's own one-line summary, #sd-disc-summary below, already
   carries that same patient-link signal at a glance). */
.dh-status { display: flex; align-items: center; gap: 8px; margin-top: 6px; flex-wrap: wrap; }
.dh-evidence { font-size: 12.5px; color: var(--ink-faint); }
.dh-evidence:empty { display: none; }
/* ROOMSFIX2 B208 — a 44×44 hit box at every width; the glyph keeps its size
   and spot (the negative margins hold the old 26-px box's centre, so the
   header row does not grow). */
.sched-drawer-x { border: 0; background: none; cursor: pointer; color: var(--ink-soft); font-size: 15px;
                   display: inline-flex; align-items: center; justify-content: center; flex: none;
                   width: 44px; height: 44px; padding: 0; border-radius: var(--radius-sm); line-height: 1;
                   margin: -9px -9px -9px auto; }
.sched-drawer-x:hover { background: var(--surface-2); color: var(--ink); }
.sched-drawer-body { padding: 16px 20px 18px; overflow-y: auto; flex: 1; min-height: 0; }
/* Hairline-divided sections (DESIGN.md "one level of surface" — never a
   bordered box inside another) replace the old single `.appt-card` box +
   `.drawer-section-label` row; "drawer-" prefixed like every other class
   in this component (never bare `.sec`, this stylesheet is 2000+ lines). */
.drawer-sec { padding: 14px 0; border-bottom: 1px solid var(--line); }
.drawer-sec:first-child { padding-top: 2px; }
.drawer-sec.actions { border-bottom: 0; padding-bottom: 0; }
.drawer-sec-label { font-size: 11px; font-weight: 640; letter-spacing: 0.05em; text-transform: uppercase;
                     color: var(--ink-faint); margin: 0 0 8px; }
.dkv { display: grid; grid-template-columns: 96px 1fr; gap: 10px 14px; font-size: 13.5px; margin-bottom: 20px; }
.dkv dt { color: var(--ink-soft); }
.dkv dd { margin: 0; font-weight: 550; }
.dkv dd .faint { color: var(--ink-faint); font-weight: 500; }
.dkv dd .est { color: var(--ink-soft); font-weight: 500; }
/* The When row's date and the Length row's estimate explanation both ride
   the SAME line as their primary value at desktop, a CSS-only "· "
   separator (never baked into the DOM text) so the mobile override below
   can drop straight to its own line with nothing left dangling. */
.dkv dd .date::before, .dkv dd .why::before { content: '· '; }
/* The primary When/Length/Service/With list only — the disclosure body's
   own OSCAR-chart/patient-link/etc list (longer labels) keeps the wider
   96px column above, untouched. */
.drawer-sec > .dkv { grid-template-columns: 72px 1fr; margin: 0; }
/* Booking source (DESK11b contract item 2) — icon tile · label · one
   detail line · one link, renders `a.source` verbatim, never a guess. */
.src { display: flex; gap: 12px; align-items: flex-start; }
.src-ic { width: 36px; height: 36px; border-radius: var(--radius); background: var(--accent-soft);
          color: var(--accent-ink); display: flex; align-items: center; justify-content: center; flex: none; }
.src-ic svg { width: 18px; height: 18px; }
.src-ic.plain { background: var(--surface-2); color: var(--ink-soft); }
.src-lbl { font-size: 13.5px; font-weight: 600; line-height: 1.3; }
.src-det { font-size: 12.5px; color: var(--ink-soft); margin-top: 3px; line-height: 1.45; }
.src-det .lnk { color: var(--accent-ink); font-weight: 600; text-decoration: none; white-space: nowrap; }
.src-det .lnk:hover { text-decoration: underline; }
.src-det .sep { color: var(--line-strong); margin: 0 6px; }
.src-missing { font-size: 12.5px; color: var(--ink-faint); margin: 0; }
/* Contact */
.contact-row { display: flex; align-items: baseline; gap: 8px; font-size: 13.5px; flex-wrap: wrap; }
.contact-row .num { font-weight: 550; font-variant-numeric: tabular-nums; }
.contact-row .lnk { margin-left: auto; color: var(--accent-ink); font-weight: 600; font-size: 12.5px; text-decoration: none; white-space: nowrap; }
.contact-row .lnk:hover { text-decoration: underline; }
/* B214 (DESKFOLLOW move 1): at phone width the visit sheet's
   'Open in Conversations' link is a 44 px tall thumb target. */
@media (max-width: 760px) { #sd-open-convo { display: inline-flex; align-items: center; min-height: 44px; } }
.contact-msg { font-size: 12.5px; color: var(--ink-soft); margin: 10px 0 0; line-height: 1.45; }
.contact-msg .faint { color: var(--ink-faint); }
.contact-msg.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dnote { font-size: 13px; color: var(--ink-soft); background: var(--surface); border-radius: 8px;
         padding: 10px 12px; margin-bottom: 18px; line-height: 1.5; }
/* Secondary-detail disclosure (SYNTHESIS #31) — chevron · label ·
   one-line summary, same shape as settings-texts.js's automation() rows,
   scoped here rather than sharing that class since this one has no
   preview pane. */
/* Desk-disclosure-sweep (2026-09-12): the drawer's single disclosure row
   now reuses the shared `.disc`/`.disc-body` grammar (settings-business.js's
   `window.deskDisclosureRow`) instead of its own `.sched-disclosure`
   `<details>` — this wrapper just keeps the standalone bordered-box look a
   lone disclosure needs outside a `.disc-list` card. */
.sched-disclosure-host { border: 1px solid var(--line); border-radius: 8px; margin: 10px 0 0; overflow: hidden; }
.sched-disclosure-host .disc { border-top: 0; padding: 10px 12px; min-height: 0; }
/* The shared `.disc-lbl` column is sized for Business tab's short field
   names (Name/Phone/Website…) — "Patient link & booking check" is longer,
   same widen-just-this-page's-own-rows pattern settings-access.js already
   uses for its email-length labels. */
.sched-disclosure-host .disc-lbl { width: auto; }
.sched-disclosure-host .disc-body { border-top: 1px solid var(--line); padding: 4px 12px 12px; }
.sched-disclosure-host .disc-body .dkv { margin: 12px 0 0; }
.sched-disclosure-host .disc-body .dnote { margin-bottom: 0; margin-top: 12px; }
.drawer-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 16px; }
/* DW-04 (2026-09-11, owner's-walk audit) — the shared `.btn:disabled` rule
   in desk.html's inline <style> (opacity: .5) reads as "still basically a
   live button" at this specific size/contrast; a flat muted fill removes
   any doubt without touching the base .btn component used everywhere else
   in the app. `cursor: not-allowed` (kept from the base rule, stronger
   signal than plain `default`) + no hover-highlight + a `title` set in
   schedule.js's openDrawer() are the other three legs of "looks disabled,
   says why". */
.drawer-actions .btn:disabled { opacity: 1; background: var(--surface-2); color: var(--ink-faint);
                                 border-color: var(--line); cursor: not-allowed; }
.drawer-actions .btn:disabled:hover { background: var(--surface-2); }
/* Cancel's destructive-outline treatment (DESK11b approved comp) — reuses
   the existing `--danger` token (already in :root, never a new colour);
   the explicit `:disabled` override keeps DW-04's "looks disabled" bar
   when Cancel is ALSO locked, since two 3-class selectors tie on
   specificity and this one must win regardless of source order. */
.drawer-actions .btn.danger-ghost { border-color: var(--danger); color: var(--danger); }
.drawer-actions .btn.danger-ghost:hover { background: var(--danger-soft); }
.drawer-actions .btn.danger-ghost:disabled { background: var(--surface-2); color: var(--ink-faint); border-color: var(--line); }
.drawer-honest { font-size: 12.5px; color: var(--ink-faint); border-top: 1px solid var(--line);
                 padding-top: 14px; line-height: 1.5; }
/* Move/Cancel real-write form (2026-09-04, NO-EMR DESK change) — same
   inline-row register as .drawer-actions above, plain inputs, no chrome. */
.drawer-move-form { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin: -6px 0 16px; }
.drawer-move-form input { border: 1px solid var(--line); border-radius: 8px; padding: 6px 8px;
                           font-size: 13px; color: var(--ink); background: var(--ground); }
.drawer-action-error { font-size: 12.5px; color: var(--warn); margin: -8px 0 12px; }
.appt-status-word { display: inline-block; font-size: 11px; font-weight: 600; padding: 2px 9px;
                     border-radius: 999px; background: var(--surface-2); color: var(--ink-soft); }

/* Thread bubbles reuse desk.html's OWN `.msgs`/`.msgrow`/`.bubble`/
   `.msg-meta` (the Inbox room's section there) — only the loading/empty
   notes and a "sending…" pending tint are here. */
.msgs-loading, .msgs-empty-note { margin: 0; padding: 8px 0; font-size: 13px; line-height: 18px; font-weight: 500; color: var(--ink-soft); }
.msgrow.pending .bubble { opacity: 0.65; }

/* ------------------------------------------------------------- composer
   DESKG2 (2026-09-26) — comp A: a strip under the thread, border-top only.
   Head row: the Write it / Tell Bea segment (the g1 kit's segmented
   control) + the saved-reply select at the row's end; a 72px message box;
   then the note, the 0/480 count and Send on one line. Tell Bea: the
   instruction field, its note and Draft it. The draft (not drawn) follows
   the same grammar: a dashed warn bubble at right, its label and actions
   under it. */
.composer { flex: none; display: flex; flex-direction: column; gap: 8px; padding: 12px 16px 16px;
  border-top: 1px solid var(--line); background: var(--ground); }
.composer-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.composer-tabs.g1-segmented { flex: none; }
.composer-saved-replies { flex: none; }
.composer-saved-replies .g1-select { width: 240px; }
.composer-pane { display: none; }
.composer-pane.on { display: flex; flex-direction: column; gap: 8px; }
.composer textarea, .composer input[type="text"] {
  width: 100%; box-sizing: border-box; border: 1px solid var(--line); border-radius: var(--radius-control);
  padding: 8px 12px; background: var(--ground); color: var(--ink);
  font: inherit; font-size: 13px; line-height: 18px; font-weight: 400;
}
.composer textarea::placeholder, .composer input[type="text"]::placeholder { color: var(--ink-faint); }
.composer textarea:focus-visible, .composer input[type="text"]:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.composer input[type="text"] { height: 36px; }
.composer textarea { height: 72px; min-height: 72px; resize: vertical; }
.composer-row { display: flex; align-items: center; gap: 12px; }
.composer-row > button { flex: none; }
.composer-note { flex: 1; min-width: 0; margin: 0; font-size: 12px; line-height: 16px; font-weight: 500; color: var(--ink-soft); }
.composer-counter { flex: none; font-size: 12px; line-height: 16px; font-weight: 500; color: var(--ink-soft); font-variant-numeric: tabular-nums; }
.composer-error { font-size: 12px; line-height: 16px; font-weight: 500; color: var(--danger); }
.draft-wrap { display: flex; flex-direction: column; gap: 8px; }
.draft-wrap[hidden] { display: none; }
.draft-label { font-size: 12px; line-height: 16px; font-weight: 500; color: var(--warn); }
.draft-bubble { align-self: flex-end; max-width: 85%; box-sizing: border-box; padding: 8px 12px;
  border: 1px dashed var(--warn); border-radius: var(--radius-card); background: var(--warn-soft); color: var(--ink);
  font-size: 14px; line-height: 20px; font-weight: 400; white-space: pre-wrap; overflow-wrap: anywhere; }
.draft-actions { display: flex; align-items: center; justify-content: flex-end; gap: 8px; }
.draft-actions .draft-label { flex: 1; min-width: 0; margin: 0; text-align: left; }
.draft-actions > button { flex: none; }
/* "Check status" after an unresolved send: a text button inside the meta line */
.msg-meta .composer-check { min-height: 0; height: auto; padding: 0; border: 0; background: none; cursor: pointer;
  font: inherit; font-size: 12px; line-height: 16px; font-weight: 500; color: var(--accent-ink); }
@media (max-width: 760px) {
  /* 390: stacked — segment, select 44 full
     width, the box, note + count on one line, Send full width. */
  .composer-head { flex-direction: column; align-items: stretch; }
  .composer-tabs.g1-segmented { align-self: flex-start; height: auto; }
  .composer-tabs.g1-segmented button { min-height: 44px; padding: 0 12px; }
  .composer-saved-replies .g1-select { width: 100%; }
  .composer-pane.on .composer-row { flex-wrap: wrap; gap: 8px 12px; }
  .composer-pane.on .composer-row > button { flex: 1 1 100%; height: 44px; }
  .composer input[type="text"] { height: 44px; }
  .msg-meta .composer-check { min-height: 44px; }
}

/* -------------------------------------------------------------- states */
.sched-state-empty { border: 1px solid var(--line); border-radius: 10px; background: var(--ground);
                      padding: 64px 32px; text-align: center; max-width: 460px; margin: 40px auto; }
.sched-state-empty .se-icon { width: 44px; height: 44px; border-radius: 50%; background: var(--surface-2);
                                display: flex; align-items: center; justify-content: center; margin: 0 auto 16px;
                                color: var(--ink-faint); }
.sched-state-empty h3 { font-size: 16px; font-weight: 620; margin-bottom: 6px; }
.sched-state-empty p { font-size: 13.5px; color: var(--ink-soft); line-height: 1.5; margin-bottom: 16px; }
.sched-state-empty a.btn { text-decoration: none; display: inline-block; }

.unreach-banner { display: flex; align-items: center; gap: 10px; background: var(--warn-soft);
                  border: 1px solid color-mix(in srgb, var(--warn) 34%, transparent); border-radius: 8px;
                  padding: 10px 14px; font-size: 13px; color: var(--warn); font-weight: 550; margin-bottom: 14px; }
.unreach-banner button { margin-left: auto; }

@media (max-width: 760px) {
  .sched-shell.daybox { max-height: calc(100vh - 300px - var(--chrome-banner-h, 0px)); }
  .sched-options .chip, .sched-options > summary { min-height: 44px; }
}
@media (max-width: 480px) {
  .viewhead.sched-header { gap: 8px; }
  .provcol-head .pmeta { display: none; }
  .sched-chips { flex-wrap: nowrap; overflow-x: auto; padding-bottom: 2px; }
  .sched-chips .chip { flex: none; }
  /* DESK11b — content-sized bottom sheet (never full-height): height:auto
     already sizes to content, 82vh is only the ceiling for a genuinely
     tall visit (a resolve panel, say) so it scrolls internally rather
     than running off-screen. */
  .sched-drawer { width: 100%; height: auto; max-height: 82vh; align-self: flex-end; margin-top: auto;
                  border: 0; border-top: 1px solid var(--line); border-radius: 14px 14px 0 0; position: relative; }
  .sched-overlay { align-items: flex-end; padding: 0; }
  .sched-drawer-head { padding: 14px 18px 12px; }
  /* Drag-handle affordance (approved comp) — visual only; the sheet still
     closes ONLY via ✕ / Esc / click-outside, same as desktop (contract
     item 5, "keep today's open/close behaviour exactly"). */
  .sched-drawer-head::before { content: ''; position: absolute; left: 50%; top: 6px; width: 36px; height: 4px;
                                border-radius: 2px; background: var(--line-strong); transform: translateX(-50%); }
  .sched-drawer-body { padding: 14px 18px calc(18px + env(safe-area-inset-bottom)); }
  .drawer-actions .btn { min-height: 44px; flex: 1; }
  /* "date and length explanation drop to their own line (nothing
     truncated, no orphans)" — contract item 1. */
  .drawer-sec > .dkv { grid-template-columns: 64px 1fr; }
  .dkv dd .date, .dkv dd .why { display: block; margin-top: 2px; }
  .dkv dd .date::before, .dkv dd .why::before { content: none; }
  .src-det .sep { display: none; }
  .src-det .lnk { display: block; margin-top: 4px; }
}

/* =================================================================
   Outreach + Results rooms (2026-09-03) — public/desk/outreach.js and
   public/desk/results.js. Same tokens/type scale/component grammar the
   shipped desk already uses (--accent, hairline --line borders,
   .widget/.btn/.viewhead/.empty2/.count reused as-is elsewhere in this
   file).
   Every new component below is prefixed
   `oc-` (Outreach) or `res-` (Results) so it can never collide with the
   Today room's own `.stat`/`.chip` classes defined earlier in this file
   and in desk.html's inline <style>.
   ================================================================= */

/* -- status band
      -- replaces the old .toplead2 lead line + .oc-mode-banner (both
      retired: the mode banner carried SYNTHESIS.md §E-banned "Shadow
      mode…" copy, and the plain lead line is now this band's own
      .oc-status-lead). Always visible, in every mode. */
.oc-status-band { padding: 16px 18px; margin-bottom: 18px; }
.oc-status-top { display: flex; align-items: baseline; gap: 12px; }
.oc-status-lead { font-size: 16px; font-weight: 640; letter-spacing: -.01em; flex: 1; margin: 0; }
.oc-status-pill { flex: none; font-size: 12px; font-weight: 600; color: var(--ink-soft);
                   background: var(--surface-2); border: 1px solid var(--line-strong);
                   border-radius: 999px; padding: 3px 11px; white-space: nowrap; }
.oc-status-sub { font-size: 13px; color: var(--ink-soft); margin: 8px 0 0; line-height: 1.5; max-width: 62ch; }
.oc-status-rules { display: block; margin-top: 10px; font-size: 12.5px; text-align: left; }
.oc-empty { border: 1px dashed var(--line-strong); border-radius: 10px; background: var(--ground);
            padding: 40px 24px; text-align: center; max-width: 460px; margin: 24px auto; }
.oc-empty h3 { font-size: 15px; font-weight: 620; margin: 0 0 6px; }
.oc-empty p { font-size: 13px; color: var(--ink-soft); line-height: 1.5; margin: 0 0 14px; }

/* -- slot grid (level 1) -- */
.oc-slot-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; margin-bottom: 18px; }
@media (max-width: 1180px) { .oc-slot-grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 760px) { .oc-slot-grid { grid-template-columns: 1fr; } }
.oc-slot-card { display: block; text-align: left; width: 100%; border: 1px solid var(--line);
                border-radius: 10px; background: var(--ground); padding: 0; cursor: pointer; overflow: hidden; }
.oc-slot-card:hover { border-color: var(--line-strong); background: var(--surface); }
.oc-slot-card:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.oc-card-body { padding: 15px 16px 0; }
.oc-sc-top { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
.oc-sc-prac { font-size: 15.5px; font-weight: 640; }
.oc-sc-date { font-size: 12px; color: var(--ink-faint); white-space: nowrap; flex: none; }
.oc-sc-time { font-size: 13.5px; font-weight: 620; margin-top: 6px; font-variant-numeric: tabular-nums; }
.oc-sc-service { font-size: 12.5px; color: var(--ink-soft); margin-top: 3px; }
.oc-sc-source { font-size: 11.5px; color: var(--ink-faint); margin-top: 3px; padding-bottom: 12px; }
.oc-sc-status { margin: 0; padding: 9px 16px; font-size: 12px; font-weight: 640; border-top: 1px solid var(--line);
                background: var(--surface); color: var(--ink-soft); }
.oc-status-offering { background: var(--accent-soft); color: var(--accent-ink); }
.oc-status-filled { background: var(--surface-2); color: var(--ink-soft); }
.oc-status-nobody_fit, .oc-status-held { background: var(--warn-soft); color: var(--warn); }
.oc-status-expired { background: var(--surface-2); color: var(--ink-faint); }
.oc-status-scanning { background: var(--surface-2); color: var(--ink-faint); }

/* -- slot view (level 2) -- */
.oc-slot-header { margin-bottom: 14px; }
.oc-slot-header .oc-card-body { padding: 15px 16px; }
.oc-funnel-card { margin-bottom: 14px; }
.oc-funnel-head { display: flex; align-items: baseline; gap: 8px; padding: 11px 16px; border-bottom: 1px solid var(--line); }
.oc-funnel-title { font-size: 13.5px; font-weight: 620; color: var(--ink); margin: 0; flex: 1; }
.oc-funnel-time { font-size: 11.5px; color: var(--ink-faint); font-weight: 550; white-space: nowrap; font-variant-numeric: tabular-nums; }
.oc-funnel-bars { display: flex; align-items: flex-end; gap: 4px; padding: 20px 16px 14px; }
.oc-funnel-col { flex: 1 1 0; display: flex; flex-direction: column; align-items: center; min-width: 0; position: relative; }
.oc-funnel-bar-btn { border: 0; background: none; padding: 0; width: 100%; display: flex; flex-direction: column;
                      align-items: center; cursor: pointer; }
.oc-funnel-bar-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
.oc-fb-num { font-size: 13px; font-weight: 660; color: var(--ink); margin-bottom: 3px; font-variant-numeric: tabular-nums; }
.oc-fb-shape { width: 72%; min-height: 4px; border-radius: 5px 5px 3px 3px; background: var(--accent); opacity: .55; }
.oc-funnel-bar-btn[aria-expanded="true"] .oc-fb-shape { opacity: 1; }
.oc-fb-label { font-size: 11px; color: var(--ink-soft); margin-top: 5px; text-align: center; line-height: 1.25; }
.oc-funnel-detail { margin-top: 8px; padding: 8px; border: 1px solid var(--line-strong); border-radius: 8px;
                     background: var(--surface); font-size: 11.5px; color: var(--ink-soft); width: 100%; text-align: left; }
.oc-fd-row { padding: 3px 0; }
.oc-fd-more { color: var(--ink-faint); margin-top: 2px; }
.oc-fd-empty { color: var(--ink-faint); }
@media (max-width: 760px) {
  .oc-funnel-bars { flex-direction: column; align-items: stretch; gap: 10px; }
  .oc-funnel-col { flex-direction: row; align-items: center; gap: 10px; }
  .oc-funnel-bar-btn { flex-direction: row; align-items: center; gap: 10px; }
  .oc-fb-num { margin-bottom: 0; width: 26px; text-align: right; }
  .oc-fb-shape { height: 12px !important; width: auto; flex: 1; }
  .oc-fb-label { margin-top: 0; text-align: left; }
}

.oc-prospect-columns { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; }
@media (max-width: 760px) { .oc-prospect-columns { grid-template-columns: 1fr; } }
.oc-plane { border: 1px solid var(--line); border-top: 3px solid var(--line-strong); border-radius: 10px;
            background: var(--ground); padding: 10px 12px 12px; }
.oc-plane-reached { border-top-color: var(--accent); }
.oc-plane-skip { border-top-color: var(--warn); }
.oc-plane-head { display: flex; align-items: baseline; gap: 7px; margin-bottom: 8px; }
.oc-plane-head h4 { font-size: 11.5px; font-weight: 640; letter-spacing: .03em; text-transform: uppercase;
                     color: var(--ink-soft); margin: 0; }
.oc-plane-reached .oc-plane-head h4 { color: var(--accent-ink); }
.oc-plane-skip .oc-plane-head h4 { color: var(--warn); }
.oc-pc-count { font-size: 11px; color: var(--ink-faint); font-variant-numeric: tabular-nums; }
.oc-plane-cards { display: flex; flex-direction: column; gap: 8px; }
.oc-plane-empty { font-size: 11.5px; color: var(--ink-faint); padding: 4px 2px; }
.oc-mini-card { border: 1px solid var(--line); border-radius: 8px; background: var(--surface);
                padding: 7px 10px; text-align: left; cursor: pointer; width: 100%; display: flex;
                flex-direction: column; gap: 3px; }
.oc-mini-card:hover { background: var(--surface-2); border-color: var(--line-strong); }
.oc-mini-card:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.oc-mc-row1 { display: flex; align-items: baseline; gap: 6px; }
.oc-mc-rank { flex: none; font-size: 11px; font-weight: 640; color: var(--ink-faint); font-variant-numeric: tabular-nums;
              white-space: nowrap; max-width: 40%; overflow: hidden; text-overflow: ellipsis; }
.oc-mc-name { flex: 1; min-width: 0; font-size: 12.5px; font-weight: 620; color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.oc-mc-phone { flex: none; font-size: 11px; color: var(--ink-faint); font-variant-numeric: tabular-nums; margin-left: auto; }
.oc-mc-when { font-size: 11px; color: var(--ink-soft); line-height: 1.4; }

/* -- mode badge (two-mode outreach, 2026-09-03) -- */
.oc-mode-badge { font-size: 11.5px; font-weight: 620; color: var(--ink-soft); margin: 0; padding: 8px 16px 0; }

/* -- fishing batch grouping (2026-09-03) -- */
.oc-batch-group { border: 1px dashed var(--line-strong); border-radius: 8px; padding: 8px; display: flex;
                   flex-direction: column; gap: 6px; }
.oc-batch-head { font-size: 11px; font-weight: 640; color: var(--ink-faint); text-transform: uppercase; letter-spacing: .03em; }
.oc-batch-cards { display: flex; flex-direction: column; gap: 6px; }
.oc-batch-cards .oc-mini-card { border-color: var(--line); }

/* -- Filling ahead (2026-09-03, two-mode outreach; header restyled
      2026-09-12 to the SAME .oc-wbd-head grammar "What Bea did" uses —
      see below — never its own bar/toggle) -- */
.oc-fa-body { border-top: 1px solid var(--line); padding: 14px 16px; }
.oc-fa-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; }
@media (max-width: 760px) { .oc-fa-grid { grid-template-columns: 1fr; } }
.oc-fa-card { display: block; text-align: left; width: 100%; border: 1px solid var(--line); border-radius: 10px;
              background: var(--ground); padding: 10px 12px; cursor: pointer; display: flex; flex-direction: column; gap: 5px; }
.oc-fa-card:hover { border-color: var(--line-strong); background: var(--surface); }
.oc-fa-card:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.oc-fa-due { font-size: 11.5px; color: var(--ink-soft); line-height: 1.4; }
.oc-fa-options { display: flex; flex-direction: column; gap: 2px; }
.oc-fa-opt { font-size: 11.5px; color: var(--ink); font-variant-numeric: tabular-nums; }
.oc-fa-when { font-size: 11px; font-weight: 620; color: var(--accent-ink); }
.oc-fa-budget { font-size: 11.5px; color: var(--ink-faint); margin: 12px 0 0; }

/* -- "What Bea did" (Now + Results tabs, 2026-09-12, room-bea comp)
      -- replaces the old collapsible "Live agent
      activity" toggle (.oc-activity-*, retired) with an always-visible
      card, rows grouped under their own calendar-day header. Shared by
      outreach.js's renderWhatBeaDid() and results.js's own copy. */
.oc-wbd-card, .res-wbd-card { margin-top: 4px; }
.oc-wbd-head, .res-wbd-head { display: flex; align-items: baseline; gap: 10px;
                    padding: 14px 18px 12px; border-bottom: 1px solid var(--line); }
.oc-wbd-head h3, .res-wbd-head h3 { font-size: 15px; font-weight: 640; letter-spacing: -.01em; margin: 0; flex: 1; }
.oc-wbd-head .count, .res-wbd-head .count { font-size: 12.5px; color: var(--ink-faint); }
.oc-wbd-body, .res-wbd-body { max-height: 420px; overflow-y: auto; }
.wbd-day { padding: 8px 18px; font-size: 12px; font-weight: 640; color: var(--ink-soft);
           background: var(--surface-2); }
.wbd-row { display: flex; gap: 14px; padding: 10px 18px; border-top: 1px solid var(--line); align-items: baseline; }
.wbd-day + .wbd-row { border-top: 0; }
.wbd-row.warn { background: var(--warn-soft); }
.wbd-time { flex: none; min-width: 58px; font-size: 12px; color: var(--ink-faint); font-variant-numeric: tabular-nums; white-space: nowrap; }
.wbd-text { flex: 1; font-size: 13px; color: var(--ink-soft); line-height: 1.5; }
.wbd-text b { color: var(--ink); font-weight: 640; }

/* -- prospect modal body + rules modal body (rendered inside the shared
      #modal shell desk.js's openModal() already owns) -- */
.oc-pm-when { font-size: 13px; font-weight: 620; color: var(--ink); margin: 0 0 12px; }
.oc-pm-why { display: flex; flex-direction: column; gap: 7px; background: var(--accent-soft);
             border-radius: 8px; padding: 12px 14px; margin-bottom: 16px; }
.oc-pm-why-line { font-size: 13px; color: var(--ink); line-height: 1.5; margin: 0; padding-left: 14px; position: relative; }
.oc-pm-why-line::before { content: '\2022'; position: absolute; left: 0; color: var(--accent-ink); font-weight: 700; }
.oc-pm-table { width: 100%; border-collapse: collapse; font-size: 12.5px; }
.oc-pm-table th { text-align: left; font-size: 11px; font-weight: 640; text-transform: uppercase; letter-spacing: .03em;
                   color: var(--ink-faint); padding: 0 10px 6px 0; border-bottom: 1px solid var(--line); }
.oc-pm-table td { padding: 7px 10px 7px 0; border-bottom: 1px solid var(--line); color: var(--ink); }
.oc-pm-table tr:last-child td { border-bottom: 0; }
.oc-pm-caption { font-size: 11.5px; color: var(--ink-faint); margin-top: 8px; }
.oc-pm-empty { font-size: 12.5px; color: var(--ink-faint); }
.oc-pm-foot { border-top: 1px solid var(--line); padding-top: 14px; margin-top: 14px; }
.oc-pm-foot a { font-size: 12.5px; font-weight: 620; text-decoration: none; }
.oc-pm-foot a:hover { text-decoration: underline; }

.oc-rule-row { padding: 13px 0; border-bottom: 1px solid var(--line); }
.oc-rule-row:last-child { border-bottom: 0; }
.oc-rule-top { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
.oc-rule-label { font-size: 13px; font-weight: 640; }
.oc-rule-floor-tag { font-size: 11px; font-weight: 640; color: var(--ink-faint); text-transform: uppercase; letter-spacing: .03em; white-space: nowrap; }
.oc-rule-value { font-size: 12.5px; color: var(--ink); margin-top: 3px; font-variant-numeric: tabular-nums; }
.oc-rule-note { font-size: 12px; color: var(--ink-soft); margin-top: 4px; line-height: 1.5; }

/* -- Results header + stat tiles -- */
.res-period-seg { display: inline-flex; border: 1px solid var(--line-strong); border-radius: 999px; padding: 3px;
                   background: var(--surface); gap: 2px; }
.res-period-seg button { border: 0; background: none; padding: 6px 13px; border-radius: 999px; font-size: 12.5px;
                          font-weight: 600; color: var(--ink-soft); cursor: pointer; }
.res-period-seg button[aria-pressed="true"] { background: var(--ground); color: var(--ink); box-shadow: var(--shadow-lift); }
.res-period-seg button:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

.res-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; margin-bottom: 14px; }
.res-stat { border: 1.5px solid var(--line-strong); border-radius: 10px; padding: 14px 16px; background: var(--ground);
            box-shadow: var(--shadow-lift); text-align: left; width: 100%; display: block; min-width: 0; }
.res-stat .n { font-size: 27px; font-weight: 660; font-variant-numeric: tabular-nums; line-height: 1.1; }
.res-stat .l { font-size: 12px; color: var(--ink-soft); margin-top: 4px; }
.res-stat .sub { font-size: 12px; color: var(--ink-faint); margin-top: 6px; line-height: 1.4; }
.res-stat .sub a { font-size: 12px; }
.res-stat.accent { border-color: var(--accent); background: var(--accent-soft); }
.res-stat.accent .n, .res-stat.accent .l { color: var(--accent-ink); }
.res-stat.accent .sub { color: var(--accent-ink); opacity: .85; }
.res-stat.clickable { cursor: pointer; position: relative; }
.res-stat.clickable[aria-expanded="true"] { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-soft), var(--shadow-lift); }
/* Chevron affordance (2026-09-12, SYNTHESIS §A row 28) — the ONE clickable
   tile gets a real visual cue, top-right, never just a changed cursor. */
.res-stat-chevron { position: absolute; top: 14px; right: 14px; color: var(--ink-faint); display: inline-flex; }
@media (max-width: 760px) { .res-stats { grid-template-columns: repeat(2, 1fr); } }

/* -- Agent results widget -- */
.res-results-widget { margin-bottom: 22px; }
.res-results-head { padding: 14px 18px 12px; border-bottom: 1px solid var(--line); }
.res-results-head h3 { font-size: 15px; font-weight: 640; letter-spacing: -.01em; margin: 0 0 2px; }
.res-results-head p { font-size: 12.5px; color: var(--ink-faint); margin: 0; }
.res-results-body { padding: 18px; display: flex; flex-direction: column; gap: 24px; }
.res-block-label { font-size: 11.5px; font-weight: 640; letter-spacing: .04em; text-transform: uppercase;
                    color: var(--ink-soft); margin: 0 0 12px; }

.res-rb-chart { display: flex; align-items: flex-end; gap: 5px; height: 132px; }
.res-rb-col { flex: 1 1 0; display: flex; flex-direction: column; align-items: center; justify-content: flex-end; height: 100%; min-width: 0; }
.res-rb-track { width: 62%; display: flex; align-items: flex-end; justify-content: center; height: 100px; }
.res-rb-bar { width: 100%; border-radius: 4px 4px 2px 2px; background: color-mix(in srgb, var(--accent) 14%, var(--surface-2));
              display: flex; align-items: flex-end; }
.res-rb-fill { width: 100%; background: var(--accent); border-radius: 4px 4px 0 0; }
.res-rb-col[data-empty="true"] .res-rb-bar { background: var(--surface-2); }
.res-rb-day { font-size: 11px; color: var(--ink-faint); margin-top: 6px; text-align: center; }
.res-rb-legend { display: flex; align-items: center; gap: 16px; margin-top: 12px; font-size: 11.5px; color: var(--ink-soft); }
.res-rb-legend-item { display: flex; align-items: center; gap: 6px; }
.res-rb-swatch { width: 10px; height: 10px; border-radius: 2.5px; display: inline-block; }
.res-rb-swatch.cancelled { background: color-mix(in srgb, var(--accent) 14%, var(--surface-2)); border: 1px solid var(--line-strong); }
.res-rb-swatch.refilled { background: var(--accent); }

.res-conv-strip { display: flex; align-items: stretch; gap: 8px; flex-wrap: wrap; }
.res-conv-step { flex: 1 1 120px; border: 1px solid var(--line); border-radius: 8px; background: var(--surface);
                  padding: 12px 14px; text-align: center; min-width: 0; }
.res-conv-step .cs-n { font-size: 21px; font-weight: 660; font-variant-numeric: tabular-nums; color: var(--ink); }
.res-conv-step .cs-l { font-size: 11.5px; color: var(--ink-soft); margin-top: 3px; }
.res-conv-arrow { flex: none; display: flex; align-items: center; justify-content: center; color: var(--ink-faint); font-size: 14px; }

.res-source-table { display: flex; flex-direction: column; gap: 10px; }
.res-source-row { display: flex; align-items: center; gap: 12px; }
.res-source-label { flex: 0 0 168px; font-size: 12.5px; color: var(--ink); }
.res-source-track { flex: 1 1 auto; height: 8px; border-radius: 999px; background: var(--surface-2); overflow: hidden; }
.res-source-fill { height: 100%; background: var(--accent); border-radius: 999px; }
.res-source-n { flex: 0 0 auto; min-width: 20px; text-align: right; font-size: 12.5px; font-weight: 640;
                font-variant-numeric: tabular-nums; color: var(--ink); }
@media (max-width: 760px) { .res-source-label { flex-basis: 128px; font-size: 12px; } }

.res-trust-lines { display: flex; flex-direction: column; gap: 8px; }
.res-trust-row { font-size: 12.5px; color: var(--ink); }
.res-time-saved-line { font-size: 12.5px; color: var(--ink); background: var(--accent-soft); border-radius: 8px;
                        padding: 10px 14px; line-height: 1.5; }
.res-time-saved-line b { color: var(--accent-ink); font-weight: 660; }
.res-ts-note { color: var(--ink-soft); }

/* -- WO-8C — cohort/coverage/lift/cost panels (Results room) -- */
.res-cohort-widget { margin-bottom: 22px; }
.res-cohort-panel { display: flex; flex-direction: column; gap: 8px; }
.res-cohort-box { font-size: 12.5px; color: var(--ink); display: flex; flex-direction: column; gap: 4px; }
.res-cohort-arms { display: flex; gap: 16px; font-weight: 640; }
.res-lift-n { font-size: 27px; font-weight: 660; font-variant-numeric: tabular-nums; line-height: 1.1; }
.res-cost-amount { font-size: 21px; font-weight: 660; font-variant-numeric: tabular-nums; color: var(--ink); }
.res-unavailable { font-size: 12.5px; color: var(--ink-faint); font-style: italic; }

/* -- Cancellations drill-down panel (Results' one surviving stat detail) -- */
.stat-detail { border: 1px solid var(--line); border-radius: 10px; background: var(--ground); margin-bottom: 22px; overflow: hidden; }
.sd-head { display: flex; align-items: baseline; gap: 8px; padding: 11px 16px; border-bottom: 1px solid var(--line); background: var(--surface); }
.sd-head h3 { font-size: 12.5px; font-weight: 640; letter-spacing: .04em; text-transform: uppercase; color: var(--ink-soft); margin: 0; flex: 1; }
.sd-close { border: 0; background: none; color: var(--ink-faint); cursor: pointer; font-size: 17px; line-height: 1;
            padding: 2px 4px; border-radius: var(--radius-sm); }
.sd-close:hover { background: var(--surface-2); color: var(--ink); }
.sd-list { display: flex; flex-direction: column; max-height: 340px; overflow-y: auto; }
.sd-row { padding: 10px 16px; border-top: 1px solid var(--line); font-size: 12.5px; line-height: 1.5; }
.sd-row:first-child { border-top: 0; }
.sd-row b { font-weight: 620; color: var(--ink); }
.sd-row .sd-sub { color: var(--ink-soft); }
.sd-empty { padding: 14px 16px; font-size: 12.5px; color: var(--ink-faint); }

/* ---------------------------------------------------- Appointments room
   DESKG2-appointments (2026-09-26) — rebuilt to comp A. Grammar: page header
   (title, the answer sentence, ONE action: Export CSV) -> ONE filter row
   (search, Status, When, "Show how they were booked" at the right end) ->
   ONE frame: surface header strip, 48 px hairline rows, status tag, ⋯ last
   -> "Showing 1–20 of N" + Previous/Next as a plain line under the frame.
   The frame ends at its content (no viewport-pinned inner scroll). At
   <=1023 each visit is a two-line row (name over "When, Service") with the
   status tag at the right of line 1; the ⋯ stays at 768–1023 and goes on a
   phone (the row itself is the tap). Colours are theme tokens through the
   g1 aliases; corners only --radius-control / --radius-card. */
.apt-toolbar { display: flex; align-items: center; gap: 8px; }
.apt-search { width: 280px; min-width: 0; }
/* DOCKCOPY1 floor kept: the Export button is never under the 44 px touch
   target on a phone (the kit's --g1-ctl is 44 there, 36 on a desktop). */
.apt-export { min-height: var(--g1-ctl); flex: none; }
/* The header's one action. #room-header is shell-owned (desk.js) and its
   right slot only takes a control on Bea/Settings, so the button lives in
   this room's filter row and is lifted into the header row here: the
   positioned ancestor is main#desk-main (padding 24 top, gutter right),
   the same box whose padding places the header title. */
@media (min-width: 1024px) {
  .apt-export { position: absolute; top: 24px; right: var(--g1-gutter); }
}
@media (min-width: 768px) and (max-width: 1023px) {
  .apt-toolbar { flex-wrap: wrap; }
  .apt-export { order: 99; }
}
.apt-attr-summary-toggle {
  margin-left: auto; height: var(--g1-ctl); padding: 0; border: 0; background: none; cursor: pointer;
  font: inherit; font-size: 13px; line-height: 18px; font-weight: 500; color: var(--g1-primary-ink); white-space: nowrap;
}
.apt-attr-summary-toggle:hover { text-decoration: underline; }
.apt-attr-summary-toggle:focus-visible { outline: 2px solid var(--g1-primary); outline-offset: 2px; border-radius: var(--radius-control); }

/* the frame: kit table, rows end at their content */
.apt-table-scroll table { width: 100%; }
.apt-table-scroll thead th { text-transform: none; letter-spacing: 0; }
.apt-table-scroll tbody td.g1-meta-cell { color: var(--g1-ink-soft); font-weight: 400; }
/* g1.css's `.g1-row-hairline` is written for a flex div-list; a real <tr>
   stays a table row here and borrows only its hairline + hover. */
.apt-table-scroll tr.g1-row-hairline { display: table-row; padding: 0; justify-content: normal; min-height: 0; }
.apt-row.click { cursor: pointer; }
.apt-row-name.faint { color: var(--g1-ink-soft); font-weight: 400; }
.apt-source-detail { font-size: 12px; line-height: 16px; font-weight: 500; color: var(--g1-ink-soft); }
.apt-table-scroll th.apt-col-more, .apt-table-scroll td.apt-col-more { width: 64px; padding: 0 16px 0 0; text-align: right; }
.apt-mobile-line, .apt-mobile-status { display: none; }
.apt-empty-row td { text-align: left; color: var(--g1-ink-soft); }

/* status tags: 24 tall, tint fill, no border (the kit tag + a tone) */
.apt-status.st-booked { background: var(--g1-surface-2); color: var(--g1-ink-soft); }
.apt-status.st-cancelled { background: var(--g1-danger-soft); color: var(--g1-danger); }
.apt-status.st-refilled { background: var(--g1-primary-soft); color: var(--g1-primary-ink); }
.apt-status.st-requested { background: var(--g1-warn-soft); color: var(--g1-warn); }
/* 'Seen in OSCAR' (SYNC3-desk) — status genuinely unknown: the neutral tag,
   its own words carry the difference from Booked. */
.apt-status.st-seen { background: var(--g1-surface); color: var(--g1-ink-soft); }

/* loading / error: one plain line in the frame's place */
.apt-notice {
  display: flex; align-items: center; gap: 12px; min-height: var(--g1-row); padding: 8px 16px; box-sizing: border-box;
  border: 1px solid var(--g1-line); border-radius: var(--radius-card);
  font-size: 13px; line-height: 18px; font-weight: 500; color: var(--g1-ink-soft);
}

/* pager: a plain line under the frame (kit .g1-pagination), text buttons */
.apt-pager-nav { display: flex; align-items: center; gap: 16px; margin-left: auto; }
.apt-pager-nav[hidden] { display: none; }
.apt-pager-btn {
  height: var(--g1-ctl); padding: 0; border: 0; background: none; cursor: pointer;
  font: inherit; font-size: 13px; line-height: 18px; font-weight: 500; color: var(--g1-primary-ink);
}
.apt-pager-btn:hover:not(:disabled) { text-decoration: underline; }
.apt-pager-btn:disabled { color: var(--g1-ink-faint); cursor: default; }
.apt-pager-btn:focus-visible { outline: 2px solid var(--g1-primary); outline-offset: 2px; border-radius: var(--radius-control); }

/* two-line rows below 1024 (the kit collapses tables only below 768) */
@media (max-width: 1023px) {
  .apt-table-scroll table, .apt-table-scroll tbody { display: block; width: 100%; }
  .apt-table-scroll thead { display: none; }
  .apt-table-scroll tr.g1-row-hairline { display: flex; align-items: flex-start; gap: 12px; width: 100%; box-sizing: border-box; min-height: 56px; padding: 8px 16px; }
  /* two classes deep: g1.css loads after this file and its desktop
     `.g1-table tbody td` (48 tall, hairline) would win a tie */
  .apt-table-scroll .g1-table tbody td { height: auto; padding: 0; border: 0; background: none; }
  .apt-table-scroll td.apt-col-when,
  .apt-table-scroll td.apt-col-prov,
  .apt-table-scroll td.apt-col-service,
  .apt-table-scroll td.apt-col-source,
  .apt-table-scroll td.apt-col-status { display: none; }
  .apt-table-scroll .apt-row td:first-child {
    flex: 1 1 auto; min-width: 0;
    display: grid; grid-template-columns: minmax(0, 1fr) auto; column-gap: 12px; align-items: start;
  }
  .apt-row td:first-child > * { grid-column: 1; }
  .apt-table-scroll .apt-mobile-line { display: block; margin-top: 2px; }
  .apt-table-scroll .apt-row .apt-mobile-status { display: block; grid-column: 2; grid-row: 1 / span 2; }
  .apt-table-scroll td.apt-col-more { flex: none; width: auto; padding: 0; }
  .apt-table-scroll .apt-empty-row td { padding: 16px 0; }
}
@media (max-width: 767px) {
  .apt-toolbar { flex-direction: column; align-items: stretch; }
  .apt-search { width: 100%; }
  .apt-export { width: 100%; min-height: 44px; margin-bottom: 8px; }
  .apt-toolbar .g1-appts-select-row { flex-direction: column; }
  .apt-toolbar .g1-appts-select-row .g1-select { flex: none; width: 100%; }
  .apt-attr-summary-toggle { margin-left: 0; align-self: flex-start; min-height: 44px; }
  .apt-table-scroll td.apt-col-more { display: none; }
  .apt-pager-btn { min-width: 44px; min-height: 44px; }
}

/* --------------------------------------------- Attribution disclosure
   (APPTATTR1 2026-09-21; DESKG2-appointments) — the per-channel breakdown
   behind "Show how they were booked": one frame of hairline rows between
   the filter row and the table, never a box inside it. */
.apt-attr-disclosure { margin: 0 0 24px; }
.apt-attr-disclosure[hidden] { display: none; }
.apt-attr-list { border: 1px solid var(--g1-line); border-radius: var(--radius-card); background: var(--g1-ground); overflow: hidden; }
.attr-row { border-bottom: 1px solid var(--g1-line); }
.attr-row:last-child { border-bottom: 0; }
.attr-row-head { all: unset; box-sizing: border-box; display: flex; align-items: center; gap: 12px; width: 100%;
                 min-height: var(--g1-row); padding: 8px 16px; cursor: pointer; }
.attr-row-head:hover { background: var(--g1-surface); }
.attr-row-head:focus-visible { outline: 2px solid var(--g1-primary); outline-offset: -2px; }
.attr-chevron { color: var(--g1-ink-soft); font-size: 14px; line-height: 20px; transform: rotate(0deg); transition: transform .12s ease; flex: none; }
.attr-chevron.open { transform: rotate(90deg); }
.attr-row-label { flex: 1 1 auto; min-width: 0; text-align: left; }
.attr-row-title { font-size: 13px; line-height: 18px; font-weight: 500; color: var(--g1-ink); }
.attr-row-desc { margin-top: 2px; font-size: 12px; line-height: 16px; font-weight: 500; color: var(--g1-ink-soft); }
.attr-row-count { font-size: 13px; line-height: 18px; font-weight: 600; color: var(--g1-ink); font-variant-numeric: tabular-nums; flex: none; }
.attr-row-detail { padding: 0 16px 12px 40px; display: flex; flex-direction: column; gap: 8px; }
.attr-patient-line { display: flex; flex-direction: column; gap: 4px; }
.attr-patient-head { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; font-size: 13px; line-height: 18px; }
.attr-patient-name { font-weight: 500; color: var(--g1-ink); min-width: 140px; }
.attr-patient-metric { color: var(--g1-ink-soft); }
.attr-patient-metric b { color: var(--g1-ink); font-weight: 600; }
.attr-door-pill { margin-left: auto; }
.attr-door-thread { background: var(--g1-primary-soft); color: var(--g1-primary-ink); }
.attr-patient-sub { font-size: 12px; line-height: 16px; color: var(--g1-primary-ink); }
.attr-empty { font-size: 13px; line-height: 18px; color: var(--g1-ink-soft); padding: 4px 0; }
.attr-patient-line.simple { flex-direction: row; align-items: baseline; gap: 12px; font-size: 13px; line-height: 18px; }
.attr-patient-meta { font-size: 12px; line-height: 16px; color: var(--g1-ink-soft); }
.attr-show-all { all: unset; color: var(--g1-primary-ink); font-size: 13px; line-height: 18px; font-weight: 500; cursor: pointer;
                 min-height: 44px; display: flex; align-items: center; }
.attr-show-all:hover { text-decoration: underline; }
.attr-show-all:focus-visible { outline: 2px solid var(--g1-primary); outline-offset: 2px; }

@media (max-width: 767px) {
  .attr-row-detail { padding: 0 16px 12px 16px; }
  .attr-patient-head { gap: 8px; }
  .attr-patient-name { min-width: 0; flex: 1 1 100%; }
  .attr-door-pill { margin-left: 0; }
}

/* Appearance tab (room-settings-b2, 2026-09-12) — same scoped `.card`/`.ph`
   header grammar as `.biz-page .card`/`.ph` (settings-business.js) and
   `.hp-embedded .card`/`.ph` (settings-hours.js): one bordered card, one
   decision. Self-scoped under `.ap-card` since this page has no shared
   ancestor wrapper class of its own. */
.ap-card { border: 1px solid var(--line); border-radius: var(--radius-card); background: var(--ground); overflow: hidden; }
.ap-card .ph { padding: 14px 16px; border-bottom: 1px solid var(--line); display: flex; align-items: center; gap: 8px; }
.ap-card .ph .h2 { font-size: 15px; font-weight: 650; margin-right: auto; }
.ap-card .ph .meta { font-size: 12.5px; color: var(--ink-soft); }
.ap-card .ap-body { padding: 16px; }

/* OPENS2 (2026-09-19) — Schedule status colours + Desk preferences cards,
   same `.ap-card`/`.ap-body` shell as the theme card above. */
.ap-pref-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; }
.ap-pref-field label { display: block; font-size: 13px; font-weight: 600; color: var(--ink-soft); margin-bottom: 6px; }
.ap-pref-field select, .ap-pref-field input[type="number"] {
  width: 100%; min-height: 44px; padding: 0 10px; border: 1px solid var(--line);
  border-radius: var(--radius-sm, 6px); background: var(--ground); color: var(--ink); font: inherit;
}
.ap-pref-field .stcaption { margin-top: 6px; }
.ap-pref-field .stcaption.err { color: var(--danger); }
.ap-pref-checkbox { display: flex; align-items: center; }
.ap-pref-checkbox label { display: flex; align-items: center; gap: 8px; font-size: 14px; font-weight: 500; color: var(--ink); min-height: 44px; }
.ap-pref-checkbox input[type="checkbox"] { width: 20px; height: 20px; }

/* Win-backs reuses the Appointments pager without its viewport-height card. */
.winbacks-page .wb-roster-pager { min-height: 48px; gap: 16px; }
.winbacks-page .wb-roster-pager[hidden] { display: none; }
@media (max-width: 760px) {
  .winbacks-page .wb-roster-pager { flex-wrap: wrap; padding-top: 8px; padding-bottom: 8px; }
}

/* P4 live booking-page editor — preview-mode affordances (2026-09-14).
   Scoped under html.p4-preview-mode (set only by app.js's initPreviewMode,
   itself gated on `?preview=1`) so a real patient NEVER sees any of this —
   the class is never present outside the desk's own iframe. Grip/pencil
   chips render only on hover/focus (never a permanent toolbar), matching
   the approved comp's "affordances are hover/focus only" rule; a
   `:focus-within` fallback keeps the chip visible for keyboard use. */
html.p4-preview-mode .p4-section {
  position: relative;
  cursor: grab;
  border-radius: 10px;
  transition: outline-color .12s ease;
  outline: 1.5px solid transparent;
  outline-offset: 8px;
}
html.p4-preview-mode .p4-section:hover,
html.p4-preview-mode .p4-section:focus-within {
  outline-color: var(--accent);
}
html.p4-preview-mode .p4-section.p4-dragging {
  opacity: .6;
  cursor: grabbing;
}
html.p4-preview-mode .p4-section.p4-drop-target {
  outline-color: var(--accent);
  outline-style: dashed;
}
html.p4-preview-mode .p4-grip-btn {
  position: absolute;
  top: 10px;
  right: 10px;
  z-index: 2;
  min-width: 32px;
  min-height: 32px;
  padding: 0 10px;
  border: 0;
  border-radius: 999px;
  background: var(--accent);
  color: var(--on-accent, #fff);
  font-size: 14px;
  line-height: 32px;
  cursor: grab;
  opacity: 0;
  pointer-events: none;
  transition: opacity .12s ease;
}
html.p4-preview-mode .p4-section:hover .p4-grip-btn,
html.p4-preview-mode .p4-section:focus-within .p4-grip-btn,
html.p4-preview-mode .p4-grip-btn:focus-visible {
  opacity: 1;
  pointer-events: auto;
}
html.p4-preview-mode .p4-grip-btn:focus-visible {
  outline: 2px solid var(--accent-ink, var(--ink));
  outline-offset: 2px;
}
/* Welcome text needs its own solid ground under the chip (the banner
   image already provides one) — a transparent chip over body copy would
   be unreadable at low contrast accents. */
html.p4-preview-mode #service-welcome.p4-section { padding-top: 4px; }
html.p4-preview-mode #service-welcome .p4-grip-btn { top: -6px; right: -6px; }
/* Welcome pencil (comp 1) sits opposite the grip so the two affordances
   never overlap — #service-welcome carries BOTH .p4-section (grip, hover
   rule above) and .p4-editable-wrap (pencil, hover rule below); hovering
   the one element satisfies both ancestor selectors at once. */
html.p4-preview-mode #service-welcome .p4-pencil-tl { top: -6px; left: -6px; right: auto; }

/* Policy pencil (step 4 review, comp 4) — same hover/focus-only chip
   grammar as the grip above, positioned relative to a thin wrapper around
   the existing #review-policy text node (no change to that node itself,
   so its content/id stay exactly what renderCancellationPolicy() sets). */
html.p4-preview-mode .p4-editable-wrap {
  position: relative;
  border-radius: 8px;
  outline: 1.5px solid transparent;
  outline-offset: 6px;
  transition: outline-color .12s ease;
}
html.p4-preview-mode .p4-editable-wrap:hover,
html.p4-preview-mode .p4-editable-wrap:focus-within {
  outline-color: var(--accent);
}
html.p4-preview-mode .p4-pencil-btn {
  position: absolute;
  top: -10px;
  right: -10px;
  z-index: 2;
  min-width: 28px;
  min-height: 28px;
  border: 0;
  border-radius: 999px;
  background: var(--accent);
  color: var(--on-accent, #fff);
  font-size: 13px;
  line-height: 28px;
  cursor: pointer;
  opacity: 0;
  pointer-events: none;
  transition: opacity .12s ease;
}
html.p4-preview-mode .p4-editable-wrap:hover .p4-pencil-btn,
html.p4-preview-mode .p4-editable-wrap:focus-within .p4-pencil-btn,
html.p4-preview-mode .p4-pencil-btn:focus-visible {
  opacity: 1;
  pointer-events: auto;
}
html.p4-preview-mode .p4-pencil-btn:focus-visible {
  outline: 2px solid var(--accent-ink, var(--ink));
  outline-offset: 2px;
}

/* Bottom-right pencil variant — opposite the grip's default top-right,
   shared by every card that carries BOTH a drag grip AND a pencil on the
   same element: the clinic banner band (comp 5), each practitioner card
   (comp 7), each service row (comp 8) — same "coexist without
   overlapping" pattern #service-welcome's own grip+pencil pair (top
   variant) already established. */
html.p4-preview-mode .p4-pencil-br { top: auto; bottom: 10px; right: 10px; }
/* Practitioner/service cards are much smaller than the banner band —
   pull the chip fully inside the card's own corner (not floating half
   off the edge like the banner's larger surface can afford) so it never
   gets clipped by a card's own overflow/grid gap. */
html.p4-preview-mode .p4-prac .p4-pencil-br,
html.p4-preview-mode .p4-svc .p4-pencil-br { bottom: 6px; right: 6px; min-width: 24px; min-height: 24px; font-size: 11px; line-height: 24px; }

/* Logo + colours pencils (comps 2/3) — on #brand (topbar), a small text
   node with no photo/content a hover-revealed overlay chip would need to
   protect — a persistent (never opacity-gated) inline icon pair reads as
   ordinary topbar chrome instead, the more discoverable pattern for the
   ONE spot visible on every step. Overrides the generic .p4-pencil-btn's
   absolute/hover-only positioning wholesale for this variant only. */
html.p4-preview-mode .p4-brand-editable { display: inline-flex; align-items: center; gap: 4px; outline: none; flex: none; }
html.p4-preview-mode .p4-brand-editable .p4-pencil-inline {
  position: static;
  opacity: 1;
  pointer-events: auto;
  min-width: 22px;
  min-height: 22px;
  font-size: 11px;
  line-height: 22px;
}

/* Practitioner drag (step 1, comp 3, 2026-09-14) — reuses .p4-section's
   hover/grip/dragging CSS wholesale (see public/app.js's
   wireOnePractitionerCard). Unlike the two plain-div sections, a
   practitioner card is itself a clickable <button> (opens the profile) —
   the whole-card "grab" cursor from .p4-section would misleadingly
   suggest the entire card drags; only the grip actually does. */
html.p4-preview-mode .p4-prac.p4-section { cursor: pointer; }
html.p4-preview-mode .p4-prac.p4-section .p4-grip-btn { cursor: grab; }

/* Practitioner/service-row modal "show on booking page" checkbox (comps
   7/8) — a plain labelled row, same 44px touch target floor every other
   interactive control in this app uses (STANDARDS.md), not scoped to
   p4-preview-mode since it renders inside the desk's OWN shared #modal,
   not the iframe. */
.p4-modal-checkbox { display: flex; align-items: center; gap: 8px; min-height: 44px; font-size: 13.5px; color: var(--ink); cursor: pointer; }
.p4-modal-checkbox input[type="checkbox"] { width: 20px; height: 20px; margin: 0; cursor: pointer; }

/* "+ Add a named service" ghost card (comp 9) — same box model as .svc
   (width/padding/radius) so it reads as a natural next row in the SAME
   list, dashed border + no fill distinguishes it as an add affordance
   rather than a real bookable service. Always visible (never hover-
   gated) — an empty-state "add" control is the one P4 affordance that
   IS the content, not an overlay on top of it. */
html.p4-preview-mode .p4-add-named-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  min-height: 44px;
  padding: 12px 16px;
  border: 1.5px dashed var(--line-strong);
  border-radius: var(--radius-card);
  background: transparent;
  color: var(--accent-ink, var(--ink));
  font: inherit;
  font-size: 13.5px;
  font-weight: 600;
  cursor: pointer;
}
html.p4-preview-mode .p4-add-named-btn:hover { background: var(--accent-soft); }
html.p4-preview-mode .p4-add-named-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ==========================================================================
   UIKIT1 (2026-09-17) — the Settings kit (public/desk/settings-kit.js).
   Chris 10:47: "Simple toggles, straight to the point, group together what
   can be grouped"; 10:56: zero scroll at the top level. The page is pinned
   to the viewport with the same calc()+--chrome-banner-h technique the
   Appointments card (.apt-table-card, above) and the Schedule shell already
   use; CARD BODIES scroll so a 17-practitioner clinic never clips and never
   scrolls the page itself.
   Tokens only — no new colour is introduced here, so every clinic theme and
   the dark theme keep working untouched.
   ========================================================================== */

.set-page {
  display: grid;
  gap: 16px;
  align-items: stretch;
  /* Each Settings page sets its own grid-template-columns in its own sheet;
     one column is the mobile default and the honest fallback. */
  grid-template-columns: 1fr;
  /* DESKG1-settings r3 (2026-09-26) — the page used to be pinned to one
     screen (calc(100vh - top)), so a page whose cards add up to more than
     that (Team at 1594px, Connection) spilled past a 652px page and the
     page ground stopped at 900px (walk-full-0e12). The page now grows with
     its content: no viewport pin, no clip — one page ground the full
     scroll height, at every viewport size. */
  height: auto;
  min-height: 320px;
}

.set-card {
  display: flex;
  flex-direction: column;
  min-height: 0; /* without this a flex child refuses to shrink and the page scrolls */
  /* A card HUGS its content and caps at the row, rather than stretching to
     fill it. Stretching turned the first real Business tab into three tall
     empty boxes — a four-row card inflated to 700px — which is the same
     defect Chris named on the old layout ("whats this right column here?
     its dead and taking up space?"), only rotated from horizontal to
     vertical. Capping at 100% keeps the taller cards inside the pinned page
     so their own bodies scroll, which is what stops the PAGE scrolling. */
  align-self: start;
  max-height: 100%;
  background: var(--ground);
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  overflow: hidden;
}
/* Opt back in where a card genuinely should fill its column — a list that
   grows with the clinic (practitioners, a day's hours) reads better held to
   a common edge than ragged. */
.set-card--fill { align-self: stretch; }

.set-card-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  padding: 14px 16px 10px;
  border-bottom: 1px solid var(--line);
}
.set-card-title { font-size: 15px; font-weight: 600; letter-spacing: -0.01em; }
.set-card-hint { font-size: 12px; color: var(--ink-faint); text-align: right; }

.set-card-body { flex: 1 1 auto; min-height: 0; overflow: auto; padding: 4px 0; }
.set-card-body--fixed { overflow: visible; }

/* ------------------------------------------------------------- set-field */

.set-field {
  display: grid;
  grid-template-columns: minmax(0, 8rem) minmax(0, 1fr);
  align-items: baseline;
  gap: 4px 12px;
  width: 100%;
  padding: 11px 16px;
  border: 0;
  border-top: 1px solid var(--line);
  background: transparent;
  text-align: left;
  cursor: pointer;
}
.set-card-body > .set-field:first-child { border-top: 0; }
.set-field:hover { background: var(--surface); }
.set-field:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.set-field--static { cursor: default; }
.set-field--static:hover { background: transparent; }

.set-field-label { font-size: 14px; font-weight: 500; color: var(--ink); }
.set-field-value {
  font-size: 14px; color: var(--ink-soft); min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.set-field-hint { grid-column: 2; font-size: 12px; color: var(--ink-faint); }

/* ------------------------------------------------------------ set-switch */

.set-switch {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
  padding: 12px 16px;
  border-top: 1px solid var(--line);
}
.set-card-body > .set-switch:first-child { border-top: 0; }

.set-switch-ctl { display: flex; align-items: flex-start; gap: 11px; cursor: pointer; min-width: 0; }
.set-switch-input { position: absolute; opacity: 0; width: 0; height: 0; }

.set-switch-track {
  position: relative; flex: 0 0 auto; margin-top: 1px;
  width: 38px; height: 22px; border-radius: 999px;
  background: var(--line-strong);
  transition: background 120ms ease;
}
.set-switch-knob {
  position: absolute; top: 2px; left: 2px; width: 18px; height: 18px;
  border-radius: 50%; background: var(--ground);
  box-shadow: 0 1px 2px rgba(16, 19, 26, 0.25);
  transition: transform 120ms ease;
}
.set-switch-input:checked + .set-switch-track { background: var(--accent); }
.set-switch-input:checked + .set-switch-track .set-switch-knob { transform: translateX(16px); }
.set-switch-input:focus-visible + .set-switch-track { outline: 2px solid var(--accent); outline-offset: 2px; }
.set-switch-input:disabled + .set-switch-track { opacity: 0.5; }
.set-switch-input:disabled ~ .set-switch-text { opacity: 0.6; }

.set-switch-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.set-switch-label { font-size: 14px; font-weight: 500; }
.set-switch-caption { font-size: 12.5px; color: var(--ink-soft); line-height: 1.4; }

.set-switch-action {
  flex: 0 0 auto; border: 0; background: transparent; padding: 0;
  font-size: 13px; color: var(--accent); cursor: pointer; white-space: nowrap;
}
.set-switch-action:hover { text-decoration: underline; }
.set-switch-action:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ------------------------------------------------------------- set-modal */

.set-modal { display: flex; flex-direction: column; gap: 14px; }
.set-modal-body { display: flex; flex-direction: column; gap: 14px; }
.set-modal-error { color: var(--danger, #9b1c1c); font-size: 13px; }
/* B220 (brief-DESKFOLLOW 7): Cancel/Save stay at the dialog's foot while
   .modal-body scrolls — sticky on the dialog's own ground, same hairline.
   Sticky stops above .modal-body's 20px bottom padding (desk.html), so a
   solid 20px ground shadow fills that strip: no scrolled text under Save. */
.set-modal-foot { display: flex; justify-content: flex-end; gap: 8px; padding-top: 4px; border-top: 1px solid var(--line);
  position: sticky; bottom: 0; background: var(--ground); box-shadow: 0 20px 0 var(--ground); }

.set-modal-cancel, .set-modal-save {
  border-radius: var(--radius-control); padding: 8px 15px;
  font-size: 14px; cursor: pointer;
}
.set-modal-cancel { border: 1px solid var(--line-strong); background: var(--ground); color: var(--ink); }
.set-modal-save { border: 1px solid var(--accent); background: var(--accent); color: var(--on-accent); }
.set-modal-save:disabled { opacity: 0.6; cursor: default; }
.set-modal-cancel:focus-visible, .set-modal-save:focus-visible { outline: 2px solid var(--g1-primary); outline-offset: 2px; }
.modal:has(.set-modal--wide) { max-width: 680px; }

/* MODAL44: the shared Cancel/Save footer is a 39px hit target at phone
   width — below the 44px tap floor every Settings modal inherits. */
@media (max-width: 760px) {
  .set-modal-cancel, .set-modal-save { min-height: 44px; }
}

/* A phone gets one column and the page stops being viewport-pinned — a
   390px screen has no room to be clever, and a natural scroll there is
   correct rather than a failure of the contract above. */
@media (max-width: 760px) {
  .set-page { height: auto; min-height: 0; grid-template-columns: 1fr; }
  .set-card-body { overflow: visible; }
  .set-field { grid-template-columns: 1fr; }
  .set-field-value { white-space: normal; }
}

/* BOOKGROUP1: one heading per practitioner type above its grid. */
/* B167: booking-label 15/600 (was 700, a stop off the booking scale). */
.prac-group-h { margin: 20px 0 10px; font-size: 15px; font-weight: 600; line-height: 19px; letter-spacing: 0.01em; color: var(--text, inherit); }
.prac-group-h:first-child { margin-top: 0; }

/* BOOKMATCH1: "Not sure who to book with?" chips */
.unsure { border: 1px dashed var(--accent); background: var(--accent-soft); border-radius: var(--radius-card); padding: 12px; margin-bottom: 16px; }
.unsure-h { margin: 0 0 8px; font-weight: 600; color: var(--ink); }
.unsure-chips { display: flex; flex-wrap: wrap; gap: 8px; }
.unsure-chip { min-height: 44px; padding: 0 16px; border-radius: 999px; border: 1px solid var(--line-strong); background: var(--ground); color: var(--ink); font: inherit; cursor: pointer; transition: background-color 0.12s ease, border-color 0.12s ease; }
.unsure-chip[aria-pressed="true"] { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
.unsure-chip:focus-visible, .unsure-reset:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.unsure-result { margin: 8px 0 0; color: var(--ink-soft); }
.unsure-result:empty { display: none; }
.unsure-reset { min-height: 44px; padding: 0; background: none; border: 0; font: inherit; color: var(--accent-ink); text-decoration: underline; cursor: pointer; }
.prac { transition: border-color 0.12s ease, background-color 0.12s ease, opacity 0.2s ease; }
.prac--hit { border-color: var(--accent); background: var(--warn-soft); }
.prac--hit .prac-meta::after { content: "Good fit"; display: block; margin-top: 4px; font-size: 12px; font-weight: 600; color: var(--accent-ink); }
.prac--dim { opacity: 0.45; }
@media (prefers-reduced-motion: reduce) {
  .unsure-chip, .prac { transition: none; }
}
@media (max-width: 480px) {
  .killrow { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
  .killrow .btn { flex: 0 1 auto; min-width: 0; }
  .killrow .statusmsg { flex: 0 0 100%; margin: 0; }
}

/* CFGFORM1 (2026-10-04) — "(optional)" after a field the clinic made
   optional; the same quiet marker the reason box's own "(optional)" uses. */
.field-opt { font-weight: 400; color: var(--ink-faint); }

/* CFGFORM1 — the desk's "Form fields" and "Review and done wording" modals
   (public/desk/booking-page-editor.js; desk.html loads this sheet too).
   Scoped to the two modal bodies, G1 aliases only, no new tokens. BOTH
   build their field rows with modalField (the Page wording modal's
   .le-field rows, look-editor.js) and share one intro, one group heading
   and one footer below; the switch is a scoped copy of
   practitioner-profile.js's .pp-switch. */
/* The .le-field rows carry 16px side padding; the intro, the group
   headings and every note sit on that same left edge. */
.p4-rw .p4-rw-intro, .p4-ff .p4-rw-intro { margin: 0; padding: 0 var(--le-x, 16px) 8px; }
.p4-rw .p4-rw-intro:last-of-type { padding-top: 12px; }
.p4-rw .p4-rw-group, .p4-ff .p4-rw-group { margin: 16px 0 0; padding: 0 var(--le-x, 16px) 8px; border-bottom: 1px solid var(--g1-line); font-size: 12px; line-height: 16px; font-weight: 500; color: var(--g1-ink-soft); }
/* Form fields: the group heading line also holds that unit's Move up /
   Move down, so the heading line (not the h4) carries the inset + hairline. */
.p4-ff .p4-ff-head { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin: 16px 0 0; padding: 0 var(--le-x, 16px) 8px; border-bottom: 1px solid var(--g1-line); }
/* CFGTEAM1 judge fix (D-I-1 / D-G1-4): a unit's name is the loudest line in
   its section — the 14/600 section stop in ink, above the 14/400 soft field
   labels under it (Form fields and Team names and groups alike). */
.p4-ff .p4-ff-head .p4-rw-group { margin: 0; padding: 0; border-bottom: 0; font-size: 14px; line-height: 20px; font-weight: 600; color: var(--g1-ink); }
/* A unit's note sits under its last row like the wording modal's closing
   note: same inset, same 12px off the row's hairline. (Team names' "No
   longer matches a group" note sits the same 12px under its heading.) */
.p4-ff .p4-ff-foot { display: flex; flex-direction: column; padding: 12px var(--le-x, 16px) 0; }
.p4-ff .p4-ff-note { margin: 0; }
.p4-ff .p4-ff-moves { display: flex; flex: none; gap: 8px; }
/* Team names and groups (CFGTEAM1 judge fix, B-DIRECTION / T-9 / D-I-14):
   each group's heading row reads, left to right, its patient-facing name,
   "Hidden from patients" while hidden, the Hide switch, then Move up / Move
   down — the switch sits with the name it hides, inside the section's
   hairline. At phone width the controls wrap under the name as one line
   (switch left, arrows right); nothing scrolls sideways. A hidden group's
   people read soft, the same ink-soft its tag uses. */
.p4-ff .p4-tn-head { flex-wrap: wrap; row-gap: 8px; }
.p4-ff .p4-tn-title { display: flex; align-items: baseline; flex-wrap: wrap; gap: 4px 8px; min-width: 0; }
.p4-ff .p4-tn-title .p4-rw-group { overflow-wrap: anywhere; }
.p4-ff .p4-tn-tag { font-size: 12px; line-height: 16px; font-weight: 500; color: var(--g1-ink-soft); }
.p4-ff .p4-tn-controls { display: flex; align-items: center; gap: 16px; margin-left: auto; }
.p4-ff .p4-ff-row[data-hidden="true"] .p4-tn-person { color: var(--g1-ink-soft); }
@media (max-width: 760px) {
  .p4-ff .p4-tn-controls { flex: 1 0 100%; justify-content: space-between; margin-left: 0; }
}
/* Service groups and prices (CFGSVC1 judge fix, D-I-1 / T-7 / D-I-18 /
   D-G1-2): inside a group each service is a sub-heading on the hairline —
   its name at 14/500 in ink, above the 14/400 soft practitioner names under
   it — with "Clinic price $120" / "No clinic price" on its own meta line.
   Each practitioner's price field reads as money: "$" inside the box, the
   amount right-aligned, 120px at most. */
.p4-ff .p4-sg-service { margin: 16px 0 0; padding: 0 var(--le-x, 16px) 8px; border-bottom: 1px solid var(--g1-line); }
.p4-ff .p4-sg-service-name { margin: 0; font-size: 14px; line-height: 20px; font-weight: 500; color: var(--g1-ink); overflow-wrap: anywhere; }
.p4-ff .p4-money { position: relative; display: block; width: 100%; max-width: 120px; }
.p4-ff .p4-money-sign { position: absolute; left: 12px; top: 50%; transform: translateY(-50%); font-size: 14px; line-height: 20px; color: var(--g1-ink-soft); pointer-events: none; }
.p4-ff .p4-money .le-input { padding-left: 24px; text-align: right; font-variant-numeric: tabular-nums; }
/* Cancel / Done stay in view while the dialog body scrolls (only the body
   scrolls, the footer has its own padding — Reconciled GRAMMAR §63). Same
   sticky foot as B220's .set-modal-foot above: sticky stops above
   .modal-body's 20px bottom padding, so a 20px ground shadow fills that
   strip and the buttons get 20px of ground above and below. */
.p4-ff > .rowactions, .p4-rw > .rowactions { position: sticky; bottom: 0; z-index: 1; margin: 16px 0 0; padding: 20px 0 0; border-top: 1px solid var(--g1-line); background: var(--g1-ground); box-shadow: 0 20px 0 var(--g1-ground); }
/* A control reached by Tab scrolls clear of that footer (20 + 1 + 44 + 20),
   never under it. */
.p4-ff .le-input, .p4-ff .p4-ff-move, .p4-ff .p4-ff-switch,
.p4-rw .le-input, .p4-rw .le-textarea { scroll-margin-bottom: 96px; }
.p4-ff .p4-ff-move { display: inline-flex; align-items: center; justify-content: center; width: 44px; height: 44px; padding: 0; border: 1px solid var(--g1-line); border-radius: var(--radius-control); background: var(--g1-ground); color: var(--g1-ink); cursor: pointer; }
.p4-ff .p4-ff-move:hover:not(:disabled) { background: var(--g1-surface-2); }
.p4-ff .p4-ff-move:disabled { opacity: .4; cursor: default; }
.p4-ff .p4-ff-switch-row { display: flex; align-items: center; gap: 8px; min-height: 44px; }
/* Same shape as B290's .pp-switch: the button is the 44 x 44 tap box, the
   44 x 26 track is drawn inside it (::before), the thumb rides that track. */
.p4-ff .p4-ff-switch { position: relative; display: inline-block; flex: none; width: 44px; height: 44px; padding: 0; border: 0; background: none; cursor: pointer; }
.p4-ff .p4-ff-switch::before { content: ''; position: absolute; left: 0; top: 9px; width: 44px; height: 26px; box-sizing: border-box; border-radius: var(--rounded-pill); border: 1px solid var(--g1-line-strong); background: var(--g1-surface-2); transition: background 0.15s, border-color 0.15s; }
.p4-ff .p4-ff-switch[aria-checked="true"]::before { background: var(--g1-primary); border-color: var(--g1-primary); }
.p4-ff .p4-ff-switch-thumb { position: absolute; top: 12px; left: 3px; width: 20px; height: 20px; box-sizing: border-box; border-radius: var(--rounded-pill); border: 1px solid var(--g1-line-strong); background: var(--color-on-primary); transition: transform 0.15s; }
.p4-ff .p4-ff-switch[aria-checked="true"] .p4-ff-switch-thumb { border-color: var(--g1-primary); transform: translateX(18px); }
.p4-ff .p4-ff-switch-label { font-size: 14px; line-height: 20px; color: var(--g1-ink); cursor: pointer; }
.p4-ff .p4-ff-move:focus-visible, .p4-ff .p4-ff-switch:focus-visible::before { outline: 2px solid var(--g1-primary); outline-offset: 2px; }
.p4-ff .p4-ff-switch:focus-visible { outline: none; }
@media (prefers-reduced-motion: reduce) {
  .p4-ff .p4-ff-switch::before, .p4-ff .p4-ff-switch-thumb { transition: none; }
}

/* ---------------------------------------------------------------------
   EDITPAGE1 (2026-10-05) — the patient page's clinic video on step 1
   (Create's #3 placed in look.sectionOrder; public/app.js's
   syncClinicVideoBand): a 16:9 player in the page's own column.
   --------------------------------------------------------------------- */
.clinic-video { margin: 0 0 24px; border-radius: var(--radius-card, 12px); overflow: hidden; background: var(--surface-2); }
.clinic-video-media { display: block; width: 100%; aspect-ratio: 16 / 9; border: 0; }
.clinic-video-media:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
#service-welcome + .clinic-video { margin-top: 8px; }
/* the banner band under the video sits below real content, as it does under the welcome */
#clinic-video-band ~ #clinic-banner-band.prof-banner { margin-top: 24px; }

/* ---------------------------------------------------------------------
   EDITPAGE1 — click-to-change marks inside the desk's preview (public/
   app.js startPreviewMarks). html.p4-marks is set only by the desk
   editor's mark-style message, so a patient never sees any of this. The
   colours are the desk's own, passed in (--p4m-*), never the clinic's
   brand colour; every size is multiplied by --p4m-inv (1 / the frame's
   scale) so the marks read the same at any preview size.
   --------------------------------------------------------------------- */
/* OPEN 3: the whole piece is the target. Hidden by specificity alone: no
   other rule here sets display on a pencil (the html.p4-preview-mode
   pencil rules set position/opacity only), so (0,2,1) beats the UA's
   button display — the [hidden] guard stays the only !important display. */
html.p4-marks .p4-pencil-btn { display: none; }
html.p4-marks .p4-section:not(.p4-drop-target):is(:hover, :focus-within),
html.p4-marks .p4-editable-wrap:is(:hover, :focus-within) { outline-color: transparent; }
html.p4-marks:hover [data-p4-piece],
html.p4-marks.p4-has-hot [data-p4-piece],
html.p4-marks.p4-marks-always [data-p4-piece] {
  outline: calc(1px * var(--p4m-inv, 1)) dashed var(--p4m-line);
  outline-offset: calc(4px * var(--p4m-inv, 1));
}
html.p4-marks [data-p4-piece] { cursor: pointer; }
html.p4-marks [data-p4-piece].p4-hot,
html.p4-marks [data-p4-piece]:focus-visible {
  outline: calc(2px * var(--p4m-inv, 1)) solid var(--p4m-accent);
  outline-offset: calc(4px * var(--p4m-inv, 1));
}
html.p4-marks .p4-tag {
  position: absolute; z-index: 60; display: flex; align-items: baseline; gap: calc(8px * var(--p4m-inv, 1));
  padding: calc(4px * var(--p4m-inv, 1)) calc(8px * var(--p4m-inv, 1)); border-radius: calc(4px * var(--p4m-inv, 1));
  background: var(--p4m-accent); color: var(--p4m-on); pointer-events: none; white-space: nowrap;
  font-family: Inter, system-ui, sans-serif; font-size: calc(12px * var(--p4m-inv, 1)); line-height: calc(16px * var(--p4m-inv, 1));
  font-weight: 500; letter-spacing: 0; text-transform: none;
}
html.p4-marks .p4-tag[hidden] { display: none; }
html.p4-marks.p4-placing [data-p4-piece] { outline: none; }
html.p4-marks.p4-placing .p4-tag { display: none; }
html.p4-marks .p4-tag b { font-weight: 600; }
/* A Create place, in the page's own flow: dashed while it can be picked,
   solid while Ask Bea's proposal waits for Confirm. */
html.p4-marks .p4-slot {
  display: flex; align-items: center; gap: calc(12px * var(--p4m-inv, 1)); box-sizing: border-box; width: 100%;
  min-height: calc(64px * var(--p4m-inv, 1)); margin: calc(12px * var(--p4m-inv, 1)) 0;
  padding: calc(12px * var(--p4m-inv, 1)) calc(16px * var(--p4m-inv, 1));
  border: calc(1.5px * var(--p4m-inv, 1)) dashed var(--p4m-accent); border-radius: calc(8px * var(--p4m-inv, 1));
  background: var(--p4m-soft); color: var(--p4m-ink); text-align: left; cursor: pointer;
  font-family: Inter, system-ui, sans-serif; font-size: calc(14px * var(--p4m-inv, 1)); line-height: calc(20px * var(--p4m-inv, 1));
  font-weight: 500; letter-spacing: 0; text-transform: none;
}
html.p4-marks .p4-slot.is-pending { border-style: solid; cursor: default; }
/* J5 (fix 3) — a band that pulls itself up under main's top padding
   (.prof-banner's -22px) sits under a place box, never over its edge. */
html.p4-marks .p4-slot + .prof-banner { margin-top: 0; }
/* J8 (fix 3) — at 390 Ask Bea's sheet leaves a short strip of page; the
   page's sticky bar scrolls away with it so the marked place shows. */
html.p4-marks.p4-strip :is(.topbar, .steps) { position: relative; top: auto; }
html.p4-marks .p4-slot:focus-visible { outline: calc(2px * var(--p4m-inv, 1)) solid var(--p4m-accent); outline-offset: calc(2px * var(--p4m-inv, 1)); }
html.p4-marks .p4-slot-num {
  flex: none; display: inline-flex; align-items: center; justify-content: center;
  min-width: calc(24px * var(--p4m-inv, 1)); height: calc(24px * var(--p4m-inv, 1)); border-radius: calc(4px * var(--p4m-inv, 1));
  background: var(--p4m-accent); color: var(--p4m-on); font-weight: 600; font-variant-numeric: tabular-nums;
}
html.p4-marks .p4-slot-words { flex: 1; min-width: 0; display: flex; flex-direction: column; }
html.p4-marks .p4-slot-where { font-size: calc(12px * var(--p4m-inv, 1)); line-height: calc(16px * var(--p4m-inv, 1)); color: var(--p4m-ink-soft); }
html.p4-marks .p4-slot-go { flex: none; color: var(--p4m-accent-ink); font-weight: 600; }
/* The one moment of motion (brief: motion budget): a placed piece settles. */
@media (prefers-reduced-motion: no-preference) {
  html.p4-marks .p4-settle { animation: p4-settle .32s cubic-bezier(.2, .8, .2, 1); }
}
@keyframes p4-settle {
  from { opacity: 0; transform: translateY(calc(-8px * var(--p4m-inv, 1))); }
  to { opacity: 1; transform: none; }
}

/* ---------------------------------------------------------------------
   EDITPAGE1 — the desk's booking-page editor, "the page map"
   (public/desk/booking-page-editor.js).
   Desk tokens only (g1.css aliases, the two radius tokens). Every rule is
   under .bkp-page .p4e, which out-ranks settings-booking-page.css's older
   editor rules (that sheet loads after this one).
   --------------------------------------------------------------------- */
.bkp-page:has(.p4e) > .bkp-intro { display: none; } /* OPEN 4: the strip's own line replaces it */
.bkp-page .p4-hero-card.p4e {
  --p4e-col: 300px;
  display: grid; height: var(--p4e-h, 736px); box-sizing: border-box; margin: 0;
  grid-template-columns: var(--p4e-col) minmax(0, 1fr);
  grid-template-rows: auto auto minmax(0, 1fr) auto;
  grid-template-areas: "ph ph" "map tb" "map stage" "bea bar";
  border: 1px solid var(--g1-line); border-radius: var(--radius-card); overflow: hidden; background: var(--g1-ground);
}
@media (min-width: 1900px) { .bkp-page .p4-hero-card.p4e { --p4e-col: 360px; } }
.bkp-page .p4-hero-card.p4e > .ph { grid-area: ph; display: flex; align-items: center; flex-wrap: nowrap; gap: 12px; min-height: 52px; padding: 8px 16px; }
.bkp-page .p4-hero-card.p4e > .ph .h2 { flex: none; margin: 0; }
.bkp-page .p4-hero-card.p4e > .ph .meta { flex: 1; min-width: 0; margin-right: auto; }
.bkp-page .p4e .p4e-end { flex: none; display: flex; align-items: center; gap: 16px; }
.bkp-page .p4e .p4e-end .seg {
  display: inline-flex; margin: 0; height: var(--g1-ctl); box-sizing: border-box;
  border: 1px solid var(--g1-line); border-radius: var(--radius-control); overflow: hidden; background: var(--g1-ground);
}
.bkp-page .p4e .p4e-end .seg button {
  flex: none; min-height: 0; height: 100%; margin: 0; padding: 0 12px; border: 0; border-radius: 0; border-right: 1px solid var(--g1-line);
  background: var(--g1-ground); color: var(--g1-ink-soft); cursor: pointer; white-space: nowrap;
  font: inherit; font-size: 13px; line-height: 18px; font-weight: 500;
}
.bkp-page .p4e .p4e-end .seg button:last-child { border-right: 0; }
.bkp-page .p4e .p4e-end .seg button:hover { background: var(--g1-surface); }
.bkp-page .p4e .p4e-end .seg button[aria-pressed="true"] { background: var(--g1-primary-soft); color: var(--g1-ink); }
.bkp-page .p4e .p4e-end .seg button:focus-visible { outline: 2px solid var(--g1-primary); outline-offset: -2px; }
.bkp-page .p4e .p4e-end .p4-open-link,
.bkp-page .p4e .p4e-link {
  display: inline-flex; align-items: center; min-height: 32px; margin: 0; padding: 0; border: 0; background: none; cursor: pointer;
  white-space: nowrap; text-decoration: none; font: inherit; font-size: 13px; line-height: 18px; font-weight: 500; color: var(--g1-primary-ink);
}
.bkp-page .p4e .p4e-link:hover { text-decoration: underline; }
.bkp-page .p4e .p4e-link:focus-visible { outline: 2px solid var(--g1-primary); outline-offset: 2px; border-radius: var(--radius-control); }
.bkp-page .p4e .p4e-link[aria-expanded="true"] { color: var(--g1-ink); }

/* the column and its panels */
.bkp-page .p4e .p4e-side { grid-area: map; display: flex; flex-direction: column; min-width: 0; min-height: 0; border-right: 1px solid var(--g1-line); background: var(--g1-ground); }
.bkp-page .p4e .p4e-panel { flex: 1; min-height: 0; display: flex; flex-direction: column; }
.bkp-page .p4e .p4e-panel[hidden] { display: none; }
.bkp-page .p4e .p4e-head {
  flex: none; display: flex; align-items: center; gap: 8px; min-height: var(--g1-row);
  padding: var(--spacing-sm) var(--spacing-sm) var(--spacing-sm) var(--spacing-lg); box-sizing: border-box;
  border-bottom: 1px solid var(--g1-line); background: var(--g1-ground);
}
.bkp-page .p4e .p4e-head-text { flex: 1; min-width: 0; }
.bkp-page .p4e .p4e-head-title { display: flex; align-items: center; gap: 8px; margin: 0; font-size: 14px; line-height: 20px; font-weight: 600; color: var(--g1-ink); outline: none; }
.bkp-page .p4e .p4e-head-sub { margin: 0; font-size: 12px; line-height: 16px; font-weight: 500; color: var(--g1-ink-soft); }
.bkp-page .p4e .p4e-x {
  flex: none; display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; padding: 0;
  border: 0; border-radius: var(--radius-control); background: none; color: var(--g1-ink-soft); cursor: pointer; font-size: 14px; line-height: 1;
}
.bkp-page .p4e .p4e-x:hover { background: var(--g1-surface); color: var(--g1-ink); }
.bkp-page .p4e .p4e-x:focus-visible { outline: 2px solid var(--g1-primary); outline-offset: -2px; }
.bkp-page .p4e .p4e-panel-map .p4e-head .p4e-x { display: none; } /* the map is never closed at desk width */
.bkp-page .p4e .p4e-body { position: relative; flex: 1; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; }
.bkp-page .p4e .p4e-foot { flex: none; display: flex; flex-direction: column; gap: 8px; padding: 12px 16px; border-top: 1px solid var(--g1-line); }
.bkp-page .p4e .p4e-create { width: 100%; }
.bkp-page .p4e .p4e-cap { margin: 0; font-size: 12px; line-height: 16px; font-weight: 500; color: var(--g1-ink-soft); }
.bkp-page .p4e .p4e-note { margin: 0; padding: 12px 16px; font-size: 12px; line-height: 16px; font-weight: 500; color: var(--g1-ink-soft); }
.bkp-page .p4e .p4e-row {
  flex: none; display: flex; flex-direction: column; justify-content: center; gap: 2px; width: 100%; min-height: var(--g1-row);
  margin: 0; padding: var(--spacing-sm) var(--spacing-lg); box-sizing: border-box; border: 0; border-bottom: 1px solid var(--g1-line); border-radius: 0;
  background: var(--g1-ground); color: var(--g1-ink); text-align: left; font: inherit; cursor: pointer;
}
.bkp-page .p4e button.p4e-row:hover, .bkp-page .p4e .p4e-row.is-hot { background: var(--g1-primary-soft); }
.bkp-page .p4e .p4e-row:focus-visible { outline: 2px solid var(--g1-primary); outline-offset: -2px; }
.bkp-page .p4e .p4e-row-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.bkp-page .p4e .p4e-row-name { font-size: 13px; line-height: 18px; font-weight: 500; color: var(--g1-ink); }
.bkp-page .p4e .p4e-row-sub {
  font-size: 12px; line-height: 16px; font-weight: 500; color: var(--g1-ink-soft); overflow-wrap: anywhere;
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden;
}
/* Create: the number is the one loud glyph */
/* J3 (fix 3) — 45px rows (44 + hairline) so all 11 fit the column at 1440
   and the sheet at 390 with nothing under a bar; still a 44px target. */
.bkp-page .p4e .p4e-cat-row {
  display: grid; grid-template-columns: 24px minmax(0, 1fr); column-gap: 12px; align-items: center;
  min-height: 44px; padding-top: var(--spacing-xs); padding-bottom: var(--spacing-xs);
}
.bkp-page .p4e .p4e-num { font-size: 14px; line-height: 20px; font-weight: 600; color: var(--g1-ink); text-align: right; font-variant-numeric: tabular-nums; }
.bkp-page .p4e .p4e-cat-row.is-on { cursor: default; }
.bkp-page .p4e .p4e-cat-row.is-on .p4e-num,
.bkp-page .p4e .p4e-cat-row.is-on .p4e-row-name { color: var(--g1-ink-faint); }
.bkp-page .p4e .p4e-badge {
  flex: none; display: inline-flex; align-items: center; justify-content: center; min-width: 24px; height: 24px; padding: 0 4px; box-sizing: border-box;
  border-radius: var(--radius-control); background: var(--g1-primary); color: var(--g1-on-primary);
  font-size: 13px; line-height: 18px; font-weight: 600; font-variant-numeric: tabular-nums;
}
.bkp-page .p4e .p4e-place-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; column-gap: 12px; align-items: center; }
.bkp-page .p4e .p4e-go { font-size: 13px; line-height: 18px; font-weight: 500; color: var(--g1-primary-ink); white-space: nowrap; }
/* F3 — the place an on-page piece sits in now: marked, never offered */
.bkp-page .p4e .p4e-place-row.is-here { cursor: default; }
.bkp-page .p4e .p4e-here {
  padding: 2px 8px; border-radius: var(--radius-control); background: var(--g1-surface-2);
  font-size: 12px; line-height: 16px; font-weight: 600; color: var(--g1-ink-soft); white-space: nowrap;
}
/* All settings: the page's rows, stacked to the column's width */
.bkp-page .p4e .p4e-settings-body { display: block; } /* the rows flow as blocks, as when they sat in their own wrapper */
.bkp-page .p4e .p4e-settings-body .bkp-row { grid-template-columns: minmax(0, 1fr) auto; row-gap: 4px; }
.bkp-page .p4e .p4e-settings-body .bkp-row-text { grid-column: 1; grid-row: 1; }
.bkp-page .p4e .p4e-settings-body .bkp-row-edit { grid-column: 2; grid-row: 1; align-self: start; }
.bkp-page .p4e .p4e-settings-body .bkp-row-value { grid-column: 1 / -1; grid-row: 2; }
/* Ask Bea in the column: the dock's own thread classes */
.bkp-page .p4e .p4e-panel-bea .askbea-thread { flex: 1; min-height: 0; }
.bkp-page .p4e .p4e-panel-bea .askbea-msg { max-width: 92%; }
.bkp-page .p4e .p4e-panel-bea .askbea-acts .askbea-btn.ghost { flex: none; }
.bkp-page .p4e .p4e-refuse { align-self: stretch; max-width: 100%; }
.bkp-page .p4e .p4e-catlines { margin: 8px 0 0; padding: 0; list-style: none; font-size: 13px; line-height: 22px; font-weight: 500; color: var(--g1-ink); }
.bkp-page .p4e .p4e-catlines li { display: grid; grid-template-columns: 24px minmax(0, 1fr); column-gap: 8px; }
.bkp-page .p4e .p4e-catnum { text-align: right; font-weight: 600; font-variant-numeric: tabular-nums; }
/* the one Ask line: under the column, level with the Publish bar */
.bkp-page .p4e .p4e-ask {
  display: flex; align-items: center; gap: 8px; min-width: 0; margin: 0; padding: 0 16px; box-sizing: border-box; background: var(--g1-ground);
}
.bkp-page .p4e .p4e-ask-bar { grid-area: bea; border-top: 1px solid var(--g1-line); border-right: 1px solid var(--g1-line); }
/* fix 4 — the field's words on an existing type stop (14/400, the dialog
   field labels'), not a 13/400 of its own; 390 keeps its 16px below */
.bkp-page .p4e .p4e-ask .askbea-field { flex: 1; min-width: 0; height: var(--g1-ctl); font-size: 14px; font-weight: 400; }
.bkp-page .p4e .p4e-ask .p4e-send { flex: none; }
.bkp-page .p4e .p4e-bar-create, .bkp-page .p4e .p4e-ask-sheet, .bkp-page .p4e .p4e-mapbtn { display: none; }
/* the page, at true size (booking-page-editor.js paintPhoneScale) */
.bkp-page .p4e .p4-toolbar { grid-area: tb; display: flex; align-items: center; justify-content: flex-start; flex-wrap: nowrap; min-width: 0; }
.bkp-page .p4e .p4-stage { grid-area: stage; position: relative; display: block; min-width: 0; min-height: 0; height: auto; padding: 0; overflow: hidden; }
.bkp-page .p4e .p4-frame-wrap.p4-frame-phone,
.bkp-page .p4e .p4-frame-wrap.p4-frame-desktop {
  position: absolute; left: var(--p4e-fx, 0); top: var(--p4e-fy, 16px); width: var(--p4e-fw, 390px); height: var(--p4e-fh, 600px);
  transform: scale(var(--p4e-fs, 1)); transform-origin: 0 0; transition: none;
  box-sizing: border-box; border: 1px solid var(--g1-line); border-bottom: 0;
}
.bkp-page .p4e .p4-demo-notice { grid-area: stage; }
/* padding on the desk spacing scale (G1 spacing_grid, fix round 2); the
   bar keeps its 56px min-height, so its height at 1440 is unchanged */
.bkp-page .p4e .p4-publish-bar { grid-area: bar; position: static; padding: var(--spacing-sm) var(--spacing-lg); }

/* J1 (fix 3) — from 1900 wide (booking-page-editor.js placeAskLine sets
   .p4e-wide): the column ends where its rows end, with Create and the Ask
   line right under them; the stage's grey runs on below it, so no tall
   empty block stands beside the desktop page. */
.bkp-page .p4-hero-card.p4e.p4e-wide {
  grid-template-areas: "ph ph" "map tb" "map stage" "map bar";
  background: var(--g1-surface);
}
.bkp-page .p4e.p4e-wide > .ph, .bkp-page .p4e.p4e-wide .p4-toolbar { background: var(--g1-ground); }
.bkp-page .p4e.p4e-wide .p4e-side { align-self: start; max-height: 100%; border-bottom: 1px solid var(--g1-line); border-bottom-right-radius: var(--radius-card); }
.bkp-page .p4e.p4e-wide .p4e-panel { flex: 0 1 auto; }
.bkp-page .p4e.p4e-wide .p4e-ask-bar { flex: none; padding: var(--spacing-md) var(--spacing-lg); border-right: 0; }

/* 390: the column is one row under the steps; Create and the Ask line sit
   in a bar above Publish; a panel rises as a sheet above the tab bar */
@media (max-width: 767px) {
  .bkp-page .p4-hero-card.p4e {
    margin: 0 -16px; border-left: 0; border-right: 0; border-radius: 0;
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto auto auto minmax(0, 1fr) auto auto;
    grid-template-areas: "ph" "tb" "mapbtn" "stage" "bea" "bar";
  }
  .bkp-page .p4-hero-card.p4e > .ph { flex-wrap: wrap; gap: 0 12px; min-height: 0; padding: 0 16px; }
  .bkp-page .p4-hero-card.p4e > .ph .h2 { display: flex; align-items: center; min-height: 44px; }
  .bkp-page .p4-hero-card.p4e > .ph .meta { order: 3; flex-basis: 100%; padding-bottom: 8px; }
  .bkp-page .p4e .p4e-end { margin-left: auto; }
  .bkp-page .p4e .p4e-end .p4-open-link, .bkp-page .p4e .p4e-link { min-height: 44px; }
  .bkp-page .p4e .p4-toolbar { padding: 8px 16px; }
  .bkp-page .p4e .p4-toolbar .p4-step-seg { display: flex; width: 100%; height: 46px; }
  /* J9 (fix 3) — six short names share the row (no step numbers) */
  .bkp-page .p4e .p4-toolbar .p4-step-seg button { flex: 1 1 auto; min-width: 44px; padding: 0 var(--spacing-xs); font-size: 13px; line-height: 18px; }
  .bkp-page .p4e .p4e-mapbtn {
    grid-area: mapbtn; display: flex; align-items: center; justify-content: space-between; gap: 12px; min-height: 44px; margin: 0; padding: 0 16px;
    border: 0; border-bottom: 1px solid var(--g1-line); border-radius: 0; background: var(--g1-surface); cursor: pointer;
    font: inherit; font-size: 13px; line-height: 18px; font-weight: 500; color: var(--g1-ink); text-align: left;
  }
  .bkp-page .p4e .p4e-mapbtn .soft { color: var(--g1-ink-soft); }
  .bkp-page .p4e .p4e-mapbtn:focus-visible { outline: 2px solid var(--g1-primary); outline-offset: -2px; }
  .bkp-page .p4e .p4-frame-wrap.p4-frame-phone,
  .bkp-page .p4e .p4-frame-wrap.p4-frame-desktop { border-left: 0; border-right: 0; }
  .bkp-page .p4e .p4e-ask-bar { grid-area: bea; padding: 8px 16px; border-right: 0; }
  .bkp-page .p4e .p4e-bar-create { display: inline-flex; flex: 0 0 96px; }
  .bkp-page .p4e .p4e-ask .askbea-field { height: 44px; font-size: 16px; }
  .bkp-page .p4e .p4e-ask-bar:not(.has-text) .p4e-send { display: none; } /* the comp's bar at rest: Create + the field */
  .bkp-page .p4e .p4-publish-bar { position: static; }
  /* the column is a sheet only while a panel is open */
  .bkp-page .p4e .p4e-side { display: none; }
  .bkp-page .p4e.p4e-sheet-open .p4e-side {
    display: flex; position: fixed; z-index: 30; left: 0; right: 0; bottom: calc(64px + env(safe-area-inset-bottom, 0px));
    max-height: calc(100vh - 64px - 200px); border-right: 0; border-top: 1px solid var(--g1-line);
    border-radius: var(--radius-card) var(--radius-card) 0 0; box-shadow: var(--shadow-lift); overflow: hidden;
  }
  .bkp-page .p4e.p4e-sheet-open:is(:has(.p4e-panel-create:not([hidden])), :has(.p4e-panel-settings:not([hidden]))) .p4e-side {
    height: calc(100vh - 64px - 200px);
  }
  /* J8 (fix 3) — Ask Bea's sheet takes at most half the screen, so the
     place it says is marked shows on the page above it (app.js centres the
     place in that strip: showPreviewPlaces' viewBottom). */
  .bkp-page .p4e.p4e-sheet-open:has(.p4e-panel-bea:not([hidden])) .p4e-side { height: auto; max-height: 50vh; }
  .bkp-page .p4e .p4e-panel-map .p4e-head .p4e-x { display: inline-flex; }
  .bkp-page .p4e .p4e-panel-map .p4e-foot { display: none; }
  .bkp-page .p4e .p4e-x { width: 44px; height: 44px; }
  .bkp-page .p4e .p4e-ask-sheet { display: flex; flex: none; padding: 12px 16px; border-top: 1px solid var(--g1-line); }
  .bkp-page .p4e .p4e-ask-sheet .p4e-send { display: inline-flex; }
  .bkp-page .p4e .p4e-panel-bea .askbea-btn { height: 44px; }
}

/* A piece's dialog sits over the column, clear of the page it edits
   (booking-page-editor.js anchorDialog measures the place). */
@media (min-width: 1024px) {
  #modal:has(.p4e-anchored) { place-items: start; padding: 0; }
  #modal:has(.p4e-anchored) .modal {
    position: fixed; left: var(--p4e-dlg-left); top: var(--p4e-dlg-top); width: var(--p4e-dlg-w);
    max-height: calc(100vh - var(--p4e-dlg-top) - 24px);
  }
}
/* F1/F4 — the editor's banner and logo dialogs: the picture, then the
   kit's buttons (no native file box), so the dialog keeps the editor's
   dialog width and nothing runs off a phone's edge. */
.bkp.p4e-asset .brand-preview { width: 72px; height: 64px; }
/* J4 (fix 3) — an SVG logo with only a viewBox has no width of its own,
   so the base grid cell sized it to 0px (a blank frame): the logo fills
   the frame, contained. */
.bkp.p4e-asset .brand-preview:not(.brand-preview-wide) img { width: 100%; height: 100%; max-width: none; max-height: none; padding: var(--spacing-xs); box-sizing: border-box; object-fit: contain; }
.bkp.p4e-asset .brand-preview-wide { width: 100%; height: auto; aspect-ratio: 16 / 9; min-height: 0; box-sizing: border-box; overflow: hidden; }
.bkp.p4e-asset .brand-preview-wide img { width: 100%; height: 100%; max-width: none; max-height: none; object-fit: cover; border-radius: 0; }
.bkp.p4e-asset .p4e-asset-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 16px; margin-top: 12px; }
.bkp.p4e-asset .p4e-asset-status { margin: 8px 0 0; }
/* J5 (fix 3) — room under Upload/Replace so its focus ring never sits on
   the hint line */
.bkp.p4e-asset .p4e-asset-status + .le-wording-hint { margin-top: var(--spacing-md); }
.bkp.p4e-asset .p4e-asset-status:empty { display: none; }
