/* ───── Easter-egg tech picker ─────
   Hidden by default. Three clicks on the home icon inside the
   collapsed #step2-summary chip reveal it. Sits BETWEEN the address
   summary and the calendar, deliberately sharing the summary chip's
   visual language (cream tint, charcoal-soft border, 14px radius)
   so it reads as a related "where + when" refinement, not a separate
   admin tool.

   Reveal mechanics:
   - HTML ships with the `hidden` attribute as a no-JS / SSR
     fallback — the element really is display:none until JS loads.
   - On JS init, the booking script removes the `hidden` attribute
     and the COLLAPSED ruleset below takes over: the element stays
     in layout but has zero visible footprint (max-height 0,
     opacity 0, visibility hidden, pointer-events off, no padding,
     no border, untabbable). Click count <3 keeps it that way.
   - Three clicks on the home icon adds .is-revealed, transitioning
     max-height + padding + margin + border + opacity together for
     a smooth grow-in. Visibility flips at the start of the reveal
     and at the END of the collapse (delay) so keyboard focus only
     enters when the picker is fully open. */
.tech-easter-egg {
  display: flex;
  align-items: center;
  gap: 12px;
  background: #FAF7F1;
  border: 1px solid rgba(39, 41, 54, 0.10);
  border-radius: 14px;
  /* Collapsed visual: zero space + invisible + untabbable. */
  max-height: 0;
  margin-top: 0;
  padding: 0 14px;
  border-top-width: 0;
  border-bottom-width: 0;
  opacity: 0;
  overflow: hidden;
  visibility: hidden;
  pointer-events: none;
  transition:
    max-height 0.45s cubic-bezier(0.22, 1, 0.36, 1),
    opacity 0.30s ease,
    padding 0.45s cubic-bezier(0.22, 1, 0.36, 1),
    margin-top 0.45s cubic-bezier(0.22, 1, 0.36, 1),
    border-top-width 0s linear 0.45s,
    border-bottom-width 0s linear 0.45s,
    visibility 0s linear 0.45s;
}
/* No-JS / pre-init fallback. Without this, view-source visitors and
   disabled-JS visitors would still get a real (zero-sized but DOM-
   present) element. The hidden attribute keeps it truly gone. */
.tech-easter-egg[hidden] { display: none !important; }
.tech-easter-egg.is-revealed {
  max-height: 100px;
  margin-top: 10px;
  padding: 10px 14px;
  border-top-width: 1px;
  border-bottom-width: 1px;
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  transition:
    max-height 0.45s cubic-bezier(0.22, 1, 0.36, 1),
    /* Slight opacity delay so the fade-in lands AFTER the height
       has started growing — feels like the row "opens", not flashes. */
    opacity 0.32s ease 0.08s,
    padding 0.45s cubic-bezier(0.22, 1, 0.36, 1),
    margin-top 0.45s cubic-bezier(0.22, 1, 0.36, 1),
    border-top-width 0s linear 0s,
    border-bottom-width 0s linear 0s,
    visibility 0s linear 0s;
}
.tech-easter-egg-icon {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  color: var(--text-muted);
}
.tech-easter-egg-icon svg { width: 18px; height: 18px; }
.tech-easter-egg-label {
  flex: 0 0 auto;
  font-weight: 600;
  font-size: 0.95rem;
  color: var(--text);
}
.tech-easter-egg-select {
  flex: 1 1 auto;
  min-width: 0;
  appearance: none;
  -webkit-appearance: none;
  background-color: #FFFFFF;
  border: 1px solid rgba(39, 41, 54, 0.12);
  border-radius: 10px;
  padding: 8px 34px 8px 12px;
  font-size: 0.95rem;
  color: var(--text);
  cursor: pointer;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23272936' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'><path d='M4 6l4 4 4-4'/></svg>");
  background-repeat: no-repeat;
  background-position: right 12px center;
  background-size: 14px;
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.tech-easter-egg-select:focus {
  outline: none;
  border-color: transparent;
  box-shadow: var(--sw-focus-shadow, 0 0 0 1px #F38A3F, 0 6px 13px rgba(243, 138, 63, 0.30));
}
.property-type-legend {
  display: block;
  font-weight: 600;
  font-size: var(--t-xs);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-muted);
  margin-bottom: 8px;
  padding: 0;
}
.property-type-segmented {
  /* Bento segmented control: warm inset wash (was cool #F4F2EE),
     larger squircle radius. */
  display: inline-flex;
  background: #FFFDF9;
  border-radius: 14px;
  padding: 4px;
  gap: 4px;
}
@media (max-width: 600px) {
  .property-type-segmented { gap: 8px; }
}
.property-type-pill {
  position: relative;
  cursor: pointer;
  user-select: none;
}
.property-type-pill input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}
.property-type-pill > span {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 9px 18px;
  font-size: 0.92rem;
  font-weight: 600;
  color: #7B8190;
  border-radius: 11px;
  transition: background 0.18s ease, color 0.18s ease, box-shadow 0.18s ease;
}
@media (max-width: 600px) {
  .property-type-pill > span {
    padding: 14px 16px;
    font-size: 0.95rem;
    min-height: 48px;
  }
}
.property-type-pill > span svg {
  width: 16px;
  height: 16px;
  flex-shrink: 0;
}
.property-type-pill:hover > span { color: #373B4D; }
.property-type-pill input:checked + span {
  /* Selected = warm card-highlight fill, deep navy ink, soft warm
     shadow + 1px hairline border so the picked option reads as a
     lifted tactile chip (not the previous cool charcoal ring). */
  background: #FFFDF9;
  color: #373B4D;
  box-shadow:
    0 2px 8px rgba(55, 59, 77, 0.08),
    0 0 0 1px #E5DED3;
}

/* ═══ 12. Commercial notice ═══ */
/* ───── Commercial redirect notice ─────
   Warm card that softly explains why we don't book commercial here
   and offers a phone CTA. Animates in from above when revealed. */
.commercial-notice {
  /* Muted neutral notice — was orange-tinted bg/border/shadow which
     read as an alert/warning. The redirect message is informational,
     not an error, so it gets the same warm-paper-on-cream treatment
     as the rest of the page chrome. */
  display: flex;
  gap: 14px;
  padding: 18px 20px;
  background: var(--bg);
  border: 1px solid rgba(39, 41, 54, 0.10);
  border-radius: 14px;
  box-shadow: none;
  /* Capped + centered so the notice loads at its final display size
     and stays put while the surrounding card morphs from the wide
     calendar layout (1200px) down to the narrower commercial layout
     (720px). Without this cap, the notice expanded to fill the card
     and visibly shrunk along with the card width, reading as content
     that "loaded wrong then corrected itself." */
  max-width: 540px;
  margin-left: auto;
  margin-right: auto;
  animation: commercialNoticeIn 0.34s cubic-bezier(0.22, 1, 0.36, 1);
}
.commercial-notice[hidden] { display: none; }
.commercial-notice-icon {
  flex-shrink: 0;
  width: 40px;
  height: 40px;
  border-radius: 10px;
  background: rgba(39, 41, 54, 0.06);
  color: var(--text);
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.commercial-notice-icon svg { width: 22px; height: 22px; }
.commercial-notice-body { flex: 1; min-width: 0; }
.commercial-notice h4 {
  margin: 0 0 6px;
  font-size: 1.02rem;
  font-weight: 650;
  color: var(--navy);
  letter-spacing: -0.005em;
}
.commercial-notice p {
  margin: 0;
  font-size: 0.92rem;
  line-height: 1.5;
  color: var(--text);
}
@keyframes commercialNoticeIn {
  from { opacity: 0; transform: translateY(-4px); }
  to   { opacity: 1; transform: none; }
}

/* ═══ 13. Trip-charge agreement ═══ */
/* ───── Trip-charge agreement (booking step 4) ─────
   Sits above the Confirm button. Custom checkbox treatment so the tick
   reads as deliberate — same orange-warm palette as the rest of the
   booking flow. The Confirm button starts disabled (set via the EJS)
   and is wired in booking.js to enable when the box is checked. */
.trip-charge-agree {
  /* Muted neutral card — was a faint orange-tinted bg + border. The
     trip-charge agreement is a passive consent; the orange is reserved
     for the Confirm Booking button below it. */
  margin: 16px 0 4px;
  padding: 14px 16px;
  background: #FAF9F6;
  border: 1px solid rgba(39, 41, 54, 0.06);
  border-radius: 12px;
  /* Mobile: same padding (no change) — kept here as a hook for future. */
}
.trip-charge-agree-row {
  display: flex;
  gap: 12px;
  align-items: flex-start;
  cursor: pointer;
  user-select: none;
  @media (max-width: 600px) { padding: 4px 0; }
}
.trip-charge-agree-row input[type="checkbox"] {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}
.trip-charge-agree-box {
  flex-shrink: 0;
  width: 22px;
  height: 22px;
  border: 1.5px solid rgba(39, 41, 54, 0.30);
  border-radius: 6px;
  background: #fff;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: #fff;
  transition: background 0.18s ease, border-color 0.18s ease, transform 0.18s ease;
  margin-top: 1px;
  /* Mobile: bigger box for confident tap. */
  @media (max-width: 600px) { width: 24px; height: 24px; }
}
.trip-charge-agree-box svg { opacity: 0; transition: opacity 0.16s ease; }
.trip-charge-agree-row:hover .trip-charge-agree-box {
  border-color: var(--selected-ink);
}
.trip-charge-agree-row input[type="checkbox"]:checked + .trip-charge-agree-box {
  /* Charcoal "checked" — was orange. Same Single-Accent rule. */
  background: var(--selected-ink);
  border-color: var(--selected-ink);
}
.trip-charge-agree-row input[type="checkbox"]:checked + .trip-charge-agree-box svg {
  opacity: 1;
}
.trip-charge-agree-row input[type="checkbox"]:focus-visible + .trip-charge-agree-box {
  box-shadow: 0 0 0 3px rgba(39, 41, 54, 0.18);
}
.trip-charge-agree-text {
  font-size: 0.95rem;
  line-height: 1.45;
  color: var(--text);
  @media (max-width: 600px) { font-size: 0.95rem; line-height: 1.5; }
}
.trip-charge-agree-text strong {
  color: var(--text);
  font-weight: 700;
}
.trip-charge-agree-note {
  display: block;
  margin-top: 2px;
  font-size: 0.8rem;
  color: var(--text-muted);
}

/* ═══ 14. Trip-charge admin input ═══ */
/* ───── Trip-charge admin input ─────
   Per-location fee field in the location settings card. Inline $
   prefix tells admins the unit without an extra label. */
/* Wrapper owns the border + bg so the $ prefix and the number share a
   single visual control. Input is borderless and lays inside the
   wrapper, separated from $ by a thin divider so the eye reads them as
   "currency · amount" instead of two stacked glyphs. The earlier
   absolute-positioned $ overlapped the input's text area at certain
   font sizes. */
.trip-charge-input-wrap {
  display: inline-flex;
  align-items: stretch;
  max-width: 160px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: #fff;
  overflow: hidden;
  transition: border-color 0.18s ease, box-shadow 0.18s ease;
}
.trip-charge-input-wrap:focus-within {
  border-color: transparent;
  box-shadow: var(--sw-focus-shadow, 0 0 0 1px #F38A3F, 0 6px 13px rgba(243, 138, 63, 0.30));
}
.trip-charge-prefix {
  display: inline-flex;
  align-items: center;
  padding: 0 10px;
  font-weight: 600;
  font-size: 0.95rem;
  color: var(--text);
  background: var(--card-tint, #FAF7F1);
  border-right: 1px solid var(--border);
  user-select: none;
}
.trip-charge-input {
  font-family: inherit;
  font-size: 0.95rem;
  font-weight: 600;
  padding: 8px 12px;
  border: 0;
  border-radius: 0;
  background: transparent;
  width: 100%;
  -moz-appearance: textfield;
}
.trip-charge-input:focus { outline: none; }
.trip-charge-input::-webkit-outer-spin-button,
.trip-charge-input::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}
/* Content fade-in synced with the card morph. The card's height
   transition is 0.7s (cubic-bezier(0.4, 0, 0.2, 1)). The new panel
   fades from opacity 0 → 1 over 0.55s with a 0.18s delay so the card
   has visibly started morphing before the content materializes —
   reads as "the card reshapes, then the new content settles in"
   rather than "card and content both swap simultaneously." */
.step-panel:not([hidden]) {
  animation: stepFadeIn 0.55s cubic-bezier(0.4, 0, 0.2, 1) 0.18s backwards;
}
.step-panel h2 {
  margin: 0 0 6px;
  font-size: 1.4rem;
  color: var(--navy);
  font-weight: 700;
  letter-spacing: -0.01em;
}
.step-panel h2 + p.muted { margin-top: 0; margin-bottom: 24px; }

/* ═══ 15. Serif heading utility ═══ */
/* ───── Serif heading utility ─────
   Editorial system-serif stack — Charter on Apple, Iowan as a close
   sibling, Cambria/Georgia as fallbacks elsewhere. Used on premium
   prompts (Step 3 "How can we help you today?") so the question
   reads as an expert's voice rather than form chrome. */
.heading-serif {
  font-family: 'Charter', 'Iowan Old Style', 'Source Serif Pro', Cambria, Georgia, 'Times New Roman', serif;
  font-weight: 600;
  font-size: 1.85rem;
  line-height: 1.2;
  letter-spacing: -0.012em;
  color: var(--ink);
  margin: 0 0 28px;
}

/* ═══ 16. Step 3 ledger textarea ═══ */
/* ───── Ledger textarea (Step 3 description) ─────
   Bottom-rule-only input style. No box, no rounded chrome — feels
   like writing on lined paper instead of filling out a form. The
   underline broadens + turns orange on focus so the customer feels
   their pen has caught the page. */
/* Step 3 ("What is going on?") — public flow.
   Standard bordered textarea on a warm card surface — the familiar
   "comment box" shape most customers expect. Admin booking has its
   own compact layout overrides above this block and isn't affected. */
.ledger-field {
  display: flex;
  flex-direction: column;
  /* Bumped 10 → 16 to give the new bottom-edge progress bar room to
     sit below the textarea without crowding the counter row beneath.
     The bar uses absolute-positioning bottom: -12px, so the gap
     needs to be at least that plus the bar's height (5px) plus a
     few px breathing room before the counter text. */
  gap: 16px;
  width: 100%;
}
.textarea-ledger {
  width: 100%;
  border: 1px solid #E5DED3;
  border-radius: 14px;
  background: #FFFDF9;
  padding: 14px 16px;
  font-family: inherit;
  font-size: 1rem;
  line-height: 1.5;
  color: var(--ink);
  resize: vertical;
  min-height: 140px;
  max-height: 360px;
  box-shadow: 0 1px 0 rgba(255, 255, 255, 0.7) inset;
  transition: border-color 0.18s ease, box-shadow 0.18s ease, background 0.18s ease;
  -webkit-appearance: none;
}
.textarea-ledger::placeholder {
  color: var(--text-muted);
  opacity: 0.95;
}
.textarea-ledger:hover:not(:focus) {
  border-color: #C9BDA8;
}
.textarea-ledger:focus {
  outline: none;
  border-color: transparent;
  box-shadow:
    var(--sw-focus-shadow, 0 0 0 1px #F38A3F, 0 6px 13px rgba(243, 138, 63, 0.30)),
    0 1px 0 rgba(255, 255, 255, 0.7) inset;
}

/* Meta row below the textarea — char count + (optional) required
   indicator. */
.ledger-meta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  font-size: 0.82rem;
  letter-spacing: 0.02em;
  color: var(--text-muted);
}

/* Textarea-with-progress-bar wrapper. Positioning context for the
   bottom-edge fill bar that visualizes progress toward the 20-char
   minimum. The wrapper takes the textarea's outer dimensions; the
   fill bar lives at its bottom edge, inset just enough to land on
   top of the textarea's bottom border without escaping the rounded
   corners. --char-progress is set by JS as a 0..1 value on the
   ledger-field root each time the textarea fires input. */
.ledger-field-wrap {
  position: relative;
  width: 100%;
}
.textarea-ledger-progress {
  position: absolute;
  left: 12px;
  right: 12px;
  /* Sits BELOW the textarea now (was bottom: 0 sitting on the border).
     The -12 / 5 combo lands the bar 7px below the textarea's bottom
     edge with a 5px-tall fill, so it reads as its own visual element
     rather than a thicker border. The .ledger-field gap was bumped
     to 16px (was 10px) so the counter row below still has room. */
  bottom: -12px;
  height: 5px;
  border-radius: 999px;
  background: rgba(55, 59, 77, 0.08);
  overflow: hidden;
  pointer-events: none;
  /* Hidden until the customer types something — empty state is the
     same chrome as the original "no progress bar" design. */
  opacity: 0;
  transition: opacity 0.25s ease;
}
.ledger-field[data-has-progress="1"] .textarea-ledger-progress {
  opacity: 1;
}
.textarea-ledger-progress::after {
  content: '';
  display: block;
  height: 100%;
  width: calc(var(--char-progress, 0) * 100%);
  background: var(--orange, #F38A3F);
  border-radius: 999px;
  transition: width 0.18s ease, background 0.28s ease;
}
.ledger-field[data-progress-complete="1"] .textarea-ledger-progress::after {
  background: #4BAA78; /* sage-green at the threshold */
}

/* Char-counter goal style. "12 / 20" before threshold, "20 ✓" after.
   The number color ramps from muted → orange (typing) → sage-green
   (goal met) via the .is-typing and .is-good classes set by JS. */
.char-count {
  font-variant-numeric: tabular-nums;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  transition: color 0.28s ease;
}
.char-count-current { font-weight: 600; }
.char-count-sep { opacity: 0.6; margin: 0 2px; }
.char-count-goal { opacity: 0.8; }
.char-count-check {
  display: inline-flex;
  margin-left: 4px;
  color: #4BAA78;
  opacity: 0;
  transform: scale(0.5) rotate(-20deg);
  transition: opacity 0.22s ease, transform 0.32s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.char-count.is-typing {
  color: var(--orange, #F38A3F);
}
.char-count.is-good {
  color: #2F7A52;
  font-weight: 600;
}
/* When the goal is reached, the "/ 20" separator + goal dim away
   and the green check pops in. Keeps the counter from forever
   flashing the target after the customer cleared it. */
.char-count.is-good .char-count-sep,
.char-count.is-good .char-count-goal {
  opacity: 0;
  width: 0;
  margin: 0;
  overflow: hidden;
  transition: opacity 0.22s ease, width 0.22s ease, margin 0.22s ease;
}
.char-count.is-good .char-count-check {
  opacity: 1;
  transform: scale(1) rotate(0);
}
@media (prefers-reduced-motion: reduce) {
  .textarea-ledger-progress,
  .textarea-ledger-progress::after,
  .char-count,
  .char-count-check { transition: none; }
}

.ledger-required {
  text-transform: uppercase;
  letter-spacing: 0.12em;
  font-size: 0.7rem;
  font-weight: 600;
  color: var(--text-muted);
}

@keyframes stepFadeIn {
  from { opacity: 0; }
  to   { opacity: 1; }
}
/* (stepIn / stepOut keyframes removed — old slide-and-scale entrance
    was replaced by the quieter fade above. Card resize handles space.) */

/* Hero entrance */
.hero { animation: heroIn 0.6s ease-out; }
@keyframes heroIn {
  from { opacity: 0; transform: translateY(-6px); }
  to { opacity: 1; transform: translateY(0); }
}

/* ═══ 17. Step 1 service cards ═══ */
/* ─────────────────────────────────────────────────────────────────
   SERVICE CARDS — booking step 1
   Tactile, elastic interaction model:
   • Rest: subtle external lift (cards feel raised off the page)
   • Hover: scale 1.02 + warm cream surface (anticipation)
   • Press (:active): scale 0.96, fast (the squish)
   • Release: bouncy cubic-bezier with overshoot (snap into place)
   • Selected: external shadow REMOVED, swapped for an inset
     depression + thick inset orange border (the "depressed" feel)
   • Procedural separator line draws from center outward on open
   No chevron — title is the only indicator; visual state does the
   talking. Title sits vertically centered when collapsed; the bullet
   list grows below it on expand.
   ───────────────────────────────────────────────────────────────── */
.service-grid {
  /* 2 columns on desktop. Was auto-fit which fit 3-4 cards across at
     wider widths and made the cards feel cramped — explicit 2-col
     gives each card room to breathe and matches the customer's read
     of the form ("a couple of options at a time"). Gap is moderate
     — enough separation between cards but not so much that the list
     becomes a vertical hike on phones. */
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 12px;
  margin-bottom: 20px;
  align-items: start;
  /* Mobile: 2 columns stays — at 14px page padding + 6px column gap,
     each card is ~155-200px wide on common phone widths and
     comfortably fits short service names. */
  @media (max-width: 600px) {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 6px;
    margin-bottom: 14px;
  }
}
.service-card {
  cursor: pointer;
  display: flex;
  /* Reserve the GPU layer up front so the elastic transform doesn't
     re-rasterize text on every press. Combined with transform: scale(1)
     at rest, this keeps subpixel rendering stable. */
  will-change: transform;
}
.service-card input { position: absolute; opacity: 0; pointer-events: none; }

.service-card-inner {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center; /* title vertically centered when collapsed */
  text-align: center;
  gap: 4px;
  /* Compact button-like card on desktop. Tighter than before so 4-6
     services don't push the Continue button below the fold on phones.
     Reads as a refined chip rather than a slab. */
  padding: 12px 18px;
  border: 1.5px solid #E5DED3;
  border-radius: 12px;
  /* Warm card surface that nests inside the canvas (#F8F5EF). The
     previous pure-#fff fill read as a bright white slab against the
     beige page background; the warm tint sits on the canvas the way
     a sheet of cream paper would, matching the bento-card palette
     used elsewhere in the admin surfaces. */
  background: #FCFAF6;
  min-height: 60px;
  width: 100%;
  font-weight: 500;
  color: var(--text);
  /* Default: soft external lift. Two-layer shadow gives both an
     ambient and contact feel — cards look pressable. */
  box-shadow:
    0 1px 1px rgba(39, 41, 54, 0.03),
    0 4px 12px rgba(39, 41, 54, 0.04);
  transform: scale(1);
  /* The "release" transition: bouncy cubic-bezier with slight overshoot.
     This is the spring back to rest after a press, and the snap when a
     card becomes selected. Press itself overrides this with a faster,
     non-bouncy curve below so the squish feels immediate. */
  transition:
    transform 0.42s cubic-bezier(0.34, 1.56, 0.64, 1),
    border-color 0.22s ease,
    background 0.22s ease,
    box-shadow 0.32s cubic-bezier(0.4, 0, 0.2, 1),
    color 0.22s ease;
  /* Mobile: compact pill in a 2-column grid. Tap target stays ≥44px.
     Padding is tight because the cards live side-by-side now and a
     roomier card would feel overfilled in the narrower column. */
  @media (max-width: 600px) {
    padding: 10px 10px;
    min-height: 48px;
    border-radius: 10px;
  }
}
.service-card.has-issues .service-card-inner {
  @media (max-width: 600px) { padding-top: 11px; padding-bottom: 11px; }
}

/* Hover (anticipation): scale up slightly and brighten the surface
   toward the lighter highlight tint so the card lifts out of the
   warm rest state. Border + shadow use the charcoal accent so the
   hover state stays neutral — orange is reserved for the picked
   state, and the contrast between charcoal-hover and orange-selected
   tells the customer at a glance which card is the active choice. */
.service-card:hover .service-card-inner {
  transform: scale(1.02);
  border-color: var(--selected-ink);
  background: #FFFDF9;
  box-shadow:
    0 2px 3px rgba(39, 41, 54, 0.05),
    0 8px 22px rgba(39, 41, 54, 0.10);
}

/* Press (the squish): drop below 100% briefly. Override the transition
   to a fast, non-bouncy curve so it feels snappy on the way down. The
   release back to rest/hover/selected uses the parent's bouncy curve
   and naturally springs with overshoot. */
.service-card:active .service-card-inner {
  transform: scale(0.96);
  transition:
    transform 0.09s ease-out,
    border-color 0.22s ease,
    background 0.22s ease,
    box-shadow 0.18s ease,
    color 0.22s ease;
}

/* Selected: warm orange highlight — matches the calendar day pill so
   the customer sees the same "picked" visual language across the
   booking flow. Filled orange wash instead of a pure orange border so
   the card actually reads as the active choice at a glance, with a
   soft orange-tinted glow to lift it off the canvas. */
.service-card input:checked + .service-card-inner {
  transform: scale(1);
  border-color: transparent;
  background: #FFE9D3;
  color: var(--navy);
  box-shadow:
    inset 0 0 0 1.5px var(--orange),
    inset 0 1px 0 rgba(255, 255, 255, 0.55),
    0 4px 16px rgba(224, 122, 42, 0.18);
}
/* While the card is selected AND being hovered, suppress the hover
   scale so the depressed feel survives. */
.service-card:hover input:checked + .service-card-inner {
  transform: scale(1);
}
/* Same idea for the press — selected cards still get a tiny squish
   on re-click, but no scale-up bounce on release. */

.service-name {
  font-weight: 650;
  font-size: 0.98rem;
  letter-spacing: -0.005em;
  font-variant-numeric: tabular-nums;
  color: var(--text);
  transition: color 0.22s ease;
  line-height: 1.25;
  @media (max-width: 600px) { font-size: 0.88rem; line-height: 1.2; }
}
.service-card:hover .service-name,
.service-card input:checked + .service-card-inner .service-name {
  color: var(--navy);
}
.service-desc {
  font-size: 0.82rem;
  color: var(--text-muted);
  line-height: 1.4;
  margin-top: 2px;
  transition: color 0.22s ease;
  @media (max-width: 600px) { font-size: 0.74rem; }
}
.service-card input:checked + .service-card-inner .service-desc {
  color: var(--orange-dark);
}

/* Cards with an expandable issues list: same vertical centering as
   plain cards. The list grows below the title when the card is
   expanded, naturally pushing it to fit. */
.service-card.has-issues .service-card-inner {
  padding-top: 16px;
  padding-bottom: 14px;
}

/* ═══ 18. Service-card issues list ═══ */
/* ───── Expanded issues list ─────
   Collapsed: max-height 0, fully transparent. Open: max-height grows
   smoothly and a procedural separator line (::before) draws from
   center outward on a slight delay. */
/* Accordion body height animation via CSS Grid 0fr → 1fr.
   Why this and not max-height: max-height has to be a fixed pixel
   number, so it either undershoots real content (clipping) or
   overshoots (forcing the visible "fully open" moment to land at a
   fraction of the transition duration). With max-height 480 vs ~200px
   real content, the old card's "visible close" and the new card's
   "visible open" landed at different times, which made the row total
   height oscillate during a swap and the Continue button bounce.
   grid-template-rows 0fr → 1fr animates TRUE content height. At any t
   during a swap, old-shrink + new-grow = constant, so the row total
   stays put. */
.service-issues-wrap {
  display: grid;
  grid-template-rows: 0fr;
  margin-top: 0;
  transition:
    grid-template-rows 0.36s cubic-bezier(0.22, 1, 0.36, 1),
    margin-top 0.36s cubic-bezier(0.22, 1, 0.36, 1);
}
.service-card.is-expanded .service-card-inner .service-issues-wrap {
  grid-template-rows: 1fr;
  margin-top: 14px;
}
.service-issues-list {
  list-style: none;
  margin: 0;
  padding: 0;
  width: 100%;
  text-align: left;
  /* min-height: 0 + overflow: hidden are required so the grid track
     can actually collapse to 0 without content spilling out. */
  min-height: 0;
  overflow: hidden;
  position: relative;
  opacity: 0;
  transition: opacity 0.28s ease;
}
.service-card.is-expanded .service-card-inner .service-issues-list {
  opacity: 1;
}
/* Each bullet: custom dot via ::before (so we can color it orange and
   keep tight control over spacing). Items fade in on a small staggered
   delay when the parent card is expanded, giving the "reveal" effect.
   Note: opacity-only animation (no transform) — earlier we used a
   translateY for a slide-in but that puts the <li> on a GPU compositing
   layer at non-integer offsets, which renders small text blurry. The
   parent <ul>'s max-height transition already provides the slide-down
   effect for the whole list. */
.service-issues-list li {
  position: relative;
  padding-left: 14px;
  margin: 0 0 6px;
  font-size: 0.82rem;
  line-height: 1.4;
  color: var(--text);
  opacity: 0;
}
.service-issues-list li:last-child { margin-bottom: 0; }
.service-issues-list li::before {
  content: '';
  position: absolute;
  left: 2px;
  top: 0.55em;
  width: 4px;
  height: 4px;
  border-radius: 50%;
  /* Bullets are charcoal — matches the divider line. Was orange. */
  background: var(--selected-ink);
  opacity: 0.7;
}
.service-card.is-expanded .service-card-inner .service-issues-list li {
  animation: bulletReveal 0.34s ease-out forwards;
}
.service-card.is-expanded .service-card-inner .service-issues-list li:nth-child(1) { animation-delay: 0.08s; }
.service-card.is-expanded .service-card-inner .service-issues-list li:nth-child(2) { animation-delay: 0.13s; }
.service-card.is-expanded .service-card-inner .service-issues-list li:nth-child(3) { animation-delay: 0.18s; }
.service-card.is-expanded .service-card-inner .service-issues-list li:nth-child(4) { animation-delay: 0.23s; }
.service-card.is-expanded .service-card-inner .service-issues-list li:nth-child(5) { animation-delay: 0.28s; }
.service-card.is-expanded .service-card-inner .service-issues-list li:nth-child(6) { animation-delay: 0.33s; }
.service-card.is-expanded .service-card-inner .service-issues-list li:nth-child(n+7) { animation-delay: 0.36s; }
.service-card.is-expanded .service-card-inner .service-issues-list li {
  color: var(--text);
}
@keyframes bulletReveal {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* Continue button: hidden entirely until a service is picked. The
   disabled toggle is already wired in JS — we just translate that
   state into actual visibility. Wrapping .actions stays in the layout
   so we get a consistent gap above whatever follows. */
/* (Was `#continue-step-1[disabled] { display: none }` — replaced by
    the smooth opacity/visibility transition in the Progressive
    Exposure block below, which applies to ALL gated CTAs across
    steps 1–4 and supports a fade-in instead of a snap.) */
.step-panel[data-step="1"] .actions {
  display: flex;
  justify-content: flex-end;
  /* margin-top: auto in a flex column absorbs all available space
     above this element, anchoring it to the bottom of the panel.
     Combined with the panel's min-height, this means a card's
     expansion takes from the slack between grid and Continue
     instead of pushing Continue (and the page below) downward. */
  margin-top: auto;
  padding-top: 24px;
  min-height: 44px;
  /* Mobile: pin Continue to the bottom of the viewport while the
     user scrolls through services, releases naturally when the
     panel's bottom comes into view. Soft top fade so the service list
     dissolves under the button instead of bumping against a hard line.
     Negative horizontal margins extend the fade across the panel's
     side padding so the gradient reaches edge-to-edge. */
  @media (max-width: 600px) {
    position: sticky;
    bottom: 8px;
    z-index: 5;
    margin-top: 6px;
    margin-left: -16px;
    margin-right: -16px;
    padding: 18px 16px 6px;
    background: linear-gradient(180deg, rgba(255, 255, 255, 0) 0, #fff 22px, #fff 100%);
  }
}
.step-panel[data-step="1"] #continue-step-1 {
  animation: continueIn 0.36s cubic-bezier(0.22, 1, 0.36, 1) backwards;
  box-shadow: 0 6px 16px rgba(224, 122, 42, 0.22);
  transition: box-shadow 0.22s ease, background 0.18s ease;
  /* Stronger lift on mobile while the button is floating, so it reads
     as elevated above the scrolling list rather than welded to the
     panel surface. */
  @media (max-width: 600px) { box-shadow: 0 6px 18px rgba(224, 122, 42, 0.32); }
}
.step-panel[data-step="1"] #continue-step-1:hover {
  box-shadow: 0 8px 22px rgba(224, 122, 42, 0.32);
}
@keyframes continueIn {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: none; }
}

/* Step 1 heading polish: a touch more presence on the question and a
   slightly warmer, longer subhead. Centered to balance the grid below. */
.step-panel[data-step="1"] > h2 {
  text-align: center;
  font-size: 1.55rem;
  letter-spacing: -0.018em;
  margin-bottom: 6px;
  @media (max-width: 600px) { font-size: 1.25rem; margin-bottom: 14px; }
}
.step-panel[data-step="1"] > p.muted {
  text-align: center;
  font-size: 0.95rem;
  margin-bottom: 28px;
  color: var(--text-muted);
  @media (max-width: 600px) { margin-bottom: 14px; font-size: 0.88rem; }
}

/* Locked work-summary — appears only after a service is picked */
.work-summary-wrap.is-locked {
  opacity: 0.4;
  pointer-events: none;
  filter: saturate(0.6);
  transition: opacity 0.32s ease, filter 0.32s ease;
}
.work-summary-wrap {
  transition: opacity 0.32s ease, filter 0.32s ease;
}
/* Note: hover/selected states for .service-card live in the unified
   service-card block earlier in this file. The previous `servicePop`
   scale-bounce animation was removed because the transform created a
   GPU-compositing layer on the card-inner that rendered the bullet
   text blurry. The new feedback is box-shadow elevation + a soft
   gradient surface — no transforms on text-bearing elements. */

/* ═══ 19. Forms / fields ═══ */
/* Forms */
.grid { display: grid; gap: 16px; }
.grid-2 {
  grid-template-columns: repeat(2, 1fr);
  @media (max-width: 600px) { grid-template-columns: 1fr; }
}
/* Step 4 (.your-info-grid) gets a roomier gap than the default .grid
   so the inputs don't crowd each other on the wider panel. The .grid
   gap stays at 16 for admin/elsewhere where density is preferred.
   max-width caps the input grid at a comfortable form-reading width
   so we don't spread inputs across the full 1040px panel — the panel
   chrome stays uniform with the other steps, but the form inside is
   the proportions a customer expects. */
.your-info-grid {
  gap: 22px;
  max-width: 760px;
  margin: 0 auto;
  /* Mobile: tighter gap (collapsed to 1-col so 22px between vertically
     stacked inputs is too much). */
  @media (max-width: 600px) { gap: 16px; }
}
.your-info-grid .field > span {
  @media (max-width: 600px) { font-size: 0.88rem; }
}
/* Step 3 (description) — content cap is wider than step 4's form
   because the heading + textarea benefit from a roomier reading
   measure (the heading is serif, the textarea is a single ledger
   line). Card itself is 1040px on this step; cap inner content at
   880 so the textarea has presence without sprawling. */
.step-panel[data-step="3"] > .heading-serif,
.step-panel[data-step="3"] > .ledger-field,
.step-panel[data-step="3"] > .field {
  max-width: 880px;
  margin-left: auto;
  margin-right: auto;
}
/* Actions row deliberately escapes the 880px cap so Back sits at the
   card's bottom-left corner and Continue at the bottom-right. The base
   .actions rule already applies `justify-content: space-between`, but
   it can only push the buttons as far apart as the row is wide — when
   the row was capped, the pair ended up clustered in the middle of a
   much wider card. Letting actions span the full card width restores
   the corner-to-corner layout. */
.step-panel[data-step="3"] > .actions {
  max-width: none;
  margin-left: 0;
  margin-right: 0;
  justify-content: space-between;
}
/* 8px between the eyebrow label and the input — was 6px, which was
   tight enough that the uppercase label felt glued to the field. */
.field { display: flex; flex-direction: column; gap: 8px; }
/* Modified-field indicator. Phase 2 of the customer-edit flow:
   when a matched customer is loaded and the admin edits a tracked
   identity field (name, email, phone, address), the .field wrapper
   gets .is-modified added by booking.js. A small orange dot
   appears next to the field label so the admin can scan the form
   at a glance and see what they've changed since the autofill.
   Submit-time preview block (admin-flow-review) lists the same
   diffs as plain-English bullets. Only present in admin mode — the
   public flow doesn't track edits since there's nothing to diff
   against. */
.field.is-modified > span:first-child {
  display: inline-flex;
  align-items: center;
  gap: 7px;
}
.field.is-modified > span:first-child::after {
  content: '';
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: #F38A3F;
  box-shadow: 0 0 0 2px rgba(243, 138, 63, 0.18);
  flex: 0 0 7px;
}
/* `[hidden]` from the UA stylesheet has the same specificity as `.field`
   and loads first — without this our hidden Apt/Suite slot still shows
   because the .field rule wins. Force display:none for any hidden field. */
.field[hidden] { display: none !important; }
.field.span-2 {
  grid-column: span 2;
  @media (max-width: 600px) { grid-column: auto; }
}
.field > span {
  /* Field labels become eyebrow-style: tiny uppercase per the
     philosophy doc's typography hierarchy, deep-navy weight,
     stronger letter-spacing so they read as a label not a sentence. */
  font-weight: 700;
  font-size: 0.7rem;
  color: #7B8190;
  text-transform: uppercase;
  letter-spacing: 0.10em;
}
.field input, .field textarea, .field select {
  font-family: inherit;
  font-size: 1rem;
  padding: 12px 14px;
  /* Bento input: warm soft border (not cool gray), warm card fill
     (not pure white), squircle radius, soft inset highlight so the
     input reads as a recessed-yet-tactile surface. */
  border: 1px solid #E5DED3;
  border-radius: 14px;
  background: #FFFDF9;
  color: #373B4D;
  box-shadow: 0 1px 0 rgba(255, 255, 255, 0.85) inset;
  transition: border-color 0.15s ease, box-shadow 0.15s ease, background 0.15s ease;
}
.field input::placeholder,
.field textarea::placeholder { color: #7B8190; }
.field input:hover:not(:focus),
.field textarea:hover:not(:focus),
.field select:hover:not(:focus) {
  background: #FFFAF1;
  border-color: rgba(243, 138, 63, 0.35);
}
.field input:focus, .field textarea:focus, .field select:focus {
  outline: none;
  background: #FFFDF9;
  /* No orange edge — the field floats on a soft orange drop shadow instead.
     Border goes transparent (not removed) so the box doesn't shift on focus. */
  border-color: transparent;
  box-shadow:
    var(--sw-focus-shadow, 0 0 0 1px #F38A3F, 0 6px 13px rgba(243, 138, 63, 0.30)),
    0 1px 0 rgba(255, 255, 255, 0.85) inset;
}
.field small { font-size: 0.78rem; color: #7B8190; }

/* ═══ 20. Cross-component mobile rules ═══ */
/* Multi-selector mobile rules that don't fit cleanly inside any one
   component. Single-selector / single-component mobile rules live as
   nested @media inside their base rule. Tiny-phone overrides for the
   booking page. */
@media (max-width: 380px) {
  .step-panel { border-radius: 16px; }
}
@media (max-width: 600px) {
  html,
  body {
    max-width: 100%;
    overflow-x: hidden;
  }
  body:has(.booking-page),
  .booking-page,
  #booking-form,
  .step-card,
  .step-panel {
    min-width: 0;
    max-width: 100%;
  }
  .booking-page {
    overflow-x: hidden;
    overflow-x: clip;
  }

  /* iOS will auto-zoom the page when an input with font-size <16px
     receives focus. Explicitly sizing every form input to 16px
     prevents that jarring zoom-in/zoom-out cycle as the user moves
     between fields. */
  .field input,
  .field textarea,
  .field select,
  .address-field input,
  .zip-field input {
    font-size: 16px !important;
    padding: 12px 14px;
  }
  .address-field input {
    /* re-add icon padding (overrides above generic) */
    padding-left: 44px;
    padding-right: 44px;
  }

  /* Primary CTAs full-width — easier thumb tap on phones. Back stays
     auto-width on the left so it doesn't dominate. The .actions row
     is already a flex row with space-between; on mobile we force
     primary to flex-grow so it spans the rest. */
  .step-panel .actions { gap: 12px; }
  .step-panel .actions .btn-primary {
    flex: 1 1 auto;
    min-width: 0;
  }
  /* Step 1 has only Continue (no Back), already centered. */
  .step-panel[data-step="1"] .actions { justify-content: stretch; }
  .step-panel[data-step="1"] .actions .btn-primary { width: 100%; }

  /* Pull Back + Continue out to the card's edges on phones so they
     sit in the corners where the thumb naturally lands. Two things
     conspire on mobile to bury them in the middle:
       1. .step-panel has 24px horizontal padding (the card's gutter
          for content like text + inputs), and the actions row sits
          inside it.
       2. Step 3 specifically has `max-width: 880px; margin: 0 auto`
          on its actions row — desktop centering that on a flex-column
          parent collapses the row to content-width on mobile, leaving
          big empty gutters on both sides.
     Cancel both on phones: kill step 3's max-width centering, and
     pull every step's actions row out by -16px against the panel's
     24px padding. Net result: Back sits ~8px from the card's left
     edge, Continue (flex-grown) reaches to ~8px from the right
     edge, and `justify-content: space-between` puts the gap in the
     middle where it belongs. */
  .step-panel > .actions,
  .step-panel[data-step="3"] > .actions {
    max-width: none;
    margin-left: -16px;
    margin-right: -16px;
  }
  .step-panel[data-step="3"] > .actions,
  .step-panel[data-step="4"] > .actions {
    max-width: none;
    margin-left: 0;
    margin-right: 0;
    margin-top: 30px;
    padding-top: 8px;
    gap: 18px;
    align-items: stretch;
  }
  .step-panel[data-step="3"] > .actions .btn,
  .step-panel[data-step="4"] > .actions .btn {
    min-height: 58px;
    padding: 13px 18px;
  }
  .step-panel[data-step="3"] > .actions .btn-secondary,
  .step-panel[data-step="4"] > .actions .btn-secondary {
    flex: 0 0 clamp(112px, 34%, 150px);
  }
  .step-panel[data-step="3"] > .actions .btn-primary,
  .step-panel[data-step="4"] > .actions .btn-primary {
    flex: 1 1 0;
    margin-left: 0;
  }

  /* Service-card hover/checked overrides — flatten the lift effects on
     mobile since hover doesn't really apply on touch and the lift can
     read as flicker on tap. The compact-pill sizing lives next to the
     base .service-card-inner rule. */
  .service-card:hover .service-card-inner {
    transform: scale(1);
    border-color: var(--border);
    background: #FFFFFF;
    box-shadow:
      0 1px 1px rgba(39, 41, 54, 0.03),
      0 4px 12px rgba(39, 41, 54, 0.05);
  }
  .service-card input:checked + .service-card-inner,
  .service-card:hover input:checked + .service-card-inner {
    transform: scale(1);
    border-color: var(--orange);
    background: #FAF7F1;
    box-shadow:
      inset 0 0 0 1px var(--orange),
      0 4px 14px rgba(224, 122, 42, 0.10);
  }

  /* Confirmed-address hint stays visible without overflowing on
     narrow screens (long city names). Lower-specificity selector
     than the .address-field .address-hint base above — kept here
     since the value matches the base's font-size on desktop. */
  .address-hint { font-size: 0.85rem; word-break: break-word; }
}

/* ═══ 24. Buttons ═══ */
/* Buttons — flat */
.btn {
  font-family: inherit;
  font-size: 1rem;
  font-weight: 600;
  padding: 12px 24px;
  border-radius: 999px;
  border: 0;
  cursor: pointer;
  transition: background 0.18s ease, color 0.18s ease, opacity 0.2s ease;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  position: relative;
}
.btn:disabled { opacity: 0.45; cursor: not-allowed; }
.btn:active:not(:disabled) { transform: scale(0.98); transition: transform 0.08s ease, background 0.18s ease; }
.btn-primary { background: var(--orange); color: #fff; }
.btn-primary:hover:not(:disabled) { background: var(--orange-dark); }

/* ═══ 25. Progressive exposure ═══ */
/* ───── Progressive exposure (booking flow) ─────
   Hide the Continue / Confirm CTA entirely until the current step is
   complete. Removes the visual "pressure" of seeing the next button
   when the user hasn't done anything yet. The buttons already toggle
   their `disabled` attribute via JS based on each step's gate
   (service picked, slot picked, work summary typed, trip-charge
   agreed) — we just translate that disabled state into a fade+rise
   reveal instead of the desaturated "click me but you can't" look.
   Scoped under .step-card so the specificity (1,2,0) matches the
   per-step button rules elsewhere — without that, .step-panel
   [data-step="1"] #continue-step-1 wins for step 1 and snaps the
   transition. The trailing `[id]` token also keeps it ahead of the
   plain .btn:disabled rule that dims admin buttons. */
.step-card #continue-step-1[disabled],
.step-card #continue-step-2[disabled],
.step-card #continue-step-3[disabled],
.step-card #submit-booking[disabled] {
  opacity: 0;
  visibility: hidden;
  transform: translateY(10px);
  pointer-events: none;
  /* visibility flips at the END of the transition when going OUT
     so the fade reads cleanly; flips at the START going IN so the
     button is immediately interactive. */
  transition:
    opacity 0.32s ease,
    transform 0.42s cubic-bezier(0.22, 1, 0.36, 1),
    visibility 0s linear 0.32s;
}
.step-card #continue-step-1:not([disabled]),
.step-card #continue-step-2:not([disabled]),
.step-card #continue-step-3:not([disabled]),
.step-card #submit-booking:not([disabled]) {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
  transition:
    opacity 0.32s ease,
    transform 0.42s cubic-bezier(0.22, 1, 0.36, 1),
    visibility 0s linear 0s;
}

/* ═══ 26. Button arrows ═══ */
/* ───── Button arrows ─────
   Inline SVG chevrons inside Back/Continue/Submit/Finish buttons. The
   .btn class already has display:inline-flex + gap:8px, so adding the
   arrow as a direct child gets correct spacing automatically. */
.btn-arrow {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  /* Default-state position. Hover and progressive-exposure rules
     below override transform with their own transitions. */
  transform: translateX(0);
  transition: transform 0.22s cubic-bezier(0.22, 1, 0.36, 1);
  will-change: transform;
}

/* Hover nudge — forward arrow scoots right, back arrow scoots left.
   Reads as "this button is going to take you that direction." */
.btn:hover:not(:disabled) .btn-arrow-forward { transform: translateX(3px); }
.btn:hover:not(:disabled) .btn-arrow-back    { transform: translateX(-3px); }
.btn:active:not(:disabled) .btn-arrow-forward { transform: translateX(1px); }
.btn:active:not(:disabled) .btn-arrow-back    { transform: translateX(-1px); }

/* Continue/Submit arrow entrance — runs as a one-shot @keyframes
   animation (not a transition) so the transform value reverts to
   the base after the animation ends, leaving the hover-nudge rule
   above free to take over without specificity conflicts.
   Delay 0.4s so the arrow lands AFTER the button itself has settled
   from its progressive-exposure fade-up. Elastic curve gives it a
   "nock into place" snap. */
.step-card #continue-step-1:not([disabled]) .btn-arrow-forward,
.step-card #continue-step-2:not([disabled]) .btn-arrow-forward,
.step-card #continue-step-3:not([disabled]) .btn-arrow-forward,
.step-card #submit-booking:not([disabled]) .btn-arrow-forward {
  animation: btnArrowEnter 0.5s cubic-bezier(0.34, 1.5, 0.64, 1) 0.4s backwards;
}
@keyframes btnArrowEnter {
  0%   { opacity: 0; transform: translateX(-10px); }
  100% { opacity: 1; transform: translateX(0); }
}
.btn-secondary {
  /* Bento ghost-card button: warm card fill, hairline warm border so
     it reads as a secondary chip beside the orange primary instead
     of a flat cool inset rectangle. */
  background: #FFFDF9;
  color: #373B4D;
  box-shadow: 0 0 0 1px #E5DED3 inset, 0 1px 0 rgba(255, 255, 255, 0.85) inset;
}
.btn-secondary:hover:not(:disabled) {
  background: #FFFAF1;
  box-shadow: 0 0 0 1px rgba(243, 138, 63, 0.42) inset, 0 1px 0 rgba(255, 255, 255, 0.85) inset;
}
.btn-ghost { background: transparent; color: #373B4D; }
.btn-ghost:hover { background: rgba(243, 138, 63, 0.10); color: #373B4D; }
.btn-block { width: 100%; }
/* Loading state — keeps the button label visible (the JS swaps it to
   "Confirming details…" or similar on click) and replaces the trailing
   arrow icon with a spinner so the customer gets both verbal AND
   visual feedback the instant they tap. The old behavior hid the
   label entirely (color: transparent) and showed only a small middle
   spinner, which read as "did anything happen?" during the 1-3s
   network round-trip. */
.btn.is-loading { pointer-events: none; }
.btn.is-loading .btn-arrow,
.btn.is-loading .btn-arrow-forward,
.btn.is-loading .btn-arrow-back { display: none; }
.btn.is-loading::after {
  content: '';
  display: inline-block;
  width: 14px; height: 14px;
  margin-left: 4px;
  border: 2px solid rgba(255,255,255,0.4);
  border-top-color: #fff;
  border-radius: 50%;
  animation: spin 0.7s linear infinite;
  vertical-align: middle;
}
.btn-secondary.is-loading::after { border-color: rgba(31,44,68,0.3); border-top-color: var(--navy); }
@keyframes spin { to { transform: rotate(360deg); } }

/* Success-state morph. Replaces the loading spinner with a sage-green
   fill sweep + an SVG checkmark that draws itself via stroke-dasharray.
   Plays for ~700ms after a successful submit before the page hands
   off to the step 5 success card. The button keeps its width so the
   layout doesn't shift mid-animation. */
.btn.is-success {
  pointer-events: none;
  position: relative;
  overflow: hidden;
  background: #2E8A56 !important;
  color: #FFFDF9 !important;
  border-color: #2E8A56 !important;
  transition: background 0.28s ease, border-color 0.28s ease;
}
.btn.is-success .btn-arrow,
.btn.is-success .btn-arrow-forward,
.btn.is-success .btn-arrow-back { display: none; }
.btn.is-success::after { display: none; }
/* The sweep — a paler-green wash that races across the button left to
   right behind the label, finishing just as the checkmark draws.
   Reads as "the booking is shipping" rather than "the form is dead." */
.btn.is-success::before {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.22) 50%, transparent 100%);
  transform: translateX(-100%);
  animation: btnSuccessSweep 0.55s ease-out forwards;
  pointer-events: none;
}
@keyframes btnSuccessSweep {
  from { transform: translateX(-100%); }
  to   { transform: translateX(120%); }
}
/* Checkmark sits centered over the label text. The label fades out
   while the check fades+draws in. Using stroke-dasharray = perimeter
   + dashoffset animation = the classic "draw" effect, fast (300ms)
   so the button doesn't linger past the human "yes!" moment. */
.btn-success-check {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 22px;
  height: 22px;
  transform: translate(-50%, -50%) scale(0.6);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.18s ease, transform 0.32s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.btn-success-check path {
  fill: none;
  stroke: #FFFDF9;
  stroke-width: 2.4;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-dasharray: 22;
  stroke-dashoffset: 22;
  transition: stroke-dashoffset 0.32s cubic-bezier(0.55, 0.06, 0.4, 0.95) 0.22s;
}
.btn.is-success .btn-success-check {
  opacity: 1;
  transform: translate(-50%, -50%) scale(1);
}
.btn.is-success .btn-success-check path { stroke-dashoffset: 0; }
.btn.is-success .btn-label,
.btn.is-success > span:not(.btn-success-check) {
  opacity: 0;
  transition: opacity 0.16s ease;
}

/* Success card reveal. The existing step-5 reveal is a hard show/hide;
   this layers a smooth slide-up + fade-in so the recap doesn't pop
   into existence. Triggered by the .is-revealing class added by JS
   right before showStep(5). */
.step-panel[data-step="5"].is-revealing {
  animation: successCardReveal 0.42s cubic-bezier(0.16, 1, 0.3, 1) both;
}
@keyframes successCardReveal {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* Confetti — purely CSS particles spawned in a one-shot container at
   the top of the success card. ~14 elements, each with a slightly
   different fall arc + rotation so they don't read as identical
   sprites. Self-cleans after 1.6s; the container is removed by JS. */
.success-confetti {
  position: absolute;
  inset: 0;
  pointer-events: none;
  overflow: hidden;
  z-index: 1;
}
.success-confetti-piece {
  position: absolute;
  top: -10px;
  width: 8px;
  height: 14px;
  border-radius: 2px;
  opacity: 0;
  animation: confettiFall 1.5s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
@keyframes confettiFall {
  0%   { opacity: 0; transform: translateY(-20px) rotate(0deg); }
  10%  { opacity: 1; }
  100% { opacity: 0; transform: translateY(360px) rotate(540deg); }
}

.actions {
  display: flex;
  justify-content: space-between;
  margin-top: 24px;
  gap: 12px;
}
.actions .btn-primary { margin-left: auto; }

/* Address field on step 1 — wider than ZIP since it accepts a full address */
/* ═══ 27. Address field ═══ */
.address-field {
  max-width: 520px;
  margin-bottom: 28px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  @media (max-width: 760px) { max-width: 100%; }
}
.address-field > span {
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--text-muted);
}
.address-input-wrap {
  position: relative;
  display: block;
  /* Establish a stacking context so .pac-container (which Google appends
     to <body>) remains positioned correctly relative to the page, while
     the input below sits cleanly above any sibling layers. */
  z-index: 1;
}
.address-field input {
  /* Opaque background — non-negotiable. With transparent we were letting
     browser autofill suggestion chips and password-manager icon bars bleed
     through underneath the input, which produced the row of mystery glyphs
     in the user's earlier screenshot. Soft cream-white instead of pure
     white for the modern "luminous" look without sacrificing autofill
     suppression. */
  background: #FCFCFA !important;
  border: 1px solid var(--border-soft, #ECE7DD);
  border-radius: 14px;
  padding: 16px 44px 16px 46px;
  width: 100%;
  font-size: 1rem;
  font-weight: 500;
  color: var(--text);
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  /* Suppress browser-native UI that loves to live inside text inputs. */
  -webkit-text-fill-color: var(--text);
  caret-color: var(--orange);
  transition: border-color 0.22s ease, box-shadow 0.22s ease, background 0.22s ease, border-radius 0.22s ease;
  position: relative;
  z-index: 2;
}
/* Kill the yellow autofill background some browsers paint behind the text. */
.address-field input:-webkit-autofill,
.address-field input:-webkit-autofill:hover,
.address-field input:-webkit-autofill:focus {
  animation: addressAutofillStart 0.01s both;
  -webkit-box-shadow: 0 0 0 1000px #FCFCFA inset !important;
  box-shadow: 0 0 0 1000px #FCFCFA inset !important;
  -webkit-text-fill-color: var(--text) !important;
  transition: background-color 9999s ease-in-out 0s;
}
@keyframes addressAutofillStart {
  from { opacity: 0.999; }
  to { opacity: 1; }
}
/* Hide unrelated WebKit-injected credential buttons while allowing
   the contact/address autofill affordance Safari uses on iOS. */
.address-field input::-webkit-credentials-auto-fill-button,
.address-field input::-webkit-credit-card-auto-fill-button {
  visibility: hidden !important;
  display: none !important;
  pointer-events: none !important;
  position: absolute !important;
  right: 0 !important;
}
.address-field input::-webkit-search-cancel-button,
.address-field input::-webkit-search-decoration {
  -webkit-appearance: none;
  display: none;
}
/* Atmospheric placeholder — charcoal at low opacity reads as "tracing
   paper" rather than as a solid medium-saturation beige. The example
   should hint at what to type without competing with real entered
   text. -webkit-text-fill-color must be set explicitly here because
   the input rule above sets it to var(--text) for autofill suppression,
   which silently overrides ::placeholder { color } on iOS Safari + on
   Chrome's autofill state. */
.address-field input::placeholder {
  color: rgba(39, 41, 54, 0.30);
  -webkit-text-fill-color: rgba(39, 41, 54, 0.30);
  font-weight: 400;
  opacity: 1;
}
/* Static "next thing" hint: while the field is empty AND not focused,
   the border picks up a super-light orange tint. No halo, no
   animation — just a quiet visual cue that this is where to type
   next. The :not(:focus) guard means it disappears the moment the
   user clicks in (the focus ring takes over). */
.address-field input:placeholder-shown:not(:focus) {
  border-color: rgba(245, 133, 54, 0.22);
}
.address-field input:hover:not(:focus) {
  /* Hover stays inside the warm palette — lighter card-highlight
     fill (was pure #FFFFFF) plus an orange-tinged border so the
     hover-state matches the rest of the bento inputs. */
  border-color: rgba(243, 138, 63, 0.35);
  background: #FFFAF1 !important;
}
.address-field input:focus {
  outline: none;
  background: #FFFDF9 !important;
  /* No orange edge — float on the shared soft orange drop shadow so the
     focused address bar matches every other .field input on the page. */
  border-color: transparent;
  box-shadow: var(--sw-focus-shadow, 0 0 0 1px #F38A3F, 0 6px 13px rgba(243, 138, 63, 0.30));
}
.address-field input.is-valid {
  border-color: var(--border-soft, #ECE7DD);
  /* Settle animation — when the address is verified, the input bar
     does a tiny spring "settle" that reads as a haptic confirmation.
     The animation is short and very subtle (1px translate, 0.99 →
     1.0 scale) so it feels physical, not bouncy. */
  animation: addressSettle 0.42s cubic-bezier(0.22, 1, 0.36, 1);
}
@keyframes addressSettle {
  0%   { transform: translateY(0)    scale(1); }
  35%  { transform: translateY(-2px) scale(1.005); }
  70%  { transform: translateY(1px)  scale(0.998); }
  100% { transform: translateY(0)    scale(1); }
}

