/* ─────────────────────────────────────────────────────────
   GOOGLE FONTS

   No @import here. The stylesheet is a <link> in layout.tsx so the
   families follow the tenant's font pack (lib/fonts.ts). The
   --font-* fallbacks below mirror the 'default' pack.
   ───────────────────────────────────────────────────────── */

/* ─────────────────────────────────────────────────────────
   DESIGN TOKENS — FIXED SYSTEM
   Brand tokens (--brand-*) are injected server-side in layout.tsx.
   These fallbacks ensure the page renders without flash.
   ───────────────────────────────────────────────────────── */

:root {
  /* Brand (overridden per-tenant at SSR) */
  --brand-primary:       #1B6CA8;
  --brand-primary-hover: #0F5A96;
  --brand-primary-light: #B5D6E8;
  --brand-accent:        #1B6CA8;

  /* Canvas */
  --canvas-white:        #FFFFFF;
  --canvas-off-white:    #F8F7F5;
  --canvas-light-gray:   #F2F1EF;
  --canvas-border:       #E5E3DF;

  /* Text */
  --text-primary:        #1A1A1A;
  --text-secondary:      #5A5A5A;
  --text-muted:          #9A9A9A;
  --text-inverse:        #FFFFFF;

  /* Status */
  --status-success:      #2D6A4F;
  --status-success-bg:   #EAF4EF;
  --status-error:        #B5341C;
  --status-error-bg:     #FDF0ED;
  --status-warning:      #92620A;
  --status-warning-bg:   #FEF6E7;

  /* Band — the alternating full-bleed sections. These defaults reproduce
     the off-white stripe exactly: each is the token it stands in for, so
     a tenant that leaves theme.band off renders as before. A tenant that
     turns it on overrides every one of these at SSR (lib/theme.ts) with
     values derived from the brand colour, and the band rule below swaps
     the canvas/text/brand tokens for them inside the band so everything
     it contains re-derives without knowing it is on colour. */
  --band-bg:               var(--canvas-off-white);
  --band-edge:             var(--canvas-border);
  --band-primary:          var(--brand-primary);
  --band-primary-hover:    var(--brand-primary-hover);
  --band-primary-light:    var(--brand-primary-light);
  --band-accent:           var(--brand-accent);
  --band-surface:          var(--canvas-white);
  --band-surface-recessed: var(--canvas-off-white);
  --band-surface-hover:    var(--canvas-light-gray);
  --band-border:           var(--canvas-border);
  --band-text-primary:     var(--text-primary);
  --band-text-secondary:   var(--text-secondary);
  --band-text-muted:       var(--text-muted);
  --band-text-inverse:     var(--text-inverse);

  /* Page — the same tokens as copies the band rule never touches, for
     an object that stays white inside a tinted band (.band-escape). The
     brand four are re-emitted per tenant at SSR beside --brand-*. */
  --page-primary:          var(--brand-primary);
  --page-primary-hover:    var(--brand-primary-hover);
  --page-primary-light:    var(--brand-primary-light);
  --page-accent:           var(--brand-accent);
  --page-white:            #FFFFFF;
  --page-off-white:        #F8F7F5;
  --page-light-gray:       #F2F1EF;
  --page-border:           #E5E3DF;
  --page-text-primary:     #1A1A1A;
  --page-text-secondary:   #5A5A5A;
  --page-text-muted:       #9A9A9A;
  --page-text-inverse:     #FFFFFF;

  /* Typography (overridden per-tenant at SSR — see lib/fonts.ts) */
  --font-display: 'DM Serif Display', Georgia, serif;
  --font-body:    'DM Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;

  /* Weight tokens. Display families vary wildly in optical weight —
     DM Serif Display ships 400 only, Cinzel wants 600 — so anything
     styled with --font-display takes its weight from here, never a
     literal. Body keeps its own 300–700 hierarchy; only the base
     weight is tokenised. */
  --font-display-weight:        400;
  --font-display-weight-strong: 400;
  --font-body-weight:           400;

  /* Optical size correction. Two families set at the same px render at
     very different apparent sizes (Cinzel is small-caps, so it reads
     large and runs wide where DM Serif Display doesn't). Packs correct
     for that here instead of every tenant overriding sizes by hand.

       --font-body-scale     multiplies the ENTIRE --text-* ramp, so it
                             moves every size in the app together.
       --font-display-scale  extra multiplier on --font-display rules
                             only. Relative to body scale, not absolute:
                             the two compound. */
  --font-body-scale:    1;
  --font-display-scale: 1;

  /* --font-display-transform and --font-display-tracking are deliberately
     NOT declared here. Every --font-display rule reads them as
     `var(--font-display-transform, none)` / `var(--font-display-tracking,
     <that rule's own value>)`, so leaving them undefined is what "this
     pack doesn't set caps" looks like — declaring a default here would
     override the per-rule tracking the ramp was drawn with. A pack that
     wants caps emits both from buildFontStyles (lib/fonts.ts). */

  /* Type scale */
  --text-xs:   calc(11px * var(--font-body-scale));
  --text-sm:   calc(13px * var(--font-body-scale));
  --text-base: calc(15px * var(--font-body-scale));
  --text-md:   calc(17px * var(--font-body-scale));
  --text-lg:   calc(21px * var(--font-body-scale));
  --text-xl:   calc(28px * var(--font-body-scale));
  --text-2xl:  calc(38px * var(--font-body-scale));
  --text-3xl:  calc(52px * var(--font-body-scale));
  --text-hero: calc(68px * var(--font-body-scale));

  /* Spacing */
  --space-xs:  4px;
  --space-sm:  8px;
  --space-md:  16px;
  --space-lg:  24px;
  --space-xl:  40px;
  --space-2xl: 64px;
  --space-3xl: 96px;

  /* Border radius */
  --radius-sm: 4px;
  --radius-md: 8px;
  --radius-lg: 12px;
  --radius-xl: 16px;

  /* Shadows */
  --shadow-sm: 0 1px 3px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.05);
  --shadow-md: 0 4px 12px rgba(0,0,0,0.08), 0 2px 4px rgba(0,0,0,0.04);
  --shadow-lg: 0 16px 40px rgba(0,0,0,0.10), 0 4px 12px rgba(0,0,0,0.06);
  --shadow-xl: 0 32px 64px rgba(0,0,0,0.12), 0 8px 24px rgba(0,0,0,0.08);

  /* Transitions */
  --transition-fast: 150ms ease;
  --transition-base: 250ms ease;
  --transition-slow: 400ms ease;

  /* Layout */
  --max-content: 680px;
  --max-form:    480px;
  --max-wide:    960px;

  /* Logo */
  --logo-height: 36px;
}

/* ─────────────────────────────────────────────────────────
   RESET & BASE
   ───────────────────────────────────────────────────────── */

*, *::before, *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

html, body {
  height: 100%;
}

body {
  font-family: var(--font-body);
  font-size: var(--text-base);
  font-weight: var(--font-body-weight);
  color: var(--text-primary);
  background: var(--canvas-white);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

a {
  color: inherit;
  text-decoration: none;
}

button {
  font-family: inherit;
  cursor: pointer;
  border: none;
  background: none;
}

img {
  display: block;
  max-width: 100%;
}

/* ─────────────────────────────────────────────────────────
   TYPOGRAPHY
   ───────────────────────────────────────────────────────── */

.headline-display {
  font-family: var(--font-display);
  text-transform: var(--font-display-transform, none);
  font-size: calc(var(--text-3xl) * var(--font-display-scale));
  font-weight: var(--font-display-weight);
  line-height: 1.15;
  letter-spacing: var(--font-display-tracking, -0.01em);
  color: var(--text-primary);
  text-align: center;
}

.headline-section {
  font-family: var(--font-display);
  text-transform: var(--font-display-transform, none);
  letter-spacing: var(--font-display-tracking, normal);
  font-size: calc(var(--text-2xl) * var(--font-display-scale));
  font-weight: var(--font-display-weight);
  line-height: 1.2;
  color: var(--text-primary);
  text-align: center;
  max-width: 560px;
  margin-left: auto;
  margin-right: auto;
}

.subheadline {
  font-family: var(--font-body);
  font-size: var(--text-md);
  font-weight: 300;
  color: var(--text-secondary);
  line-height: 1.65;
  text-align: center;
}

/* ─────────────────────────────────────────────────────────
   EYEBROW LABEL
   ───────────────────────────────────────────────────────── */

.eyebrow {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-sm);
  font-family: var(--font-body);
  font-size: var(--text-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--brand-accent);
  margin-bottom: var(--space-md);
}

.eyebrow::before {
  content: '';
  display: block;
  width: 24px;
  height: 1px;
  background: var(--brand-accent);
  flex-shrink: 0;
}

/* ─────────────────────────────────────────────────────────
   PAGE LAYOUT
   ───────────────────────────────────────────────────────── */

.page-wrapper {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  /* clip, not hidden: hidden makes this a scroll container, and a
     scroll container is what every descendant's position: sticky
     sticks to. This one never scrolls (it grows with its content), so
     the intake submit bar and the admin tabs/footer just sat in flow
     at the end of the page instead of pinning to the viewport. clip
     cuts off sideways overflow the same way without becoming a
     scroll container. */
  overflow-x: clip;
}

.page-content {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 100%;
  max-width: var(--max-content);
  margin: 0 auto;
  padding: 0 var(--space-xl) var(--space-2xl);
}

.page-content.wide {
  max-width: var(--max-wide);
}

/* ─────────────────────────────────────────────────────────
   SITE HEADER
   ───────────────────────────────────────────────────────── */

.site-header {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-xl);
  background: var(--canvas-white);
}

.site-header-logo img,
.site-header-logo-img {
  height: var(--logo-height);
  width: auto;
  display: block;
}

.site-header-logo-text {
  font-family: var(--font-display);
  text-transform: var(--font-display-transform, none);
  letter-spacing: var(--font-display-tracking, normal);
  font-size: calc(var(--text-lg) * var(--font-display-scale));
  font-weight: var(--font-display-weight-strong);
  color: var(--brand-primary);
}

.site-header-phone {
  position: absolute;
  right: var(--space-xl);
  top: 50%;
  transform: translateY(-50%);
  font-size: var(--text-base);
  font-weight: 400;
  color: var(--text-secondary);
  text-decoration: none;
  transition: color var(--transition-fast);
}

.site-header-phone:hover {
  color: var(--text-primary);
}

/* ─────────────────────────────────────────────────────────
   BUTTONS
   ───────────────────────────────────────────────────────── */

.btn-primary {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-sm);
  padding: 14px 28px;
  background: var(--brand-primary);
  color: var(--text-inverse);
  font-family: var(--font-body);
  font-size: var(--text-base);
  font-weight: 600;
  letter-spacing: 0.01em;
  border-radius: var(--radius-md);
  border: none;
  cursor: pointer;
  transition: background var(--transition-fast), transform var(--transition-fast), box-shadow var(--transition-fast);
  text-decoration: none;
  white-space: nowrap;
}

.btn-primary:hover {
  background: var(--brand-primary-hover);
  transform: translateY(-1px);
  box-shadow: var(--shadow-md);
}

.btn-primary:active {
  transform: none;
  box-shadow: none;
}

.btn-primary:disabled {
  opacity: 0.45;
  cursor: not-allowed;
  transform: none;
  box-shadow: none;
}

.btn-primary.btn-lg {
  padding: 18px 40px;
  font-size: var(--text-md);
  width: 100%;
  max-width: 400px;
}

.btn-secondary {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-sm);
  padding: 10px 20px;
  background: transparent;
  color: var(--text-secondary);
  font-family: var(--font-body);
  font-size: var(--text-sm);
  font-weight: 500;
  border: 1px solid var(--canvas-border);
  border-radius: var(--radius-md);
  cursor: pointer;
  transition: color var(--transition-fast), border-color var(--transition-fast), background var(--transition-fast);
  text-decoration: none;
}

.btn-secondary:hover {
  border-color: var(--text-muted);
  background: var(--canvas-light-gray);
  color: var(--text-primary);
}

.btn-ghost {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  padding: 6px 0;
  background: none;
  border: none;
  color: var(--text-muted);
  font-family: var(--font-body);
  font-size: var(--text-sm);
  font-weight: 400;
  cursor: pointer;
  text-decoration: underline;
  text-decoration-color: transparent;
  transition: color var(--transition-fast), text-decoration-color var(--transition-fast);
}

.btn-ghost:hover {
  color: var(--text-secondary);
  text-decoration-color: var(--canvas-border);
}

/* ─────────────────────────────────────────────────────────
   FORM ELEMENTS
   ───────────────────────────────────────────────────────── */

.form-group {
  margin-bottom: var(--space-md);
  width: 100%;
}

.form-label {
  display: block;
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--text-primary);
  margin-bottom: var(--space-xs);
}

.form-input {
  width: 100%;
  padding: 11px 14px;
  font-family: var(--font-body);
  font-size: var(--text-base);
  color: var(--text-primary);
  background: var(--canvas-white);
  border: 1px solid var(--canvas-border);
  border-radius: var(--radius-md);
  outline: none;
  transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
  appearance: none;
}

.form-input::placeholder {
  color: var(--text-muted);
}

.form-input:focus {
  border-color: var(--brand-primary);
  box-shadow: 0 0 0 3px rgba(0,0,0,0.06);
}

.form-input.has-error {
  border-color: var(--status-error);
}

.form-hint {
  margin-top: var(--space-xs);
  font-size: var(--text-sm);
  color: var(--text-muted);
}

.form-error {
  margin-top: var(--space-xs);
  font-size: var(--text-sm);
  color: var(--status-error);
  display: flex;
  align-items: center;
  gap: 4px;
}

.form-success {
  margin-top: var(--space-xs);
  font-size: var(--text-sm);
  color: var(--status-success);
  display: flex;
  align-items: center;
  gap: 4px;
}

/* ─────────────────────────────────────────────────────────
   OPTION CARDS (selection cards — text only, no icons)
   ───────────────────────────────────────────────────────── */

.option-card-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-md);
  width: 100%;
  align-items: stretch;
}

.option-card-grid.cols-3 {
  grid-template-columns: 1fr 1fr 1fr;
}

.option-card {
  position: relative;
  padding: var(--space-lg);
  background: var(--canvas-white);
  border: 1.5px solid var(--canvas-border);
  border-radius: var(--radius-lg);
  cursor: pointer;
  transition: all var(--transition-fast);
  text-align: left;
  outline: none;
  display: flex;
  flex-direction: column;
}

.option-card:hover {
  border-color: var(--brand-primary-light);
  background: var(--canvas-off-white);
}

.option-card.selected {
  border-color: var(--brand-primary);
  background: var(--canvas-white);
  box-shadow: var(--shadow-sm);
}

.option-card.selected::after {
  content: '';
  position: absolute;
  top: 12px;
  right: 12px;
  width: 18px;
  height: 18px;
  background: var(--brand-primary);
  border-radius: 50%;
  background-image: url("data:image/svg+xml,%3Csvg width='10' height='8' viewBox='0 0 10 8' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 4L3.5 6.5L9 1' stroke='white' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: center;
}

.option-card-name {
  font-size: var(--text-base);
  font-weight: 500;
  color: var(--text-primary);
  margin-bottom: 2px;
}

/* Model name left, manufacturer wordmark right. Concrete models have no
   manufacturer, so this row holds a single child and lays out exactly as the
   bare name did before. */
.option-card-title-row {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-sm);
}

/* Sized by height, never width. Height comes from the manufacturer (see
   ManufacturerDef.logoHeight): a horizontal wordmark reads at 13px, a stacked
   lockup needs roughly double before its strapline is legible. Held back with
   opacity so the manufacturer never out-shouts the model name beside it. */
.option-card-mark {
  width: auto;
  max-width: 40%;
  object-fit: contain;
  flex-shrink: 0;
  opacity: 0.75;
  margin-top: 3px;
}

/* Fallback for a manufacturer with no wordmark on file. */
.option-card-mark-text {
  flex-shrink: 0;
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-muted);
  margin-top: 3px;
}

.option-card-desc {
  font-size: var(--text-sm);
  color: var(--text-secondary);
  font-weight: 300;
  line-height: 1.5;
  margin-top: var(--space-xs);
}

.option-card-meta {
  font-size: var(--text-xs);
  color: var(--text-muted);
  margin-top: var(--space-sm);
  font-weight: 500;
  letter-spacing: 0.02em;
}

.option-card-price {
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--brand-primary);
  margin-top: auto;
  padding-top: var(--space-sm);
}

.option-card-incompat-note {
  font-size: var(--text-xs);
  font-weight: 500;
  color: var(--text-muted);
  margin-top: auto;
  padding-top: var(--space-sm);
  font-style: italic;
}

.option-card-dims {
  font-size: var(--text-sm);
  color: var(--text-muted);
  margin-top: 2px;
}

/* Model specs and features, inside the pool-model drawer. Deliberately
   quiet: this is reference material someone opened on purpose, so it sits
   below the photos and never competes with the card above it. Flat chips,
   no border colour, no hover — nothing here is clickable. */
.model-spec-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  list-style: none;
  margin: 0;
  padding: var(--space-md);
}

.model-spec-chip {
  padding: 4px 10px;
  border: 1px solid var(--canvas-border);
  border-radius: var(--radius-sm);
  background: var(--canvas-white);
  font-size: var(--text-xs);
  font-weight: 500;
  color: var(--text-secondary);
}

/* ─────────────────────────────────────────────────────────
   REVEAL PANELS — configuration revealed by a selection above it,
   shared by the pool decking cascade and both outdoor cascades.
   ───────────────────────────────────────────────────────── */

/* A block of configuration revealed by a selection above it. Reads as a
   CHILD of whatever produced it — the tinted ground and brand-coloured
   left edge echo the selected card's own border — while its bounds
   separate it from the next block. A full-width rule was tried first and
   read as a page break rather than a grouping. */
.reveal-panel {
  padding: var(--space-md);
  background: var(--canvas-off-white);
  border-radius: var(--radius-lg);
  border-left: 3px solid var(--brand-primary);
}
.reveal-panel + .reveal-panel { margin-top: var(--space-md); }

/* The label pair heading a section or a reveal panel. */
.section-label {
  font-size: var(--text-base);
  font-weight: 500;
  color: var(--text-primary);
  margin-bottom: var(--space-xs);
}

.section-sublabel {
  font-size: var(--text-sm);
  color: var(--text-muted);
  font-weight: 300;
}

/* ─────────────────────────────────────────────────────────
   OUTDOOR SCOPE — genuinely outdoor-specific controls only.
   Selection (cards, variant chips, tier chips) all reuse
   .option-card / .option-card-grid from OPTION CARDS above;
   see OUTDOOR SELECTION BLOCKS below for where they render.
   ───────────────────────────────────────────────────────── */

.outdoor-field {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
}

.outdoor-field-label {
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--text-primary);
}

.outdoor-exact-toggle {
  align-self: flex-start;
  background: none;
  border: none;
  padding: 0;
  font-family: var(--font-body);
  font-size: var(--text-xs);
  color: var(--text-muted);
  text-decoration: underline;
  cursor: pointer;
  transition: color var(--transition-fast);
}

.outdoor-exact-toggle:hover {
  color: var(--brand-primary);
}

.outdoor-qty-row {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
}

.outdoor-qty-input {
  max-width: 140px;
}

.outdoor-qty-suffix {
  font-size: var(--text-sm);
  color: var(--text-muted);
  white-space: nowrap;
}


.outdoor-card-note {
  font-size: var(--text-xs);
  color: var(--text-muted);
  font-style: italic;
}

/* ─────────────────────────────────────────────────────────
   OUTDOOR SELECTION BLOCKS (configuration for selected
   features, rendered directly beneath the subsection's own
   card grid — one block per selected feature in that
   subsection, in catalog order. The card grid is
   selection-only and never resizes; this is where variant
   chips, tier chips, and the exact-size input live.
   Modeled on the decking section in PoolModelStep: a label,
   an optional sub-label, then the controls.)
   ───────────────────────────────────────────────────────── */

/* The configuration panel for one selected card uses the shared
   .reveal-panel class (see REVEAL PANELS above). */

.outdoor-selection-name {
  font-size: var(--text-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--brand-primary);
  margin-bottom: var(--space-sm);
}

.outdoor-selection-controls {
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
}

/* ─────────────────────────────────────────────────────────
   OUTDOOR OFFER CARD (attached-mode gate, shown in the pool funnel)
   ───────────────────────────────────────────────────────── */

.outdoor-offer-card {
  width: 100%;
  padding: var(--space-lg);
  background: var(--canvas-off-white);
  border: 1.5px solid var(--canvas-border);
  border-radius: var(--radius-lg);
  margin-bottom: var(--space-xl);
}

.outdoor-offer-headline {
  font-size: var(--text-base);
  font-weight: 500;
  color: var(--text-primary);
  margin-bottom: 4px;
}

.outdoor-offer-body {
  font-size: var(--text-sm);
  color: var(--text-secondary);
  font-weight: 300;
  line-height: 1.5;
  margin-bottom: var(--space-md);
}

.outdoor-offer-toggle {
  display: flex;
  gap: var(--space-sm);
}

.outdoor-offer-option {
  flex: 1;
  padding: var(--space-sm) var(--space-md);
  background: var(--canvas-white);
  border: 1.5px solid var(--canvas-border);
  border-radius: var(--radius-md);
  font-family: var(--font-body);
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--text-secondary);
  cursor: pointer;
  text-align: center;
  transition: all var(--transition-fast);
  outline: none;
}

.outdoor-offer-option:hover {
  border-color: var(--brand-primary-light);
}

.outdoor-offer-option.selected {
  border-color: var(--brand-primary);
  background: var(--brand-primary-light);
  color: var(--brand-primary);
}

/* ─────────────────────────────────────────────────────────
   STEP NAV (back / continue row)
   ───────────────────────────────────────────────────────── */

.step-nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  margin-top: var(--space-xl);
  padding-top: var(--space-xl);
  border-top: 1px solid var(--canvas-border);
}

.step-nav.centered {
  justify-content: center;
}

/* ─────────────────────────────────────────────────────────
   PROGRESS INDICATOR (milestone stepper)
   ───────────────────────────────────────────────────────── */

.step-milestone {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  max-width: 280px;
  margin: 0 auto var(--space-xl);
  padding: 8px 0;
}

.step-milestone-track {
  position: absolute;
  left: 6px;
  right: 6px;
  top: 50%;
  transform: translateY(-50%);
  height: 2px;
  background: var(--canvas-border);
}

.step-milestone-fill {
  height: 100%;
  width: 0;
  background: var(--brand-primary);
}

.step-milestone-dot {
  position: relative;
  z-index: 1;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: var(--canvas-white);
  border: 2px solid var(--canvas-border);
  flex-shrink: 0;
  transition: all 0.35s ease;
}

.step-milestone-dot.past {
  background: var(--brand-primary);
  border-color: var(--brand-primary);
}

.step-milestone-dot.active {
  width: 16px;
  height: 16px;
  background: var(--brand-primary);
  border-color: var(--brand-primary);
  box-shadow: 0 0 0 4px rgba(0, 0, 0, 0.07);
}

/* ─────────────────────────────────────────────────────────
   MODALS
   ───────────────────────────────────────────────────────── */

.modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(10,10,10,0.55);
  backdrop-filter: blur(4px);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-md);
  z-index: 1000;
  /* A modal taller than the viewport still has to be reachable, and a
     drag that runs past its end must stop here rather than scrolling
     the page underneath. */
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
}

.modal-card {
  position: relative;
  background: var(--canvas-white);
  border-radius: var(--radius-xl);
  padding: 40px;
  box-shadow: var(--shadow-xl);
  max-width: var(--max-form);
  width: 100%;
  /* dvh, not vh: on phones vh is the tallest the viewport ever gets, so
     a 90vh card is taller than the screen while the address bar is
     showing and its buttons sit under the chrome. The vh line stays as
     the fallback for browsers without dvh. */
  max-height: calc(90vh - var(--space-md));
  max-height: calc(90dvh - var(--space-md));
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  /* Centres inside the now-scrollable overlay without the flexbox
     overflow trap, where a card taller than the line gets its top
     clipped and made unreachable. */
  margin: auto;
}

.modal-close {
  position: absolute;
  top: 16px;
  right: 16px;
  width: 32px;
  height: 32px;
  background: var(--canvas-light-gray);
  border: none;
  border-radius: 50%;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: calc(18px * var(--font-body-scale));
  color: var(--text-secondary);
  transition: background var(--transition-fast);
  line-height: 1;
}

.modal-close:hover {
  background: var(--canvas-border);
}

.modal-title {
  font-family: var(--font-display);
  text-transform: var(--font-display-transform, none);
  letter-spacing: var(--font-display-tracking, normal);
  font-size: calc(var(--text-xl) * var(--font-display-scale));
  font-weight: var(--font-display-weight);
  color: var(--text-primary);
  margin-bottom: var(--space-xs);
}

.modal-sub {
  font-size: var(--text-sm);
  color: var(--text-muted);
  margin-bottom: var(--space-lg);
}

/* ─────────────────────────────────────────────────────────
   FAQ ACCORDION
   ───────────────────────────────────────────────────────── */

.faq-list {
  width: 100%;
  display: flex;
  flex-direction: column;
  gap: 2px;
  text-align: left;
}

.faq-item {
  border: 1px solid var(--canvas-border);
  border-radius: var(--radius-lg);
  overflow: hidden;
}

.faq-question {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--space-md) var(--space-lg);
  background: var(--canvas-white);
  font-size: var(--text-base);
  font-weight: 500;
  color: var(--text-primary);
  text-align: left;
  cursor: pointer;
  transition: background var(--transition-fast);
  gap: var(--space-md);
  border: none;
}

.faq-question:hover,
.faq-item.open .faq-question {
  background: var(--canvas-off-white);
}

.faq-chevron {
  position: relative;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: var(--canvas-light-gray);
  flex-shrink: 0;
  transition: background var(--transition-fast);
}

.faq-chevron::before,
.faq-chevron::after {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  background: var(--text-secondary);
  border-radius: 1px;
  transition: transform var(--transition-fast), background var(--transition-fast);
}

/* Vertical bar */
.faq-chevron::before {
  width: 1.5px;
  height: 8px;
  transform: translate(-50%, -50%);
}

/* Horizontal bar */
.faq-chevron::after {
  width: 8px;
  height: 1.5px;
  transform: translate(-50%, -50%);
}

.faq-item.open .faq-chevron {
  background: var(--brand-primary-light);
}

.faq-item.open .faq-chevron::before {
  transform: translate(-50%, -50%) rotate(45deg);
  background: var(--brand-primary);
}

.faq-item.open .faq-chevron::after {
  transform: translate(-50%, -50%) rotate(45deg);
  background: var(--brand-primary);
}

.faq-answer {
  overflow: hidden;
  height: 0;
}

.faq-link {
  color: var(--brand-primary);
  font-weight: 500;
  text-decoration: underline;
  text-underline-offset: 2px;
}

.faq-answer-inner {
  white-space: pre-line; /* an answer's own line breaks are paragraphs */
  padding: var(--space-md) var(--space-lg) var(--space-lg);
  background: var(--canvas-off-white);
  font-size: var(--text-base);
  font-weight: 300;
  color: var(--text-secondary);
  line-height: 1.7;
  text-align: left;
}

/* ─────────────────────────────────────────────────────────
   TESTIMONIAL CARDS
   ───────────────────────────────────────────────────────── */

.testimonial-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-md);
  width: 100%;
}

.testimonial-card {
  background: var(--canvas-off-white);
  border: 1px solid var(--canvas-border);
  border-radius: var(--radius-lg);
  padding: var(--space-lg);
  text-align: center;
  display: flex;
  flex-direction: column;
}

.testimonial-stars {
  display: flex;
  justify-content: center;
  gap: 2px;
  margin-bottom: var(--space-sm);
  color: var(--brand-accent);
  font-size: calc(14px * var(--font-body-scale));
}

.testimonial-quote {
  font-style: italic;
  font-size: var(--text-sm);
  color: var(--text-secondary);
  line-height: 1.7;
  margin-bottom: var(--space-md);
  flex: 1;
}

.testimonial-author {
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--text-primary);
}

.testimonial-location {
  font-size: var(--text-xs);
  color: var(--text-muted);
  margin-top: 2px;
}

.testimonial-google-badge {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  margin-top: var(--space-lg);
  font-size: var(--text-sm);
  color: var(--text-muted);
}

/* ─────────────────────────────────────────────────────────
   CHIP BADGES
   ───────────────────────────────────────────────────────── */

.chip-badge {
  display: inline-flex;
  align-items: center;
  padding: 4px 10px;
  background: rgba(255,255,255,0.88);
  backdrop-filter: blur(8px);
  border-radius: var(--radius-sm);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-primary);
  box-shadow: 0 1px 4px rgba(0,0,0,0.10);
}

/* ─────────────────────────────────────────────────────────
   DIVIDERS
   ───────────────────────────────────────────────────────── */

.divider {
  width: 100%;
  height: 1px;
  background: var(--canvas-border);
  margin: var(--space-xl) 0;
}

.divider-accent {
  width: 48px;
  height: 1.5px;
  background: var(--brand-accent);
  opacity: 0.6;
  margin: var(--space-md) 0;
}

/* ─────────────────────────────────────────────────────────
   PAGE SECTIONS (full-bleed below main content)
   ───────────────────────────────────────────────────────── */

.page-section {
  width: 100%;
  border-top: 1px solid var(--canvas-border);
  padding: var(--space-2xl) var(--space-xl);
}

/* Banding resolves itself from render order rather than being assigned
   per section. Sections are conditionally rendered — no testimonials
   configured, an out-of-area visitor — so a hand-set background is a
   guess about what precedes it, and guessing wrong puts two off-white
   bands back to back. nth-of-type counts only the sections that
   actually rendered, so the stripe re-derives whenever one drops out.
   Odd is off-white because the hero above is white.

   The band is a surface, not just a background. On a tenant that tints
   it (theme.band) the ground can be the brand colour itself, and the
   eyebrow, buttons and dots that are ALSO brand-coloured would vanish
   into it, while the text tokens assume a light ground. So the rule
   re-scopes every canvas, text and brand token to its --band-*
   counterpart. Untinted, those are the same tokens (see :root); tinted,
   they are the palette lib/theme.ts derived for that colour. Nothing
   inside the band has to know which it got.

   The report's bands are the same surface, so they take the same rule.
   The booklet landing's .bk-section.bg-off-white is deliberately NOT
   here: its contents are literal greys and whites from the reference it
   was ported from, which the token swap cannot reach, so it keeps the
   off-white stripe until it is tokenised. .bg-white is excluded rather
   than overridden after: an odd section forced white must not inherit
   a palette meant for colour. */
.page-section:nth-of-type(odd):not(.bg-white),
.page-section.bg-off-white,
.report-bands > section:nth-of-type(odd):not(.bg-white) {
  background: var(--band-bg);
  border-top-color: var(--band-edge);
  border-bottom-color: var(--band-edge);
  --brand-primary:       var(--band-primary);
  --brand-primary-hover: var(--band-primary-hover);
  --brand-primary-light: var(--band-primary-light);
  --brand-accent:        var(--band-accent);
  --canvas-white:        var(--band-surface);
  --canvas-off-white:    var(--band-surface-recessed);
  --canvas-light-gray:   var(--band-surface-hover);
  --canvas-border:       var(--band-border);
  --text-primary:        var(--band-text-primary);
  --text-secondary:      var(--band-text-secondary);
  --text-muted:          var(--band-text-muted);
  --text-inverse:        var(--band-text-inverse);
}

/* Escape hatch. Forces the white ground; the band rule above already
   skips it, so the tokens inside stay the page's. */
.page-section.bg-white {
  background: var(--canvas-white);
}

/* The other escape: one object that stays a white window inside a
   tinted band (the chatbot flip card). Every token the band rule swapped
   goes back to the page's copy, so its text, borders and buttons are
   the page's, not the band's inks. */
.band-escape,
.ask-chat,
.ask-hub {
  --brand-primary:       var(--page-primary);
  --brand-primary-hover: var(--page-primary-hover);
  --brand-primary-light: var(--page-primary-light);
  --brand-accent:        var(--page-accent);
  --canvas-white:        var(--page-white);
  --canvas-off-white:    var(--page-off-white);
  --canvas-light-gray:   var(--page-light-gray);
  --canvas-border:       var(--page-border);
  --text-primary:        var(--page-text-primary);
  --text-secondary:      var(--page-text-secondary);
  --text-muted:          var(--page-text-muted);
  --text-inverse:        var(--page-text-inverse);
}

.page-section-inner {
  max-width: var(--max-wide);
  margin: 0 auto;
  text-align: center;
}

.page-section-inner .eyebrow {
  justify-content: center;
}

/* A column of questions reads best narrower than the section's full
   width: at 960px each row is a long thin bar and the text sits far
   from the toggle. 720px is the AI tool's FAQ width. */
.page-section-inner.is-narrow {
  max-width: 720px;
}

/* ─────────────────────────────────────────────────────────
   STATUS BANNERS
   ───────────────────────────────────────────────────────── */

.status-banner {
  display: flex;
  align-items: flex-start;
  gap: var(--space-sm);
  padding: var(--space-md);
  border-radius: var(--radius-md);
  font-size: var(--text-sm);
  width: 100%;
}

.status-banner.error {
  background: var(--status-error-bg);
  color: var(--status-error);
}

.status-banner.warning {
  background: var(--status-warning-bg);
  color: var(--status-warning);
}

.status-banner.success {
  background: var(--status-success-bg);
  color: var(--status-success);
}

/* ─────────────────────────────────────────────────────────
   ADDRESS AUTOCOMPLETE DROPDOWN
   ───────────────────────────────────────────────────────── */

.autocomplete-wrapper {
  position: relative;
  width: 100%;
}

.autocomplete-dropdown {
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  right: 0;
  background: var(--canvas-white);
  border: 1px solid var(--canvas-border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
  z-index: 100;
  overflow: hidden;
}

.autocomplete-item {
  width: 100%;
  padding: 12px 14px;
  text-align: left;
  font-family: var(--font-body);
  font-size: var(--text-sm);
  color: var(--text-primary);
  background: none;
  border: none;
  cursor: pointer;
  transition: background var(--transition-fast);
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.autocomplete-item:hover,
.autocomplete-item.highlighted {
  background: var(--canvas-off-white);
}

.autocomplete-item-main {
  font-weight: 500;
}

.autocomplete-item-secondary {
  font-size: var(--text-xs);
  color: var(--text-muted);
}

/* ─────────────────────────────────────────────────────────
   RESULTS PAGE
   ───────────────────────────────────────────────────────── */

.results-price-range {
  width: 100%;
  margin-bottom: var(--space-md);
  text-align: center;
}

.results-price-display {
  font-family: var(--font-display);
  text-transform: var(--font-display-transform, none);
  letter-spacing: var(--font-display-tracking, normal);
  font-size: calc(var(--text-3xl) * var(--font-display-scale));
  font-weight: var(--font-display-weight);
  color: var(--text-primary);
  line-height: 1.1;
  margin-bottom: var(--space-sm);
}

.results-price-per {
  font-family: var(--font-body);
  font-size: var(--text-lg);
  font-weight: 300;
  color: var(--text-secondary);
  text-transform: none;
  letter-spacing: normal;
}

.results-price-note {
  font-size: var(--text-sm);
  color: var(--text-muted);
  font-weight: 300;
  max-width: 400px;
  margin: 0 auto;
  line-height: 1.6;
}

.why-range-toggle {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  font-size: var(--text-sm);
  color: var(--text-muted);
  font-weight: 400;
  background: none;
  border: none;
  cursor: pointer;
  padding: var(--space-sm) 0;
  transition: color var(--transition-fast);
  width: 100%;
  justify-content: center;
}

.why-range-toggle:hover {
  color: var(--text-secondary);
}

.why-range-panel {
  overflow: hidden;
  width: 100%;
}

.why-range-inner {
  padding: var(--space-md) var(--space-lg);
  background: var(--canvas-off-white);
  border: 1px solid var(--canvas-border);
  border-radius: var(--radius-lg);
  font-size: var(--text-sm);
  color: var(--text-secondary);
  font-weight: 300;
  line-height: 1.7;
  text-align: left;
  margin-top: var(--space-sm);
}

/* Estimate card */
.estimate-card {
  width: 100%;
  border: 1px solid var(--canvas-border);
  border-radius: var(--radius-xl);
  overflow: hidden;
  box-shadow: var(--shadow-lg);
}

.estimate-card-header {
  padding: var(--space-lg) var(--space-xl);
  border-bottom: 1px solid var(--canvas-border);
}

.estimate-card-pool-name {
  font-family: var(--font-display);
  text-transform: var(--font-display-transform, none);
  letter-spacing: var(--font-display-tracking, normal);
  font-size: calc(var(--text-xl) * var(--font-display-scale));
  font-weight: var(--font-display-weight);
  color: var(--text-primary);
}

.estimate-card-pool-dims {
  font-size: var(--text-sm);
  color: var(--text-muted);
  margin-top: 2px;
}

.estimate-features-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  /* The last row's bottom hairline sits under the total row's top one
     rather than beside it. */
  margin-bottom: -1px;
}

/* Dividers are borders on the cells, not a border-coloured background
   showing through 1px gaps. A gap lands on a half pixel whenever the
   column width is fractional, and at some zoom levels the column line
   simply vanished while the row line (whole-pixel rows) stayed. */
.estimate-feature-cell {
  padding: var(--space-md) var(--space-lg);
  background: var(--canvas-white);
  border-bottom: 1px solid var(--canvas-border);
}

.estimate-feature-cell:nth-child(odd) {
  border-right: 1px solid var(--canvas-border);
}

.estimate-feature-name {
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--text-primary);
}

.estimate-feature-desc {
  font-size: var(--text-xs);
  color: var(--text-muted);
  margin-top: 2px;
  font-weight: 300;
}

.estimate-card-total {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--space-lg) var(--space-md);
  border-top: 1px solid var(--canvas-border);
  background: var(--canvas-off-white);
  gap: var(--space-sm);
}

@media (max-width: 640px) {
  .estimate-card-total {
    flex-direction: column;
    align-items: center;
    gap: 4px;
  }
}

.estimate-card-total-label {
  font-size: var(--text-sm);
  color: var(--text-secondary);
  font-weight: 500;
}

.estimate-card-total-value {
  font-family: var(--font-display);
  text-transform: var(--font-display-transform, none);
  letter-spacing: var(--font-display-tracking, normal);
  font-size: calc(var(--text-xl) * var(--font-display-scale));
  font-weight: var(--font-display-weight);
  color: var(--text-primary);
  white-space: nowrap;
}

/* Outdoor living reuses .estimate-card and .estimate-features-grid whole,
   so the two cards are the same object in attached mode rather than two
   designs sharing a page. The only addition is the odd-count filler. */
.estimate-feature-cell-filler {
  /* The empty slot stays a visible block (the total row's off-white,
     one shade lighter than the dividers) so the column hairline shows
     between it and the cell beside it, the way the row hairlines do. */
  padding: 0;
  background: var(--canvas-off-white);
}

/* Consultation CTA */
.consultation-card {
  width: 100%;
  background: var(--brand-primary);
  border-radius: var(--radius-xl);
  padding: var(--space-xl);
  text-align: center;
  color: var(--text-inverse);
}

.consultation-card-headline {
  font-family: var(--font-display);
  text-transform: var(--font-display-transform, none);
  letter-spacing: var(--font-display-tracking, normal);
  font-size: calc(var(--text-xl) * var(--font-display-scale));
  font-weight: var(--font-display-weight);
  margin-bottom: var(--space-sm);
  color: var(--text-inverse);
}

.consultation-card-sub {
  font-size: var(--text-base);
  font-weight: 300;
  opacity: 0.85;
  line-height: 1.6;
  max-width: 440px;
  margin: 0 auto var(--space-lg);
}

.consultation-btn {
  /* --text-inverse rather than --canvas-white: white on the page, and the
     band colour inside a tinted band, where the card itself has become
     ink-coloured and a white-ish button with ink text would not read. */
  background: var(--text-inverse);
  color: var(--brand-primary);
  border: none;
  padding: 14px 28px;
  border-radius: var(--radius-md);
  font-family: var(--font-body);
  font-size: var(--text-base);
  font-weight: 600;
  cursor: pointer;
  transition: opacity var(--transition-fast), transform var(--transition-fast);
}

.consultation-btn:hover {
  opacity: 0.9;
  transform: translateY(-1px);
}

.consultation-picker {
  margin-top: var(--space-lg);
  padding: var(--space-lg);
  background: rgba(255,255,255,0.12);
  border-radius: var(--radius-lg);
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
  text-align: left;
}

.consultation-picker select {
  width: 100%;
  padding: 11px 14px;
  font-family: var(--font-body);
  font-size: var(--text-base);
  color: var(--text-primary);
  background: var(--canvas-white);
  border: 1px solid var(--canvas-border);
  border-radius: var(--radius-md);
  outline: none;
  appearance: none;
  cursor: pointer;
}

.consultation-picker label {
  display: block;
  font-size: var(--text-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: rgba(255,255,255,0.7);
  margin-bottom: var(--space-xs);
}

.consultation-confirmed {
  background: var(--status-success-bg);
  border: 1px solid var(--status-success);
  border-radius: var(--radius-lg);
  padding: var(--space-lg) var(--space-xl);
  text-align: center;
  width: 100%;
}

.consultation-confirmed-headline {
  font-family: var(--font-display);
  text-transform: var(--font-display-transform, none);
  letter-spacing: var(--font-display-tracking, normal);
  font-size: calc(var(--text-xl) * var(--font-display-scale));
  font-weight: var(--font-display-weight);
  color: var(--status-success);
  margin-bottom: var(--space-sm);
}

.consultation-confirmed-sub {
  font-size: var(--text-base);
  color: var(--status-success);
  font-weight: 300;
  line-height: 1.6;
}

/* Financing calculator */
.financing-card {
  width: 100%;
  padding: var(--space-xl);
  background: var(--canvas-off-white);
  border: 1px solid var(--canvas-border);
  border-radius: var(--radius-xl);
  text-align: center;
}

.financing-monthly-range {
  font-family: var(--font-display);
  text-transform: var(--font-display-transform, none);
  letter-spacing: var(--font-display-tracking, normal);
  font-size: calc(var(--text-2xl) * var(--font-display-scale));
  font-weight: var(--font-display-weight);
  color: var(--text-primary);
  margin-bottom: var(--space-xs);
  line-height: 1.1;
}

.financing-range-label {
  font-size: var(--text-sm);
  color: var(--text-secondary);
  font-weight: 300;
  margin-bottom: var(--space-xl);
}

.financing-sliders {
  display: flex;
  flex-direction: column;
  gap: var(--space-lg);
  margin-bottom: var(--space-lg);
  text-align: left;
}

.financing-slider-row {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
}

.financing-slider-header {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  justify-content: space-between;
}

.financing-slider-label {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: var(--text-sm);
  color: var(--text-secondary);
  font-weight: 400;
}

.financing-slider-label svg {
  color: var(--text-muted);
  flex-shrink: 0;
}

.financing-slider-value {
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--text-primary);
  min-width: 56px;
  text-align: right;
}

.financing-slider {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: 20px;
  background: transparent;
  outline: none;
  cursor: pointer;
  padding: 0;
  margin: 0;
}

.financing-slider::-webkit-slider-runnable-track {
  height: 5px;
  border-radius: 2.5px;
  /* --fill is set as an inline style on the <input>; custom props cascade into pseudo-elements */
  background: linear-gradient(
    to right,
    var(--brand-primary) var(--fill, 0%),
    var(--canvas-border) var(--fill, 0%)
  );
}

.financing-slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: var(--brand-primary);
  cursor: pointer;
  /* Center on 5px track: -(20 - 5) / 2 */
  margin-top: -7.5px;
  box-shadow: 0 1px 4px rgba(0,0,0,0.18);
  transition: transform 0.1s ease;
}

.financing-slider::-webkit-slider-thumb:hover {
  transform: scale(1.15);
}

.financing-slider::-moz-range-track {
  height: 5px;
  border-radius: 2.5px;
  background: var(--canvas-border);
}

.financing-slider::-moz-range-progress {
  height: 5px;
  border-radius: 2.5px;
  background: var(--brand-primary);
}

.financing-slider::-moz-range-thumb {
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: var(--brand-primary);
  cursor: pointer;
  border: none;
  box-shadow: 0 1px 4px rgba(0,0,0,0.18);
}

.financing-instruction {
  font-size: var(--text-sm);
  color: var(--text-secondary);
  margin-bottom: var(--space-md);
  line-height: 1.5;
}

.financing-disclaimer {
  font-size: var(--text-xs);
  color: var(--text-muted);
  line-height: 1.6;
  max-width: 480px;
  margin-left: auto;
  margin-right: auto;
}

/* Legacy — kept for any remaining references */
.financing-amount {
  font-family: var(--font-display);
  text-transform: var(--font-display-transform, none);
  letter-spacing: var(--font-display-tracking, normal);
  font-size: calc(var(--text-2xl) * var(--font-display-scale));
  font-weight: var(--font-display-weight);
  color: var(--brand-primary);
  margin-bottom: var(--space-xs);
}

.financing-period {
  font-size: var(--text-base);
  color: var(--text-secondary);
  font-weight: 300;
}

.financing-note {
  font-size: var(--text-xs);
  color: var(--text-muted);
  margin-top: var(--space-md);
  line-height: 1.6;
  max-width: 480px;
  margin-left: auto;
  margin-right: auto;
}

/* What happens next — alternating timeline */
.nst-timeline {
  max-width: 680px;
  margin: 0 auto;
}

.nst-item {
  display: grid;
  grid-template-columns: 1fr 48px 1fr;
  gap: var(--space-xl);
  align-items: center; /* dot centered to full content block */
  /* opacity set by gsap.set() — not CSS — so items are visible if JS fails */
}

.nst-connector {
  display: flex;
  justify-content: center;
  height: 56px;
}

.nst-segment {
  width: 2px;
  height: 100%;
  background: var(--canvas-border);
  transform: scaleY(0);
  transform-origin: top center;
}

.nst-dot {
  width: 48px;
  height: 48px;
  border-radius: 50%;
  background: var(--brand-primary);
  /* The token, not #fff: inside a tinted band --brand-primary becomes the
     ink and --text-inverse the band colour, and a literal white number
     would sit on a white dot. */
  color: var(--text-inverse);
  font-size: var(--text-base);
  font-weight: 600;
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
  z-index: 2;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.12);
  flex-shrink: 0;
}

.nst-spacer {
  /* empty column placeholder */
}

.nst-content {
  /* Prevent grid item from expanding beyond its 1fr allocation */
  min-width: 0;
  overflow-wrap: break-word;
}

.nst-content h4 {
  font-size: var(--text-base);
  font-weight: 600;
  color: var(--text-primary);
  margin-bottom: 4px;
}

.nst-content p {
  font-size: var(--text-sm);
  color: var(--text-secondary);
  font-weight: 300;
  line-height: 1.6;
}

.nst-content-left {
  text-align: right;
}

.nst-content-right {
  text-align: left;
}

/* Mobile: keep center alternating layout, scale down */
@media (max-width: 600px) {
  .nst-item {
    grid-template-columns: 1fr 32px 1fr;
    gap: 10px;
  }

  .nst-connector {
    height: 32px;
  }

  .nst-dot {
    width: 32px;
    height: 32px;
    font-size: calc(12px * var(--font-body-scale));
    box-shadow: 0 1px 6px rgba(0, 0, 0, 0.1);
  }

  .nst-content h4 {
    font-size: calc(13px * var(--font-body-scale));
    margin-bottom: 2px;
  }

  .nst-content p {
    font-size: calc(12px * var(--font-body-scale));
    line-height: 1.5;
  }
}

/* ─────────────────────────────────────────────────────────
   TIME SLOT CARDS (booking step)
   ───────────────────────────────────────────────────────── */

.time-slot-card {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  padding: var(--space-md);
  background: var(--canvas-white);
  border: 1px solid var(--canvas-border);
  border-radius: var(--radius-lg);
  cursor: pointer;
  text-align: left;
  transition: border-color 0.15s ease, background 0.15s ease;
  width: 100%;
}

.time-slot-card:hover {
  border-color: var(--brand-primary);
}

.time-slot-card.selected {
  border-color: var(--brand-primary);
  background: var(--brand-primary-light);
}

.time-slot-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  background: var(--canvas-off-white);
  border-radius: var(--radius-md);
  color: var(--text-secondary);
  flex-shrink: 0;
}

.time-slot-card.selected .time-slot-icon {
  background: rgba(255, 255, 255, 0.6);
  color: var(--brand-primary);
}

.time-slot-name {
  font-size: var(--text-base);
  font-weight: 600;
  color: var(--text-primary);
  margin-bottom: 2px;
}

.time-slot-desc {
  font-size: var(--text-sm);
  color: var(--text-secondary);
  font-weight: 300;
}

/* ─────────────────────────────────────────────────────────
   DAY PICKER (react-day-picker v9 custom styles)
   ───────────────────────────────────────────────────────── */

.rdp-root {
  --rdp-accent-color: var(--brand-primary);
  --rdp-accent-color-dark: var(--brand-primary-hover);
  --rdp-background-color: var(--brand-primary-light);
}

/* Force brand color — library's :root blue overrides our variable via load order */
.rdp-selected .rdp-day_button {
  border-color: var(--brand-primary) !important;
  color: var(--brand-primary) !important;
}

.rdp-selected:not([aria-disabled]) .rdp-day_button {
  background-color: var(--brand-primary) !important;
  color: #fff !important;
  border-color: var(--brand-primary) !important;
}

.rdp-today:not(.rdp-selected) .rdp-day_button {
  color: var(--brand-primary) !important;
  font-weight: 700;
}

.rdp-chevron {
  fill: var(--brand-primary) !important;
}

.rdp-day_button:focus-visible {
  outline-color: var(--brand-primary) !important;
}

.rdp-day_button {
  font-family: var(--font-body);
}

/* ─────────────────────────────────────────────────────────
   DAY + TIME PICKER (real times from the tenant's calendar)

   components/booking/DayTimePicker.tsx, slots mode on every booking
   surface (request mode keeps ConsultationPicker). Typographic, no card:
   a "Pick a day" eyebrow, seven days with round arrows at the ends and a
   brand bar gliding under the day picked; the day's every time as a
   tile, taken ones dashed and struck through; the pick as a sentence in
   the display face. Both rows are native radios under a label: the input
   is kept in the tab order but drawn by nothing, and the label is the
   control the eye sees.
   ───────────────────────────────────────────────────────── */

.pick {
  width: 100%;
  margin-bottom: var(--space-lg);
  text-align: center;
}

.pick-notice { margin-bottom: var(--space-md); text-align: left; }

/* The funnel eyebrow, with a short line on both sides. */
.pick-eyebrow { margin: 0 0 var(--space-md); }

.pick-eyebrow::after {
  content: '';
  display: block;
  width: 24px;
  height: 1px;
  background: var(--brand-accent);
  flex-shrink: 0;
}

.pick-days {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
}

.pick-nav,
.pick-nav-space {
  flex: none;
  width: calc(var(--space-lg) + var(--space-sm) + 2px);
  height: calc(var(--space-lg) + var(--space-sm) + 2px);
}

.pick-nav {
  display: grid;
  place-items: center;
  padding: 0;
  border-radius: 999px;
  border: 1px solid var(--canvas-border);
  background: var(--canvas-white);
  color: var(--text-primary);
  cursor: pointer;
  transition: border-color var(--transition-fast), color var(--transition-fast), opacity var(--transition-fast);
}

.pick-nav:hover:not(:disabled) {
  border-color: var(--brand-primary);
  color: var(--brand-primary);
}

.pick-nav:disabled {
  opacity: 0.3;
  cursor: default;
}

.pick-nav:focus-visible {
  outline: 2px solid var(--brand-primary);
  outline-offset: 2px;
}

.pick-dayrow {
  position: relative;
  flex: 1;
  min-width: 0;
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  padding-bottom: calc(var(--space-sm) + 2px);
}

/* The cell only pairs an input with its label; the label is the grid or flex item. */
.pick-cell { display: contents; }

/* The input keeps focus and the arrow keys; the label is what shows. */
.pick-radio {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

.pick-day {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: calc(var(--space-xs) / 2);
  padding: calc(var(--space-xs) + 2px) 0 var(--space-xs);
  border-radius: var(--radius-md);
  color: var(--text-primary);
  cursor: pointer;
  transition: color 200ms ease, opacity 200ms ease, transform 200ms ease;
}

.pick-dow {
  font-family: var(--font-body);
  font-size: calc(var(--text-xs) * 0.9);
  font-weight: 600;
  line-height: 1;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-secondary);
  transition: color 200ms ease;
}

.pick-num {
  font-family: var(--font-display);
  text-transform: var(--font-display-transform, none);
  letter-spacing: var(--font-display-tracking, normal);
  font-weight: var(--font-display-weight);
  font-size: calc(var(--text-xl) * 1.2 * var(--font-display-scale));
  line-height: 1.05;
  font-variant-numeric: lining-nums;
  transition: color 200ms ease;
}

/* The month under the page's first day and under the 1st; the space stays either way. */
.pick-mon {
  min-height: 1em;
  font-family: var(--font-body);
  font-size: calc(var(--text-xs) * 0.9);
  font-weight: 500;
  line-height: 1;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-secondary);
}

.pick-radio:not(:disabled):not(:checked) + .pick-day:hover { transform: translateY(-2px); }

.pick-radio:checked + .pick-day .pick-dow,
.pick-radio:checked + .pick-day .pick-num { color: var(--brand-primary); }

.pick-radio:disabled + .pick-day {
  opacity: 0.22;
  cursor: default;
}

.pick-radio:focus-visible + .pick-day,
.pick-radio:focus-visible + .pick-tile {
  outline: 2px solid var(--brand-primary);
  outline-offset: 2px;
}

/* One column wide, moved a whole column per day: the transform glides it. */
.pick-bar {
  position: absolute;
  left: 0;
  bottom: 0;
  width: calc(100% / 7);
  height: calc(var(--space-xs) - 1px);
  transform: translateX(calc(var(--pick-i, 0) * 100%));
  transition: transform 450ms cubic-bezier(0.2, 0.8, 0.2, 1);
  pointer-events: none;
}

.pick-bar::after {
  content: '';
  display: block;
  width: calc(var(--space-lg) + var(--space-xs));
  height: 100%;
  margin: 0 auto;
  border-radius: 999px;
  background: var(--brand-primary);
}

/* Equal tiles, four to a row (three on a phone), a short last row centred. */
.pick-times {
  --pick-gap: calc(var(--space-sm) + 2px);
  --pick-cols: 4;
  position: relative;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--pick-gap);
  margin-top: var(--space-xl);
}

.pick-tile,
.pick-tile-skeleton {
  flex: 0 0 calc((100% - (var(--pick-cols) - 1) * var(--pick-gap)) / var(--pick-cols));
  border-radius: calc(var(--radius-md) + 2px);
}

.pick-tile {
  display: flex;
  align-items: baseline;
  justify-content: center;
  gap: calc(var(--space-xs) + 1px);
  padding: calc(var(--space-md) - 2px) calc(var(--space-sm) - 2px) calc(var(--space-md) - 3px);
  border: 1px solid var(--canvas-border);
  background: var(--canvas-white);
  color: var(--text-primary);
  cursor: pointer;
  transition: border-color var(--transition-fast), background var(--transition-fast), color var(--transition-fast), transform var(--transition-fast);
  animation: pick-in 350ms cubic-bezier(0.2, 0.8, 0.2, 1) both;
  animation-delay: calc(var(--pick-n, 0) * 30ms);
}

.pick-time {
  font-family: var(--font-display);
  text-transform: var(--font-display-transform, none);
  letter-spacing: var(--font-display-tracking, normal);
  font-weight: var(--font-display-weight);
  font-size: calc(var(--text-lg) * 0.95 * var(--font-display-scale));
  line-height: 1;
  font-variant-numeric: lining-nums tabular-nums;
}

.pick-ampm {
  font-family: var(--font-body);
  font-size: calc(var(--text-xs) * 0.86);
  font-weight: 600;
  line-height: 1;
  letter-spacing: 0.12em;
  color: var(--text-secondary);
}

.pick-radio:not(:disabled):not(:checked) + .pick-tile:hover {
  border-color: var(--brand-primary);
  transform: translateY(-1px);
}

.pick-radio:checked + .pick-tile {
  border-color: var(--brand-primary);
  background: color-mix(in srgb, var(--brand-primary) 7%, var(--canvas-white));
  color: var(--brand-primary);
  box-shadow: inset 0 0 0 1px var(--brand-primary);
}

.pick-radio:checked + .pick-tile .pick-ampm { color: var(--brand-primary); }

.pick-tile.is-taken {
  border-style: dashed;
  background: transparent;
  color: var(--text-secondary);
  cursor: default;
}

.pick-tile.is-taken .pick-time,
.pick-tile.is-taken .pick-ampm { opacity: 0.45; }

.pick-tile.is-taken .pick-time {
  text-decoration: line-through;
  text-decoration-thickness: 1px;
}

@keyframes pick-in {
  from { opacity: 0; transform: translateY(var(--space-xs)); }
  to   { opacity: 1; transform: none; }
}

.pick-empty { margin-top: var(--space-lg); }

.pick-empty-text {
  margin: var(--space-lg) 0 var(--space-sm);
  font-size: var(--text-sm);
  color: var(--text-secondary);
  text-align: center;
}

/* The pick as a sentence, the zone under it; the space is kept so nothing jumps. */
.pick-summary {
  min-height: calc(var(--space-xl) + var(--space-xs));
  margin: var(--space-xl) 0 0;
  font-family: var(--font-display);
  text-transform: var(--font-display-transform, none);
  letter-spacing: var(--font-display-tracking, normal);
  font-weight: var(--font-display-weight);
  font-size: calc(var(--text-lg) * 1.05 * var(--font-display-scale));
  line-height: 1.3;
  color: var(--text-primary);
  text-align: center;
  opacity: 0;
  transform: translateY(var(--space-xs));
  transition: opacity 300ms ease, transform 300ms ease;
}

.pick-summary.is-on {
  opacity: 1;
  transform: none;
}

.pick-summary-dot {
  padding: 0 var(--space-xs);
  color: var(--brand-primary);
}

.pick-summary-tz {
  display: block;
  margin-top: var(--space-xs);
  font-family: var(--font-body);
  font-size: var(--text-xs);
  font-weight: 500;
  line-height: 1;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-secondary);
}

/* Skeleton: the same shapes, shimmering. .skeleton supplies the motion. */
.pick-day-skeleton {
  height: calc(var(--space-2xl) - var(--space-sm));
  margin: 0 calc(var(--space-xs) / 2);
  border-radius: var(--radius-md);
}

.pick-tile-skeleton { min-height: calc(var(--space-xl) + var(--space-sm)); }

@media (max-width: 640px) {
  .pick-summary { margin-top: var(--space-lg); }
}

@media (max-width: 560px) {
  .pick-num { font-size: calc(var(--text-xl) * var(--font-display-scale)); }
  .pick-times { --pick-cols: 3; }
  .pick-time { font-size: calc(var(--text-md) * 1.06 * var(--font-display-scale)); }
}

@media (prefers-reduced-motion: reduce) {
  .pick-nav,
  .pick-day,
  .pick-bar,
  .pick-tile,
  .pick-summary {
    transition: none;
    animation: none;
  }
}

/* ─────────────────────────────────────────────────────────
   LIVE PRICE BADGE
   ───────────────────────────────────────────────────────── */

.price-badge {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  padding: 6px 14px;
  background: var(--canvas-off-white);
  border: 1px solid var(--canvas-border);
  border-radius: var(--radius-sm);
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--brand-primary);
}

.price-badge-label {
  font-weight: 400;
  color: var(--text-muted);
}

/* ─────────────────────────────────────────────────────────
   ADMIN PAGE
   ───────────────────────────────────────────────────────── */

.admin-wrapper {
  min-height: 100vh;
  background: var(--canvas-off-white);
}

.admin-header {
  background: var(--canvas-white);
  border-bottom: 1px solid var(--canvas-border);
  padding: var(--space-lg) var(--space-xl);
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.admin-header-title {
  font-family: var(--font-display);
  text-transform: var(--font-display-transform, none);
  letter-spacing: var(--font-display-tracking, normal);
  font-size: calc(var(--text-xl) * var(--font-display-scale));
  font-weight: var(--font-display-weight);
  color: var(--text-primary);
}

.admin-header-tenant {
  font-size: var(--text-sm);
  color: var(--text-muted);
  display: flex;
  align-items: center;
  gap: var(--space-xs);
}

/* ── Admin tabs ──────────────────────────────────────────────
   Two groups on one rail: what every funnel shares, then one tab per
   vertical. Sticky, because the catalog and outdoor panels are long
   enough that you lose track of which screen you are on. */

/* One row, one height, one baseline. Every tab is a single line of text,
   so the group labels and the two groups' underlines all line up
   regardless of how many funnels are folded in. */
.admin-tabs {
  position: sticky;
  top: 0;
  z-index: 20;
  background: var(--canvas-white);
  border-bottom: 1px solid var(--canvas-border);
  padding: 0 var(--space-xl);
  display: flex;
  align-items: stretch;
  gap: var(--space-lg);
  overflow-x: auto;
  scrollbar-width: thin;
}

.admin-tabs-group {
  display: flex;
  align-items: stretch;
  gap: 2px;
  flex-shrink: 0;
}

.admin-tabs-group + .admin-tabs-group {
  padding-left: var(--space-lg);
  border-left: 1px solid var(--canvas-border);
}

.admin-tabs-group-label {
  display: flex;
  align-items: center;
  font-size: calc(10px * var(--font-body-scale));
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--text-muted);
  padding-right: var(--space-sm);
  white-space: nowrap;
}

.admin-tab {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  background: none;
  border: none;
  /* Matching transparent top border so the active tab's 2px underline
     does not shift the label up by two pixels. */
  border-top: 2px solid transparent;
  border-bottom: 2px solid transparent;
  padding: 0 var(--space-md);
  min-height: 52px;
  cursor: pointer;
  color: var(--text-secondary);
  font-size: var(--text-sm);
  font-weight: 500;
  line-height: 1;
  white-space: nowrap;
  transition: color 0.15s ease, background 0.15s ease, border-color 0.15s ease;
}

.admin-tab:hover {
  background: var(--canvas-off-white);
  color: var(--text-primary);
}

.admin-tab:focus-visible {
  outline: 2px solid var(--brand-primary);
  outline-offset: -2px;
}

.admin-tab.is-active {
  color: var(--brand-primary);
  border-bottom-color: var(--brand-primary);
  font-weight: 600;
}

/* On/off at a glance: a tenant with three funnels dark and one lit should
   not have to open each tab to find that out. */
.admin-tab-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--canvas-border);
  flex-shrink: 0;
}

.admin-tab-dot.is-live {
  background: #16a34a;
  box-shadow: 0 0 0 2px rgba(22, 163, 74, 0.18);
}

.admin-tab-badge {
  min-width: 17px;
  height: 17px;
  padding: 0 5px;
  border-radius: 9px;
  background: #dc2626;
  color: #fff;
  font-size: calc(10px * var(--font-body-scale));
  font-weight: 700;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

.admin-tab-panel {
  display: flex;
  flex-direction: column;
  gap: var(--space-lg);
}

.admin-tab-panel[hidden] {
  display: none;
}

.admin-panel-head {
  padding-bottom: var(--space-xs);
}

.admin-panel-title {
  font-family: var(--font-display);
  text-transform: var(--font-display-transform, none);
  letter-spacing: var(--font-display-tracking, normal);
  font-size: calc(var(--text-lg) * var(--font-display-scale));
  font-weight: var(--font-display-weight);
  color: var(--text-primary);
  margin: 0;
}

.admin-panel-description {
  font-size: var(--text-sm);
  color: var(--text-muted);
  margin: 4px 0 0;
  line-height: 1.5;
}

/* Jump from a validation error to the tab that is hiding the field. */
.admin-error-jump {
  margin-left: var(--space-xs);
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  font: inherit;
  font-weight: 600;
  text-decoration: underline;
  color: inherit;
}

@media (max-width: 768px) {
  /* The header was laid out for a desktop-only tool and overflowed the
     viewport on a phone. Let it wrap now that the tabs below it are the
     thing you actually navigate with. */
  .admin-header {
    flex-wrap: wrap;
    gap: var(--space-md);
    padding: var(--space-md);
  }

  .admin-header-tenant {
    flex-wrap: wrap;
  }

  .admin-tabs {
    padding: 0 var(--space-md);
    gap: var(--space-md);
  }

  .admin-tab {
    min-height: 46px;
    padding: 0 var(--space-sm);
  }

  .admin-tabs-group-label {
    display: none;
  }

  .admin-tabs-group + .admin-tabs-group {
    padding-left: var(--space-md);
  }
}

.admin-body {
  max-width: 820px;
  margin: 0 auto;
  padding: var(--space-xl);
  display: flex;
  flex-direction: column;
  gap: var(--space-lg);
}

.admin-card {
  background: var(--canvas-white);
  border: 1px solid var(--canvas-border);
  border-radius: var(--radius-xl);
  padding: var(--space-xl);
}

.admin-card-title {
  font-size: var(--text-md);
  font-weight: 600;
  color: var(--text-primary);
  margin-bottom: var(--space-lg);
  padding-bottom: var(--space-md);
  border-bottom: 1px solid var(--canvas-border);
}

.admin-field-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-md);
}

.admin-field-action {
  margin-top: var(--space-xs);
}

.admin-field-full {
  grid-column: 1 / -1;
}

.admin-color-preview {
  width: 32px;
  height: 32px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--canvas-border);
  flex-shrink: 0;
}

.admin-toggle {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  cursor: pointer;
}

.admin-toggle input[type="checkbox"] {
  width: 18px;
  height: 18px;
  accent-color: var(--brand-primary);
  cursor: pointer;
}

.admin-footer {
  position: sticky;
  bottom: 0;
  background: var(--canvas-white);
  border-top: 1px solid var(--canvas-border);
  padding: var(--space-md) var(--space-xl);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
}

.admin-copy-success {
  font-size: var(--text-sm);
  color: var(--status-success);
  font-weight: 500;
  display: flex;
  align-items: center;
  gap: var(--space-xs);
}

.admin-section-toggle {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  background: none;
  border: none;
  border-top: 1px solid var(--canvas-border);
  padding: var(--space-md) 0;
  margin-top: var(--space-md);
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--text-secondary);
  cursor: pointer;
  text-align: left;
}

.admin-section-toggle:hover {
  color: var(--text-primary);
}

.admin-catalog-row {
  display: grid;
  grid-template-columns: 1fr auto auto;
  align-items: center;
  gap: var(--space-md);
  padding: var(--space-sm) 0;
  border-bottom: 1px solid var(--canvas-border);
}

.admin-catalog-row:last-child {
  border-bottom: none;
}

.admin-catalog-name {
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--text-primary);
}

/* Manufacturer heading inside the fiberglass list. Quieter than the section
   label above it — it groups rows, it does not introduce a new section. */
.admin-catalog-group {
  font-size: var(--text-xs);
  font-weight: 600;
  color: var(--text-secondary);
  padding-bottom: 4px;
  margin-bottom: 4px;
  border-bottom: 1px solid var(--canvas-border);
}

.admin-catalog-sub {
  font-size: var(--text-xs);
  color: var(--text-muted);
}

.admin-price-input {
  width: 120px;
  padding: 8px 10px;
  font-family: var(--font-body);
  font-size: var(--text-sm);
  border: 1px solid var(--canvas-border);
  border-radius: var(--radius-md);
  outline: none;
  text-align: right;
}

.admin-price-input:focus {
  border-color: var(--brand-primary);
}

/* ── Landing showcase: the pricing fields, then the live preview ──
   One column: the admin's content column is too narrow to hold the
   phone-width preview beside the field grid. */
.admin-showcase-split {
  display: flex;
  flex-direction: column;
  gap: var(--space-xl);
}

/* The flip card's back at the width the landing draws it, centred; a
   flat frame here, since the admin card already carries the shadow. */
.admin-showcase-phone {
  box-shadow: none;
  max-width: 380px;
  margin: 0 auto;
}

/* ── Ordered lists of samples: the grid's order, and the library ── */
.admin-grid-list {
  list-style: none;
  margin: 0;
  padding: 0;
  border: 1px solid var(--canvas-border);
  border-radius: var(--radius-lg);
  overflow: hidden;
}

.admin-grid-row {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  padding: var(--space-sm) var(--space-md);
  background: var(--canvas-white);
}

.admin-grid-row + .admin-grid-row { border-top: 1px solid var(--canvas-border); }

.admin-grid-row.is-pick { background: var(--canvas-off-white); }
.admin-grid-row.is-over { opacity: 0.55; }

.admin-grid-thumb {
  flex: 0 0 64px;
  width: 64px;
  height: 44px;
  object-fit: cover;
  border-radius: var(--radius-sm);
  background: var(--canvas-light-gray);
}

.admin-grid-label {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
  font-size: var(--text-sm);
  color: var(--text-primary);
}

.admin-grid-note {
  font-size: var(--text-xs);
  color: var(--text-muted);
}

.admin-grid-tools {
  display: flex;
  gap: var(--space-sm);
  flex-shrink: 0;
}

.admin-grid-tools .admin-sample-tool:disabled {
  opacity: 0.35;
  cursor: default;
  text-decoration: none;
}

/* ── Ad creative: the canvas preview ── */
.admin-ad-preview {
  max-width: 420px;
  border-radius: var(--radius-md);
  overflow: hidden;
  border: 1px solid var(--canvas-border);
  background: var(--canvas-light-gray);
}

.admin-ad-preview.is-tall { max-width: 300px; }

.admin-ad-preview canvas {
  display: block;
  width: 100%;
  height: auto;
}

.admin-ad-preview[aria-busy="true"] canvas { opacity: 0.6; }

.admin-ad-pan {
  width: 100%;
  accent-color: var(--brand-primary);
}

/* ── Landing showcase: the sample builds ──
   A finite row of small sliders the admin swipes through and picks
   from. Plain scroll-snap, no script: it is a list, not a carousel. The
   negative margin lets the picked card's ring and the row's shadow
   breathe past the card's padding without clipping. */
.admin-sample-row {
  display: flex;
  gap: var(--space-md);
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  -webkit-overflow-scrolling: touch;
  padding: var(--space-xs);
  margin: 0 calc(-1 * var(--space-xs));
  scrollbar-width: thin;
}

.admin-sample {
  flex: 0 0 260px;
  scroll-snap-align: start;
  border-radius: var(--radius-lg);
  padding: var(--space-xs);
  cursor: pointer;
  transition: box-shadow 0.15s;
}

.admin-sample:hover {
  box-shadow: 0 0 0 1px var(--canvas-border);
}

.admin-sample.is-picked {
  box-shadow: 0 0 0 2px var(--brand-primary);
}

/* The slider at card size: the landing's 4:3 frame, a lighter shadow,
   a handle a thumb can still find. */
.ba-container.is-sample {
  aspect-ratio: 4 / 3;
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-sm);
}

.ba-container.is-sample .ba-handle {
  width: 28px;
  height: 28px;
}

.ba-container.is-sample .ba-label {
  padding: 2px 6px;
  font-size: calc(9px * var(--font-body-scale));
}

.admin-sample-pick {
  display: block;
  width: 100%;
  padding: var(--space-sm) 2px 0;
  border: 0;
  background: none;
  text-align: left;
  font: inherit;
  cursor: pointer;
  border-radius: var(--radius-sm);
}

.admin-sample-pick:focus-visible {
  outline: 2px solid var(--brand-primary);
  outline-offset: 2px;
}

.admin-sample-label {
  display: block;
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--text-primary);
  line-height: 1.3;
}

.admin-sample-region {
  display: block;
  margin-top: 2px;
  font-size: var(--text-xs);
  color: var(--text-muted);
}

.admin-sample-manage {
  display: flex;
  gap: var(--space-sm);
  padding: var(--space-xs) 2px 0;
}

.admin-sample-tool {
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  font-size: var(--text-xs);
  color: var(--text-muted);
  text-decoration: underline;
  cursor: pointer;
}

.admin-sample-tool:hover {
  color: var(--text-primary);
}

.admin-sample-admin {
  margin-top: var(--space-md);
}

.admin-sample-form {
  padding: var(--space-md);
  border: 1px solid var(--canvas-border);
  border-radius: var(--radius-lg);
  background: var(--canvas-off-white);
}

.admin-sample-image {
  display: flex;
  gap: var(--space-sm);
  align-items: flex-start;
}

.admin-sample-thumb {
  flex: 0 0 72px;
  width: 72px;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  border-radius: var(--radius-sm);
  background: var(--canvas-light-gray);
}

/* ─────────────────────────────────────────────────────────
   ASSET UPLOADER
   ───────────────────────────────────────────────────────── */

.asset-dropzone {
  border: 2px dashed var(--canvas-border);
  border-radius: var(--radius-lg);
  padding: var(--space-xl);
  text-align: center;
  cursor: pointer;
  transition: border-color 0.15s, background 0.15s;
  background: var(--canvas-off-white);
  margin-bottom: var(--space-md);
}

.asset-dropzone:hover,
.asset-dropzone--active {
  border-color: var(--brand-primary);
  background: var(--primary-light, #eaf3fb);
}

.asset-dropzone--uploading {
  cursor: default;
  opacity: 0.75;
}

.asset-dropzone-inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-xs);
  color: var(--text-secondary);
  font-size: var(--text-sm);
}

.asset-dropzone-hint {
  font-size: var(--text-xs);
  color: var(--text-muted);
}

.asset-spinner {
  width: 20px;
  height: 20px;
  border: 2px solid var(--canvas-border);
  border-top-color: var(--brand-primary);
  border-radius: 50%;
  animation: asset-spin 0.7s linear infinite;
}

@keyframes asset-spin {
  to { transform: rotate(360deg); }
}

.asset-error {
  font-size: var(--text-sm);
  color: var(--status-error, #c0392b);
  margin-bottom: var(--space-md);
  padding: var(--space-sm) var(--space-md);
  background: #fdf2f2;
  border-radius: var(--radius-md);
  border: 1px solid #f5c6c6;
}

.asset-list {
  display: flex;
  flex-direction: column;
  gap: 0;
}

.asset-list-empty {
  font-size: var(--text-sm);
  color: var(--text-muted);
  padding: var(--space-md) 0;
}

.asset-row {
  display: grid;
  grid-template-columns: 56px 1fr auto;
  align-items: center;
  gap: var(--space-md);
  padding: var(--space-sm) 0;
  border-bottom: 1px solid var(--canvas-border);
}

.asset-row:last-child {
  border-bottom: none;
}

.asset-thumb {
  width: 56px;
  height: 40px;
  object-fit: contain;
  border-radius: var(--radius-sm);
  border: 1px solid var(--canvas-border);
  background: var(--canvas-off-white);
  flex-shrink: 0;
}

.asset-info {
  min-width: 0;
}

.asset-filename {
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--text-primary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.asset-url {
  font-size: var(--text-xs);
  color: var(--text-muted);
  font-family: monospace;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  margin-top: 2px;
}

.asset-meta {
  font-size: var(--text-xs);
  color: var(--text-muted);
  margin-top: 2px;
}

.asset-actions {
  display: flex;
  gap: var(--space-xs);
  flex-shrink: 0;
}

.asset-btn {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 5px 10px;
  font-family: var(--font-body);
  font-size: var(--text-xs);
  font-weight: 500;
  border: 1px solid var(--canvas-border);
  border-radius: var(--radius-md);
  background: var(--canvas-white);
  color: var(--text-secondary);
  cursor: pointer;
  white-space: nowrap;
  transition: background 0.1s, color 0.1s;
}

.asset-btn:hover {
  background: var(--canvas-off-white);
  color: var(--text-primary);
}

.asset-btn--primary {
  border-color: var(--brand-primary);
  color: var(--brand-primary);
}

.asset-btn--primary:hover {
  background: var(--brand-primary);
  color: #fff;
}

/* ─────────────────────────────────────────────────────────
   TRUST BULLETS
   ───────────────────────────────────────────────────────── */

.trust-list {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-xl);
  margin-top: var(--space-2xl);
}

.trust-item {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  font-size: var(--text-sm);
  color: var(--text-secondary);
}

.trust-item-icon {
  display: flex;
  align-items: center;
  flex-shrink: 0;
  color: var(--brand-accent);
}

/* ─────────────────────────────────────────────────────────
   HOW IT WORKS CARDS
   ───────────────────────────────────────────────────────── */

.how-it-works-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: var(--space-md);
  margin-top: var(--space-xl);
  text-align: left;
}

.how-it-works-card {
  background: var(--canvas-white);
  border: 1px solid var(--canvas-border);
  border-radius: var(--radius-xl);
  padding: var(--space-lg);
}

.how-it-works-step {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  background: var(--brand-primary-light);
  color: var(--brand-primary);
  font-size: var(--text-sm);
  font-weight: 600;
  margin-bottom: var(--space-md);
}

.how-it-works-title {
  font-size: var(--text-base);
  font-weight: 600;
  color: var(--text-primary);
  margin-bottom: var(--space-xs);
}

.how-it-works-desc {
  font-size: var(--text-sm);
  color: var(--text-secondary);
  font-weight: 300;
  line-height: 1.65;
}

@media (max-width: 820px) {
  .how-it-works-grid {
    grid-template-columns: 1fr 1fr;
  }
}

/* ─────────────────────────────────────────────────────────
   ASK CHATGPT, THEN FLIP THE CARD (design landing)
   One phone-width card on the report's flip. Front: a chat window,
   the question and the paragraph everyone gets. Back: the hub at
   phone width, the results window. Each face is a window with the
   flip notch as its bottom edge, so the notch turns with the card.
   ───────────────────────────────────────────────────────── */

.ask-compare {
  width: 100%;
  text-align: left;
}

.ask-flip {
  max-width: 460px;
  margin: 0 auto;
}

/* Both windows fill their face, so the shorter one (the chat) grows to
   the taller (the hub) and the notch stays on the bottom edge. */
.ask-chat,
.ask-hub {
  display: flex;
  flex-direction: column;
  height: 100%;
  background: var(--canvas-white);
  border: 1px solid var(--canvas-border);
  border-radius: var(--radius-xl);
  overflow: hidden;
  box-shadow: var(--shadow-lg);
}

/* The hub face pushes its notch to the bottom edge; on the chat face
   the composer takes that role, so the notch sits right under it. */
.ask-hub .flip-notch { margin-top: auto; }
.ask-chat .flip-notch { margin-top: 0; }

/* ── Front: the chat window ── */

/* Three dots in the title bar: a window, not a card. */
.ask-chat-bar {
  display: flex;
  gap: 6px;
  padding: 10px var(--space-md);
  border-bottom: 1px solid var(--canvas-border);
  background: var(--canvas-off-white);
}

.ask-chat-bar i {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--canvas-border);
}

.ask-chat-body {
  position: relative;
  display: flex;
  flex-direction: column;
  /* One rhythm between every turn; nothing inside sets its own margin. */
  gap: var(--space-md);
  padding: var(--space-lg);
}

.ask-bubble {
  max-width: 88%;
  padding: var(--space-md);
  font-size: var(--text-sm);
  line-height: 1.55;
  border-radius: var(--radius-lg);
}

.ask-bubble.is-user { padding: var(--space-sm) var(--space-md); }

.ask-bubble.is-user {
  align-self: flex-end;
  background: var(--text-primary);
  color: var(--text-inverse);
  border-bottom-right-radius: 4px;
}

.ask-bubble.is-bot {
  align-self: flex-start;
  background: var(--canvas-off-white);
  color: var(--text-secondary);
  border-bottom-left-radius: 4px;
}

.ask-bubble.is-bot p { margin: 0; }

/* Three bouncing dots while the chatbot "types". In flow, before the
   answer it stands in for; GSAP shows them for a beat, then collapses
   them as the answer takes the space. Without the script they never
   show. */
.ask-typing {
  display: inline-flex;
  align-self: flex-start;
  gap: 4px;
  height: 34px;
  padding: 10px 12px;
  box-sizing: border-box;
  overflow: hidden;
  background: var(--canvas-off-white);
  border-radius: var(--radius-lg);
  border-bottom-left-radius: 4px;
  opacity: 0;
  pointer-events: none;
}

.ask-typing i {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--text-muted);
  animation: ask-typing 1.1s ease-in-out infinite;
}

.ask-typing i:nth-child(2) { animation-delay: 0.15s; }
.ask-typing i:nth-child(3) { animation-delay: 0.3s; }

@keyframes ask-typing {
  0%, 60%, 100% { transform: translateY(0); opacity: 0.5; }
  30% { transform: translateY(-3px); opacity: 1; }
}

/* The picture a chatbot hands back. Shown at full colour: it is a
   lovely pool, and that is the setup. The let-down is the next answer. */
.ask-generic { margin: 0 0 var(--space-md); }

.ask-generic img {
  display: block;
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  border-radius: var(--radius-md);
}

.ask-generic figcaption {
  margin-top: var(--space-xs);
  font-size: var(--text-xs);
  font-style: italic;
  color: var(--text-muted);
}

.ask-chat-note {
  margin: 0;
  font-size: var(--text-xs);
  color: var(--text-muted);
  text-align: center;
}

/* The composer, drawn not wired: the window's bottom edge above the
   notch, so the front reads as an app and not a card of text. */
.ask-composer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin: auto var(--space-lg) var(--space-lg);
  padding: 10px var(--space-md);
  border: 1px solid var(--canvas-border);
  border-radius: var(--radius-xl);
  font-size: var(--text-sm);
  color: var(--text-muted);
}

.ask-composer i {
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: var(--canvas-border);
}

/* ── Back: the hub at phone width ── */

.ask-hub-body {
  display: flex;
  flex-direction: column;
  gap: var(--space-lg);
  padding: var(--space-lg);
}

.ask-hub-frame {
  width: 100%;
}

/* The hub's pill, static, scaled to the miniature. Its segments are
   spans here rather than buttons, so they are laid out explicitly. */
.ask-hub-version {
  transform: scale(0.85);
  transform-origin: top right;
}

.ask-hub-version .hub-version-tab {
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

/* The results page's own card, one shadow step lighter: it sits inside
   a window that already carries the page's shadow, and stacked at the
   page's weight the pair read heavier than the card does on the page.
   The frame steps down the same way. Only the numerals are otherwise
   pinned, so the count-up does not jitter. */
.ask-hub .ask-hub-card { box-shadow: var(--shadow-md); }
.ask-hub .ask-hub-frame { box-shadow: var(--shadow-lg); }

.ask-hub-card .estimate-card-total-value {
  font-variant-numeric: tabular-nums;
}

/* The ticks: what made this answer different, landing one by one. */
.ask-ticks {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-xs) var(--space-sm);
  margin: 0;
  padding: 0;
  list-style: none;
}

.ask-tick {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 10px;
  border-radius: var(--radius-xl);
  background: color-mix(in srgb, var(--brand-primary) 9%, var(--canvas-white));
  color: var(--brand-primary);
  font-size: var(--text-xs);
  font-weight: 600;
}

/* Read by a screen reader, drawn by nothing. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

@media (max-width: 640px) {
  .compare-head,
  .compare-row {
    grid-template-columns: 1fr 1fr;
  }

  /* The claim takes its own line above the two verdicts. */
  .compare-label {
    grid-column: 1 / -1;
    padding-bottom: 0;
  }

  .compare-head .compare-label { display: none; }

  .compare-cell {
    padding: var(--space-sm) var(--space-md);
    font-size: var(--text-xs);
  }

  .compare-row .compare-label {
    padding-top: var(--space-md);
    font-size: var(--text-sm);
  }

  .compare-row .compare-cell:not(.compare-label) { padding-bottom: var(--space-md); }
}

/* ─────────────────────────────────────────────────────────
   INTRO PAGE HERO AREA
   ───────────────────────────────────────────────────────── */

.intro-content {
  padding-top: var(--space-xl);
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
}

/* Primary CTA plus, on /outdoor only, the escape hatch to the pool
   funnel. Wraps to a stack on narrow screens so the ghost link never
   gets squeezed onto one line with the button. */
.intro-cta-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--space-lg);
}

/* A bare .btn-ghost disappears next to a btn-lg. One step darker so it
   reads as a real second option, still clearly subordinate to the CTA. */
.intro-cta-switch {
  color: var(--text-secondary);
  text-align: center;
}

.intro-cta-switch:hover {
  color: var(--text-primary);
  text-decoration-color: var(--text-muted);
}

@media (max-width: 560px) {
  .intro-cta-row {
    flex-direction: column;
    gap: var(--space-md);
  }
}

/* ─────────────────────────────────────────────────────────
   SKELETON / LOADING
   ───────────────────────────────────────────────────────── */

.skeleton {
  background: linear-gradient(90deg, var(--canvas-light-gray) 25%, var(--canvas-off-white) 50%, var(--canvas-light-gray) 75%);
  background-size: 200% 100%;
  animation: skeleton-shimmer 1.5s infinite;
  border-radius: var(--radius-sm);
}

@keyframes skeleton-shimmer {
  0%   { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}

/* ─────────────────────────────────────────────────────────
   RESPONSIVE — 640px breakpoint
   ───────────────────────────────────────────────────────── */

@media (max-width: 640px) {
  .headline-display {
    font-size: calc(var(--text-2xl) * var(--font-display-scale));
  }

  .page-content {
    padding: 0 var(--space-md) var(--space-2xl);
  }

  .site-header {
    padding: var(--space-lg) var(--space-md);
  }

  .site-header-phone {
    display: none;
  }

  .modal-card {
    padding: 28px 20px;
  }

  .option-card-grid {
    grid-template-columns: 1fr;
  }

  .option-card-grid.cols-3 {
    grid-template-columns: 1fr;
  }

  .testimonial-grid {
    grid-template-columns: 1fr;
  }

  .how-it-works-grid {
    grid-template-columns: 1fr;
  }

  .estimate-features-grid {
    grid-template-columns: 1fr;
  }

  /* One column: no column divider, and no missing cell to fill (the
     filler would just add an empty row above the total). */
  .estimate-feature-cell:nth-child(odd) {
    border-right: 0;
  }

  .estimate-feature-cell-filler {
    display: none;
  }

  .admin-field-grid {
    grid-template-columns: 1fr;
  }

  .admin-body {
    padding: var(--space-md);
  }

  .page-section {
    padding: var(--space-xl) var(--space-md);
  }

  .results-price-display {
    font-size: calc(var(--text-2xl) * var(--font-display-scale));
  }

  /* Vertical tightens with the rest of the page; horizontal stays at
     the desktop inset so the copy sits where the estimate and financing
     cards' does, not 24px closer to the edge than its neighbours. */
  .consultation-card {
    padding: var(--space-lg) var(--space-xl);
  }
}

/* ─────────────────────────────────────────────────────────
   SITE FOOTER
   ───────────────────────────────────────────────────────── */

.site-footer {
  text-align: center;
  padding: var(--space-xl);
  background: var(--canvas-white);
}

.site-footer-row {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 4px var(--space-sm);
  margin-top: var(--space-sm);
  font-size: var(--text-xs);
  color: var(--text-muted);
}

.site-footer-row a {
  color: var(--text-muted);
  text-decoration: none;
  transition: color var(--transition-fast);
}

.site-footer-row a:hover {
  color: var(--text-secondary);
}

.site-footer-dot {
  color: var(--canvas-border);
  user-select: none;
}

.site-footer-logo {
  display: flex;
  justify-content: center;
}

.site-footer-logo-img {
  height: var(--logo-height);
  width: auto;
  display: block;
  opacity: 0.85;
}

.site-footer-logo-text {
  font-family: var(--font-display);
  text-transform: var(--font-display-transform, none);
  letter-spacing: var(--font-display-tracking, normal);
  font-size: calc(var(--text-base) * var(--font-display-scale));
  font-weight: var(--font-display-weight-strong);
  color: var(--text-muted);
}

/* ─────────────────────────────────────────────────────────
   INTAKE FORM
   ───────────────────────────────────────────────────────── */

.intake-wrapper {
  max-width: 760px;
  margin: 0 auto;
  padding: var(--space-2xl) var(--space-xl) var(--space-2xl);
}

/* Hero */
.intake-hero {
  text-align: center;
  margin-bottom: var(--space-2xl);
  padding-bottom: var(--space-2xl);
  border-bottom: 1px solid var(--canvas-border);
}

.intake-hero-eyebrow {
  font-size: var(--text-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--brand-primary);
  margin-bottom: var(--space-sm);
}

.intake-hero-title {
  font-family: var(--font-display);
  text-transform: var(--font-display-transform, none);
  letter-spacing: var(--font-display-tracking, normal);
  font-size: calc(var(--text-2xl) * var(--font-display-scale));
  font-weight: var(--font-display-weight);
  color: var(--text-primary);
  margin-bottom: var(--space-md);
  line-height: 1.2;
}

.intake-hero-subtitle {
  font-size: var(--text-base);
  color: var(--text-secondary);
  line-height: 1.7;
  max-width: 560px;
  margin: 0 auto var(--space-lg);
}

.intake-hero-saves {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  font-size: var(--text-sm);
  color: var(--text-muted);
  background: var(--canvas-off-white);
  border: 1px solid var(--canvas-border);
  border-radius: 100px;
  padding: 5px 14px;
}

.intake-saves-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--status-success);
  animation: intake-pulse 2.5s ease-in-out infinite;
}

@keyframes intake-pulse {
  0%, 100% { opacity: 1; }
  50%       { opacity: 0.4; }
}

/* Section */
.intake-section {
  margin-bottom: var(--space-2xl);
}

.intake-section-header {
  display: flex;
  align-items: flex-start;
  gap: var(--space-md);
  margin-bottom: var(--space-lg);
}

.intake-section-number {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  background: var(--brand-primary);
  color: #fff;
  font-size: var(--text-sm);
  font-weight: 700;
  flex-shrink: 0;
  margin-top: 2px;
}

.intake-section-title {
  font-size: var(--text-md);
  font-weight: 600;
  color: var(--text-primary);
  line-height: 1.3;
}

.intake-section-subtitle {
  font-size: var(--text-sm);
  color: var(--text-muted);
  margin-top: 2px;
}

/* Callout */
.intake-callout {
  display: flex;
  align-items: flex-start;
  gap: var(--space-sm);
  background: var(--canvas-off-white);
  border: 1px solid var(--canvas-border);
  border-left: 3px solid var(--brand-primary);
  border-radius: var(--radius-md);
  padding: 12px 16px;
  font-size: var(--text-sm);
  color: var(--text-secondary);
  line-height: 1.65;
  margin-bottom: var(--space-lg);
}

.intake-callout-icon {
  color: var(--brand-primary);
  flex-shrink: 0;
  margin-top: 1px;
}

/* Items list */
.intake-items {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}

/* Item card */
.intake-item-card {
  background: var(--canvas-white);
  border: 1px solid var(--canvas-border);
  border-radius: var(--radius-lg);
  padding: var(--space-lg);
  transition: border-color var(--transition-fast);
}

.intake-item-card:hover {
  border-color: #c8c5c0;
}

.intake-item-card.is-disabled {
  background: var(--canvas-off-white);
  opacity: 0.65;
}

.intake-item-card.has-error {
  border-color: var(--status-error);
}

.intake-item-top {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: var(--space-md);
  margin-bottom: var(--space-sm);
}

.intake-item-name {
  font-size: var(--text-base);
  font-weight: 600;
  color: var(--text-primary);
}

.intake-item-meta {
  font-size: var(--text-sm);
  color: var(--text-muted);
  margin-top: 2px;
}

.intake-item-desc {
  font-size: var(--text-sm);
  color: var(--text-secondary);
  line-height: 1.65;
  margin-bottom: var(--space-md);
}

/* Benchmark: what the other partners charge for the same line. Opt-in,
   behind a small Compare button in the label row; the sentence it
   opens sits between label and input and reads as a note, not a field. */
.intake-price-label-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-sm);
}

.intake-benchmark-btn {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  font-size: var(--text-xs);
  color: var(--text-muted);
  cursor: pointer;
  white-space: nowrap;
  margin-bottom: 6px;
}

.intake-benchmark-btn:hover,
.intake-benchmark-btn.is-open {
  color: var(--brand-primary);
}

.intake-benchmark {
  font-size: var(--text-xs);
  color: var(--text-secondary);
  line-height: 1.5;
  margin: -2px 0 8px;
}

.intake-benchmark strong {
  font-weight: 600;
  color: var(--text-primary);
}

/* Toggle */
.intake-toggle-wrap {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  cursor: pointer;
  flex-shrink: 0;
}

.intake-toggle {
  width: 40px;
  height: 22px;
  border-radius: 11px;
  background: var(--canvas-border);
  position: relative;
  cursor: pointer;
  transition: background var(--transition-fast);
  flex-shrink: 0;
}

.intake-toggle::after {
  content: '';
  position: absolute;
  top: 3px;
  left: 3px;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: #fff;
  box-shadow: var(--shadow-sm);
  transition: transform var(--transition-fast);
}

.intake-toggle.is-on {
  background: var(--brand-primary);
}

.intake-toggle.is-on::after {
  transform: translateX(18px);
}

.intake-toggle-label {
  font-size: var(--text-sm);
  color: var(--text-secondary);
  white-space: nowrap;
  min-width: 76px;
}

/* Pricing area */
.intake-pricing-area {
  margin-top: var(--space-sm);
}

.intake-price-row {
  display: flex;
  align-items: flex-end;
  gap: var(--space-sm);
  flex-wrap: wrap;
}

.intake-price-field {
  flex: 1;
  min-width: 180px;
}

.intake-price-label {
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--text-secondary);
  margin-bottom: 6px;
}

.intake-price-input-wrap {
  position: relative;
}

.intake-price-unit {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  font-size: var(--text-sm);
  color: var(--text-muted);
  pointer-events: none;
  user-select: none;
}

.intake-price-unit--prefix {
  left: 11px;
}

.intake-price-unit--suffix {
  right: 11px;
}

.intake-price-input {
  width: 100%;
  padding: 10px 12px;
  border: 1px solid var(--canvas-border);
  border-radius: var(--radius-md);
  font-family: var(--font-body);
  font-size: var(--text-base);
  color: var(--text-primary);
  background: var(--canvas-white);
  transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
  -moz-appearance: textfield;
}

.intake-price-input::-webkit-outer-spin-button,
.intake-price-input::-webkit-inner-spin-button {
  -webkit-appearance: none;
}

.intake-price-input.has-prefix {
  padding-left: 24px;
}

.intake-price-input.has-suffix {
  padding-right: 56px;
}

.intake-price-input:focus {
  outline: none;
  border-color: var(--brand-primary);
  box-shadow: 0 0 0 3px var(--brand-primary-light);
}

.intake-price-input.has-error {
  border-color: var(--status-error);
}

.intake-price-input:disabled {
  background: var(--canvas-light-gray);
  color: var(--text-muted);
  cursor: not-allowed;
}

/* Dynamic pricing */
.intake-dynamic-btn {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 9px 14px;
  font-family: var(--font-body);
  font-size: var(--text-sm);
  font-weight: 500;
  border: 1px solid var(--canvas-border);
  border-radius: var(--radius-md);
  background: var(--canvas-white);
  color: var(--text-secondary);
  cursor: pointer;
  white-space: nowrap;
  transition: background var(--transition-fast), border-color var(--transition-fast), color var(--transition-fast);
  flex-shrink: 0;
  margin-bottom: 1px;
}

.intake-dynamic-btn:hover {
  background: var(--canvas-off-white);
  border-color: #c8c5c0;
}

.intake-dynamic-btn.is-active {
  background: var(--brand-primary);
  border-color: var(--brand-primary);
  color: #fff;
}

.intake-dynamic-panel {
  background: var(--canvas-off-white);
  border: 1px solid var(--canvas-border);
  border-radius: var(--radius-md);
  padding: 12px 16px;
  margin-top: var(--space-sm);
}

.intake-dynamic-badge {
  font-size: var(--text-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--brand-primary);
  margin-bottom: var(--space-xs);
}

.intake-dynamic-explain {
  font-size: var(--text-sm);
  color: var(--text-secondary);
  line-height: 1.65;
  margin: 0;
}

/* Category groups */
.intake-category-group {
  margin-bottom: var(--space-sm);
}

.intake-category-toggle {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  padding: 8px 2px;
  background: none;
  border: none;
  border-bottom: 1px solid var(--canvas-border);
  cursor: pointer;
  margin-bottom: var(--space-sm);
  color: var(--text-muted);
}

.intake-category-label {
  font-size: var(--text-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.1em;
}

.intake-category-items {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}

/* Field-level error */
.intake-field-error {
  font-size: var(--text-xs);
  color: var(--status-error);
  margin-top: var(--space-xs);
}

/* Error banner */
.intake-error-banner {
  display: flex;
  align-items: flex-start;
  gap: var(--space-md);
  background: var(--status-error-bg);
  border: 1px solid var(--status-error);
  border-radius: var(--radius-md);
  padding: var(--space-md);
  margin-bottom: var(--space-lg);
}

.intake-error-banner-icon {
  color: var(--status-error);
  flex-shrink: 0;
  margin-top: 1px;
}

.intake-error-banner-title {
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--status-error);
  margin-bottom: var(--space-xs);
}

.intake-error-list {
  margin: 0;
  padding-left: var(--space-lg);
  font-size: var(--text-sm);
  color: var(--status-error);
  line-height: 1.8;
}

/* Submit bar */
.intake-submit-bar {
  position: sticky;
  bottom: 0;
  background: var(--canvas-white);
  border-top: 1px solid var(--canvas-border);
  padding: var(--space-md) var(--space-xl);
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--space-md);
  z-index: 100;
  box-shadow: 0 -4px 16px rgba(0,0,0,0.06);
}

.intake-submit-bar-left {
  flex: 1;
  min-width: 0;
}

.intake-submit-hint {
  font-size: var(--text-sm);
  color: var(--text-muted);
}

.intake-submit-error {
  font-size: var(--text-sm);
  color: var(--status-error);
}

.intake-submit-btn {
  flex-shrink: 0;
  padding: 12px 28px;
  font-size: var(--text-base);
}

/* Success screen */
.intake-success {
  text-align: center;
  padding: 80px var(--space-xl);
  max-width: 520px;
  margin: 0 auto;
}

.intake-success-icon {
  color: var(--status-success);
  display: flex;
  justify-content: center;
  margin-bottom: var(--space-lg);
}

.intake-success-title {
  font-family: var(--font-display);
  text-transform: var(--font-display-transform, none);
  letter-spacing: var(--font-display-tracking, normal);
  font-size: calc(var(--text-2xl) * var(--font-display-scale));
  font-weight: var(--font-display-weight);
  color: var(--text-primary);
  margin-bottom: var(--space-md);
}

.intake-success-body {
  font-size: var(--text-base);
  color: var(--text-secondary);
  line-height: 1.7;
  margin-bottom: var(--space-lg);
}

.intake-success-footnote {
  font-size: var(--text-sm);
  color: var(--text-muted);
}

.intake-success-footnote a {
  color: var(--brand-primary);
}

/* Outdoor living toggle (Section 5) */
.intake-outdoor-toggle-row {
  display: flex;
  align-items: flex-start;
  gap: var(--space-md);
  padding: var(--space-md) var(--space-lg);
  background: var(--canvas-white);
  border: 1px solid var(--canvas-border);
  border-radius: var(--radius-lg);
  cursor: pointer;
}

.intake-outdoor-toggle-label {
  font-size: var(--text-base);
  font-weight: 600;
  color: var(--text-primary);
  margin-bottom: 2px;
}

.intake-outdoor-toggle-hint {
  font-size: var(--text-sm);
  color: var(--text-muted);
  line-height: 1.6;
}

.intake-outdoor-body {
  margin-top: var(--space-lg);
}

.intake-outdoor-tier-grid,
.intake-outdoor-variant-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: var(--space-sm);
}

/* Size × material price matrix. One row per size, one input per option —
   the builder reads down a size and across the materials. */
.intake-outdoor-matrix {
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
}

.intake-outdoor-matrix-row {
  display: grid;
  grid-template-columns: 120px 1fr;
  gap: var(--space-md);
  align-items: start;
}

.intake-outdoor-matrix-head {
  padding-top: 2px;
}

.intake-outdoor-matrix-size {
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--text-primary);
}

.intake-outdoor-matrix-sub {
  font-size: var(--text-xs);
  color: var(--text-muted);
  margin-top: 1px;
}

.intake-outdoor-matrix-inputs {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
  gap: var(--space-sm);
}

.intake-price-sublabel {
  font-size: var(--text-xs);
  color: var(--text-muted);
  margin-top: -2px;
  margin-bottom: 3px;
}

@media (max-width: 640px) {
  /* Stacked: the size becomes a heading over its own inputs. */
  .intake-outdoor-matrix-row {
    grid-template-columns: 1fr;
    gap: var(--space-sm);
  }
}

/* Admin intake import */
.admin-intake-import {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  padding: var(--space-md);
  background: var(--canvas-off-white);
  border: 1px solid var(--canvas-border);
  border-radius: var(--radius-md);
  margin-bottom: var(--space-lg);
}

/* Intake responsive */
@media (max-width: 640px) {
  .intake-wrapper {
    padding: var(--space-xl) var(--space-md) var(--space-xl);
  }

  .intake-submit-bar {
    padding: var(--space-md);
    flex-direction: column;
    align-items: stretch;
    padding-bottom: max(var(--space-md), env(safe-area-inset-bottom));
    transition: opacity 0.15s ease, transform 0.15s ease;
  }

  .intake-submit-bar.keyboard-open {
    opacity: 0;
    pointer-events: none;
    transform: translateY(100%);
  }

  .intake-submit-btn {
    width: 100%;
    justify-content: center;
  }

  .intake-price-row {
    flex-direction: column;
    align-items: stretch;
  }

  .intake-dynamic-btn {
    width: 100%;
    justify-content: center;
  }
}

/* ─────────────────────────────────────────────────────────
   SURVEY BANNER
───────────────────────────────────────────────────────── */

.survey-banner {
  position: fixed;
  bottom: var(--space-lg);
  left: 50%;
  transform: translateX(-50%) translateY(0);
  width: calc(100% - var(--space-xl));
  max-width: 440px;
  background: var(--canvas-white);
  border: 1px solid var(--canvas-border);
  border-radius: var(--radius-xl);
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12), 0 2px 8px rgba(0, 0, 0, 0.06);
  padding: var(--space-md) var(--space-lg);
  z-index: 200;
  animation: survey-slide-up 0.35s cubic-bezier(0.22, 1, 0.36, 1) both;
}

@keyframes survey-slide-up {
  from {
    opacity: 0;
    transform: translateX(-50%) translateY(20px);
  }
  to {
    opacity: 1;
    transform: translateX(-50%) translateY(0);
  }
}

.survey-banner-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: var(--space-xs);
}

.survey-banner-eyebrow {
  font-size: var(--text-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-muted);
}

.survey-banner-close {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  border: none;
  background: none;
  color: var(--text-muted);
  cursor: pointer;
  border-radius: var(--radius-sm);
  font-size: calc(14px * var(--font-body-scale));
  line-height: 1;
  padding: 0;
  transition: color 0.15s, background 0.15s;
}

.survey-banner-close:hover {
  color: var(--text-primary);
  background: var(--canvas-off-white);
}

.survey-banner-question {
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--text-primary);
  margin: 0 0 var(--space-sm);
}

.survey-choices {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  margin-bottom: var(--space-md);
}

.survey-choice {
  display: block;
  width: 100%;
  text-align: left;
  padding: var(--space-xs) var(--space-sm);
  border: 1.5px solid var(--canvas-border);
  border-radius: var(--radius-md);
  background: var(--canvas-white);
  font-size: var(--text-sm);
  color: var(--text-primary);
  cursor: pointer;
  transition: border-color 0.15s, background 0.15s, color 0.15s;
}

.survey-choice:hover {
  border-color: var(--brand-primary);
  background: color-mix(in srgb, var(--brand-primary) 5%, transparent);
}

.survey-choice.selected {
  border-color: var(--brand-primary);
  background: color-mix(in srgb, var(--brand-primary) 8%, transparent);
  color: var(--brand-primary);
  font-weight: 500;
}

.survey-banner-thanks {
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--text-primary);
  text-align: center;
  padding: var(--space-sm) 0;
}

/* ───────────────────────────────────────────────
   BOOKLET LEAD-MAGNET FUNNEL (/guide/[slug])
   ─────────────────────────────────────────────── */
.bk-narrow { max-width: 760px; margin: 0 auto; text-align: center; }

.bk-hero { padding: var(--space-3xl) var(--space-lg) var(--space-2xl); text-align: center; }
.bk-section { padding: var(--space-3xl) var(--space-lg); text-align: center; }
/* Off-white band (e.g. "Inside The Booklet") — the shared bg-off-white rule is
   scoped to .page-section, so booklet sections need their own. Matches the
   reference's alternating white → off-white → white rhythm. Not part of the
   tintable band (see PAGE SECTIONS): the copy inside is literal greys. */
.bk-section.bg-off-white {
  background: var(--canvas-off-white);
  border-top: 1px solid var(--canvas-border);
  border-bottom: 1px solid var(--canvas-border);
}

.bk-h1 {
  font-family: var(--font-display); text-transform: var(--font-display-transform, none); font-weight: var(--font-display-weight);
  font-size: calc(56px * var(--font-body-scale) * var(--font-display-scale)); line-height: 1.1; letter-spacing: var(--font-display-tracking, -0.015em);
  color: var(--text-primary); text-wrap: pretty;
}
.bk-h2 {
  font-family: var(--font-display); text-transform: var(--font-display-transform, none); font-weight: var(--font-display-weight);
  font-size: calc(42px * var(--font-body-scale) * var(--font-display-scale)); line-height: 1.14; letter-spacing: var(--font-display-tracking, -0.01em);
  color: var(--text-primary); text-wrap: pretty;
}
.bk-sub {
  max-width: 600px; margin: var(--space-lg) auto 0;
  font-size: calc(18.5px * var(--font-body-scale)); line-height: 1.6; color: var(--text-secondary);
}

.bk-cover { margin: var(--space-2xl) auto 0; max-width: 430px; }
.bk-cover img {
  width: 100%; height: auto; display: block;
  filter: drop-shadow(0 30px 50px rgba(21, 40, 60, 0.22));
}

.bk-cta {
  appearance: none; border: none; cursor: pointer;
  background: var(--brand-primary); color: var(--text-inverse);
  font-family: var(--font-body); padding: 17px 44px; border-radius: var(--radius-md);
  box-shadow: 0 12px 26px rgba(28, 107, 160, 0.28);
  transition: transform 0.15s ease, box-shadow 0.15s ease, background 0.15s ease;
  display: inline-flex; flex-direction: column; align-items: center; gap: 4px;
}
.bk-cta:hover { background: var(--brand-primary-hover); transform: translateY(-2px); box-shadow: 0 16px 32px rgba(28, 107, 160, 0.34); }
.bk-cta-main { font-weight: 700; font-size: calc(24px * var(--font-body-scale)); letter-spacing: 0.01em; }
.bk-cta-sub { font-weight: 400; font-size: calc(12.5px * var(--font-body-scale)); color: rgba(255, 255, 255, 0.8); font-style: italic; }

.bk-trust {
  display: flex; flex-wrap: wrap; justify-content: center; gap: 30px;
  margin-top: var(--space-lg); font-size: calc(13.5px * var(--font-body-scale)); font-weight: 600; color: #6b6b66;
}
.bk-trust span { display: inline-flex; align-items: center; gap: 8px; }
.bk-trust svg { color: var(--brand-primary); }

.bk-bullets { max-width: 580px; margin: var(--space-xl) auto 0; display: flex; flex-direction: column; gap: 22px; text-align: left; }
.bk-bullet { display: flex; gap: 15px; align-items: flex-start; }
.bk-bullet p { font-size: calc(17px * var(--font-body-scale)); line-height: 1.6; color: #3a3a38; }
.bk-bullet-mark {
  flex: none; width: 23px; height: 23px; border-radius: 50%;
  background: var(--brand-primary); color: #fff;
  display: flex; align-items: center; justify-content: center; margin-top: 3px;
}
.bk-bonus { margin-top: 4px; padding-top: var(--space-lg); border-top: 1px solid var(--canvas-border); }
.bk-bonus strong { color: var(--brand-primary); font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; font-size: calc(13.5px * var(--font-body-scale)); }

.bk-modal-trust {
  display: flex; align-items: center; justify-content: center; gap: 8px;
  margin-top: var(--space-md); font-size: calc(13px * var(--font-body-scale)); color: var(--text-muted);
}

@media (max-width: 640px) {
  .bk-h1 { font-size: calc(34px * var(--font-body-scale) * var(--font-display-scale)); }
  .bk-h2 { font-size: calc(28px * var(--font-body-scale) * var(--font-display-scale)); }
  .bk-sub { font-size: calc(16px * var(--font-body-scale)); }
  .bk-hero, .bk-section { padding: var(--space-2xl) var(--space-lg); }
  .bk-cta-main { font-size: calc(20px * var(--font-body-scale)); }
  .bk-trust { gap: 18px; }
}

/* Phone handoff card. Hidden on phones — a QR pointing at the device
   showing it is nonsense. CSS, not a JS width check, so there is no
   hydration flash of a QR that then vanishes. */
.qr-desktop-only {
  display: flex;
}

@media (max-width: 768px) {
  /* A narrower card is a shorter one; 280px of aerial on a phone pushes
     the flip control off the fold. */
  .settled-aerial {
    min-height: 200px;
  }

  .settled-cell {
    padding-block: var(--space-md);
  }

  .qr-desktop-only {
    display: none;
  }
}

/* ─────────────────────────────────────────────────────────
   CONSULT PREP (/prep)
   Ported from the virtual-consult intake. The donor pinned these to a
   fixed teal (--intake-accent); here they run on the tenant's resolved
   brand vars, so the flow is branded by the same Edge Config value that
   themes every other page.
   ───────────────────────────────────────────────────────── */

.iw-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-sm);
  padding: 13px 24px;
  background: var(--brand-primary);
  color: #fff;
  font-family: var(--font-body);
  font-size: var(--text-base);
  font-weight: 600;
  border-radius: var(--radius-md);
  border: none;
  cursor: pointer;
  transition: background var(--transition-fast), transform var(--transition-fast),
    box-shadow var(--transition-fast);
}

.iw-btn:hover {
  background: var(--brand-primary-hover);
  transform: translateY(-1px);
  box-shadow: var(--shadow-md);
}

.iw-btn:active {
  transform: none;
  box-shadow: none;
}

.iw-btn:disabled {
  opacity: 0.4;
  cursor: not-allowed;
  transform: none;
  box-shadow: none;
}

.iw-btn-ghost {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  padding: 6px 0;
  background: none;
  border: none;
  color: var(--text-muted);
  font-family: var(--font-body);
  font-size: var(--text-sm);
  font-weight: 400;
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-color: transparent;
  transition: color var(--transition-fast), text-decoration-color var(--transition-fast);
}

.iw-btn-ghost:hover {
  color: var(--text-secondary);
  text-decoration-color: var(--canvas-border);
}

.iw-eyebrow {
  display: block;
  text-align: center;
  font-size: var(--text-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--brand-primary);
  margin-bottom: var(--space-sm);
}

.iw-headline {
  font-family: var(--font-display);
  text-transform: var(--font-display-transform, none);
  letter-spacing: var(--font-display-tracking, normal);
  /* The donor hardcoded a clamp here. This repo scales display type per
     tenant font pack, and check-font-scale.mjs enforces it, so the size
     flows through --text-3xl and the pack multiplier like every other
     display selector. */
  font-size: calc(var(--text-3xl) * var(--font-display-scale));
  font-weight: var(--font-display-weight);
  color: var(--text-primary);
  line-height: 1.2;
  margin-bottom: var(--space-sm);
  text-align: center;
}

.iw-subheadline {
  font-size: var(--text-base);
  color: var(--text-secondary);
  line-height: 1.65;
  margin-bottom: var(--space-xl);
  text-align: center;
}

.iw-form-group {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
}

.iw-form-label {
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--text-primary);
}

.iw-form-input {
  width: 100%;
  padding: 11px 14px;
  font-family: var(--font-body);
  font-size: var(--text-base);
  color: var(--text-primary);
  background: var(--canvas-white);
  border: 1.5px solid var(--canvas-border);
  border-radius: var(--radius-md);
  outline: none;
  transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
  appearance: none;
}

.iw-form-input::placeholder {
  color: var(--text-muted);
}

.iw-form-input:focus {
  border-color: var(--brand-primary);
  box-shadow: 0 0 0 3px var(--brand-primary-light);
}

/* ─────────────────────────────────────────────────────────
   FALL SPECIAL LANDING PAGE (jpiercetx)
   ───────────────────────────────────────────────────────── */

/* The two CTAs are a matched pair, not a primary with a footnote: same cell
   in a 50/50 grid, so they come out identical in width, and grid's default
   stretch makes them identical in height whatever the copy does. */
.fs-cta-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-md);
  width: 100%;
  max-width: 520px;
  margin-left: auto;
  margin-right: auto;
}

/* .btn-secondary reads as a footnote next to a btn-lg — too small, too grey.
   This is the same button at btn-lg's weight and metrics, outlined instead of
   filled, so it offers a real second path without competing for the click. */
.fs-btn-outline {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-sm);
  padding: 18px 24px;
  background: var(--canvas-white);
  /* Label is text-primary, not brand-primary: the brand gold only reaches
     2.5:1 on white, well under AA. The border carries the brand instead. */
  color: var(--text-primary);
  font-family: var(--font-body);
  font-size: var(--text-md);
  font-weight: 600;
  letter-spacing: 0.01em;
  border: 1.5px solid var(--brand-primary);
  border-radius: var(--radius-md);
  cursor: pointer;
  white-space: nowrap;
  text-decoration: none;
  transition: background var(--transition-fast), border-color var(--transition-fast),
    transform var(--transition-fast), box-shadow var(--transition-fast);
}

.fs-btn-outline:hover {
  background: var(--brand-primary-light);
  border-color: var(--brand-primary-hover);
  transform: translateY(-1px);
  box-shadow: var(--shadow-md);
}

.fs-btn-outline:active {
  transform: none;
  box-shadow: none;
}

/* Hero price reveal. The card itself is the estimator's .estimate-card;
   the only thing it can't express is a discount, so the right-hand side of
   .estimate-card-total becomes a small stack — struck regular price beside
   the offer, savings underneath — instead of a single value. */
.fs-total-figures {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 2px;
}

.fs-total-prices {
  display: flex;
  align-items: baseline;
  gap: var(--space-sm);
}

.fs-total-was {
  font-size: var(--text-md);
  color: var(--text-muted);
  text-decoration: line-through;
}

.fs-total-save {
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--brand-primary);
}

.fs-financing-note {
  font-size: var(--text-xs);
  color: var(--text-muted);
  margin-top: var(--space-md);
  text-align: center;
}

/* Constrained and centred rather than spanning the full 960px section:
   three short blurbs stretched edge-to-edge read as sprawl, not columns. */
.fs-why-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-xl);
  max-width: 780px;
  margin: var(--space-2xl) auto 0;
  text-align: center;
}

/* Same card as .how-it-works-card: white on the off-white band. */
.fs-why-item {
  background: var(--canvas-white);
  border: 1px solid var(--canvas-border);
  border-radius: var(--radius-xl);
  padding: var(--space-lg);
}

/* Circular badge in the .how-it-works-step idiom — a white disc would
   disappear into the card behind it. */
.fs-why-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  margin: 0 auto var(--space-md);
  border-radius: 50%;
  background: var(--brand-primary-light);
  color: var(--brand-primary);
}

.fs-why-item h4 {
  font-size: var(--text-base);
  font-weight: 600;
  color: var(--text-primary);
  margin-bottom: 6px;
}

.fs-why-item p {
  font-size: var(--text-sm);
  color: var(--text-secondary);
  font-weight: 300;
  line-height: 1.6;
}

/* .financing-card is width:100%, sized for the funnel's 680px column. The
   landing page's sections are 960px wide, where it would sprawl — so hold it
   to the width of the hero's estimate card and let the two read as the same
   object. */
.fs-financing-panel {
  max-width: 600px;
  margin: var(--space-xl) auto 0;
  text-align: left;
}

@media (max-width: 560px) {
  /* Two full-size buttons side by side stop fitting well before the copy
     wraps, so stack them rather than shrink them. */
  .fs-cta-row { grid-template-columns: 1fr; }
}

@media (max-width: 640px) {
  .fs-why-grid { grid-template-columns: 1fr; }

  /* .estimate-card-total stacks and centres at this width, so the
     figures have to stop hugging the right edge with it. */
  .fs-total-figures { align-items: center; }
}

/* ─────────────────────────────────────────────────────────
   VIDEO TESTIMONIALS (Cloudflare Stream)

   Same social-proof slot as .testimonial-grid, so it inherits the
   section chrome around it and only styles the player itself.
   ───────────────────────────────────────────────────────── */

/* Flex rather than grid, purely so a partial last row centres itself.
   Grid pins every item to a column track, so a leftover card sits hard
   left with a hole beside it, and centring it needs a separate rule per
   leftover count — workable for one orphan in a 2-up row, but portrait
   is 3-up and can strand either one card or two. justify-content
   handles every count at both widths with no special cases. */
.vt-grid {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-xl) var(--space-lg);
  width: 100%;
  max-width: 860px;
  margin: 0 auto;
}

/* Two up. The basis subtracts this card's share of the row's single
   column gap, since gap sits outside flex-basis. */
.vt-card {
  flex: 0 1 calc(50% - (var(--space-lg) / 2));
  min-width: 0;
  margin: 0;
}

.vt-grid-landscape {
  --vt-aspect: 16 / 9;
}

/* Portrait cards are tall, so two across would tower over the section.
   Three narrower columns keep the row a sane height and still read as a
   set. The grid also tightens, since 9:16 at half a 860px row would be
   nearly 800px tall per card. */
.vt-grid-portrait {
  --vt-aspect: 9 / 16;
  max-width: 720px;
}

/* Three up: two column gaps to share across three cards. */
.vt-grid-portrait .vt-card {
  flex: 0 1 calc(33.333% - (var(--space-lg) * 2 / 3));
}

/* Exactly four portrait tiles, staggered across three columns:

       [2]
   [1] [2] [4]      centres form a diamond
       [3]

   Wrapping alone gives 3-then-1, which strands the fourth card under a
   full row and reads as a T. Grid can place this directly: the middle
   column stacks two cards, and the outer two each span both rows and
   centre against them. It also comes out shorter than stacking the four
   as 1/2/1, since only two card-heights are ever stacked.

   Columns are fractions rather than a fixed width so the block still
   shrinks between this breakpoint and the phone one. */
.vt-grid-diamond {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  grid-template-rows: auto auto;
  align-items: center;
}

.vt-grid-diamond .vt-card {
  width: auto;
}

.vt-grid-diamond .vt-card:nth-child(1) {
  grid-column: 1;
  grid-row: 1 / 3;
}

.vt-grid-diamond .vt-card:nth-child(2) {
  grid-column: 2;
  grid-row: 1;
}

.vt-grid-diamond .vt-card:nth-child(3) {
  grid-column: 2;
  grid-row: 2;
}

.vt-grid-diamond .vt-card:nth-child(4) {
  grid-column: 3;
  grid-row: 1 / 3;
}

.vt-grid-single {
  max-width: 520px;
}

.vt-grid-portrait.vt-grid-single {
  max-width: 300px;
}

.vt-frame {
  position: relative;
  width: 100%;
  /* Set by the grid's orientation modifier below. */
  aspect-ratio: var(--vt-aspect, 16 / 9);
  border-radius: var(--radius-lg);
  overflow: hidden;
  background: var(--text-primary);
  box-shadow: var(--shadow-lg);
}

/* Playback keeps the source aspect ratio, so a vertical phone clip
   pillarboxes rather than getting its speaker's head cropped off. */
.vt-video {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
}

/* The poster is the one thing allowed to crop: it fills the 16:9 frame
   so the grid reads evenly before anything is played. */
.vt-play {
  position: absolute;
  inset: 0;
  padding: 0;
  border: none;
  background: none;
  cursor: pointer;
  display: block;
  width: 100%;
  height: 100%;
  transition: transform var(--transition-base);
}

/* The thumbnail crops to fill the frame — a clip that doesn't match the
   chosen orientation gets its edges trimmed rather than shown with black
   bands down the sides. Playback is the opposite (.vt-video uses
   contain), so nothing is ever cut off the video itself. */
.vt-poster {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  /* Sits above the video but must never swallow a click meant for the
     play button, or for the native controls once playback starts. */
  pointer-events: none;
  transition: opacity var(--transition-base);
}

/* Cleared only once frames are actually on screen, so the gap between
   pressing play and the stream arriving shows the thumbnail rather than
   an empty black frame. */
.vt-poster-gone {
  opacity: 0;
}

.vt-play-scrim {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-sm);
  background: linear-gradient(160deg, rgba(26, 26, 26, 0.3), rgba(26, 26, 26, 0.52));
  transition: background var(--transition-base);
}

.vt-play:hover .vt-play-scrim,
.vt-play:focus-visible .vt-play-scrim {
  background: rgba(26, 26, 26, 0.18);
}

.vt-play-icon {
  width: 62px;
  height: 62px;
  border-radius: 50%;
  background: var(--brand-primary);
  color: var(--text-inverse);
  display: flex;
  align-items: center;
  justify-content: center;
  /* Optical centring: a triangle's visual mass sits left of its box. */
  padding-left: 3px;
  box-shadow: 0 12px 30px rgba(0, 0, 0, 0.34);
  transition: transform var(--transition-base);
}

.vt-play:hover .vt-play-icon,
.vt-play:focus-visible .vt-play-icon {
  transform: scale(1.08);
}

.vt-play-label {
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  /* Literal, deliberately. The label sits on the scrim above, which is a
     fixed dark regardless of the section under the video — so it cannot
     take --text-inverse, which a tinted band remaps to the band colour
     and would put a dark label on a dark scrim. */
  color: #fff;
}

.vt-client {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-sm);
  margin: var(--space-md) 0 0;
}

.vt-avatar {
  flex: none;
  width: 42px;
  height: 42px;
  border-radius: 50%;
  background: var(--brand-primary-light);
  color: var(--brand-primary);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--text-sm);
  font-weight: 600;
}

.vt-client-text {
  display: flex;
  flex-direction: column;
  text-align: left;
}

.vt-client-name {
  font-size: var(--text-base);
  font-weight: 600;
  color: var(--text-primary);
  line-height: 1.3;
}

.vt-client-meta {
  font-size: var(--text-xs);
  color: var(--text-muted);
  line-height: 1.3;
}

@media (max-width: 640px) {
  .vt-grid {
    max-width: 460px;
    gap: var(--space-xl);
  }

  .vt-card,
  .vt-grid-portrait .vt-card {
    flex: 0 1 100%;
  }

  /* One column on a phone, so the stagger returns to a plain stack. */
  .vt-grid-diamond {
    display: flex;
  }

  /* A 9:16 card at full phone width is taller than the screen, so cap it
     to something that leaves the name and location visible without
     scrolling past the video. */
  .vt-grid-portrait {
    max-width: 300px;
  }

  .vt-card:last-child:nth-child(odd) {
    width: 100%;
  }

  .vt-play-icon {
    width: 52px;
    height: 52px;
  }
}

/* ─────────────────────────────────────────────────────────
   CONSULT BUILD MAP — "built down your road" (jpiercetx /consult)
   components/consult/BuildShowcase.tsx, BuildMap.tsx, BuildCard.tsx

   A map of the Waco area drawn from Census geometry, one pin per build
   at town level, each opening a card with the build's video. .bs-* is
   the section and its cards, .bm-* the map.

   Everything on the map is a token: the ground is --canvas-*, water is
   --brand-primary-light, so a tinted band or another tenant's brand
   re-derives it with no baked palette. Scrims over footage and the
   gesture hint mix --page-text-primary rather than the band's ink, for
   the same reason .vt-play-scrim is dark whatever the section is.
   ───────────────────────────────────────────────────────── */

.bs-intro {
  max-width: 540px;
  margin: 0 auto;
}

/* Desktop: every build video is vertical, so the card is a 9:16 player
   with its details under it. Its width is set from the viewport height so
   the whole card stays about one screen tall (at 1280x800 it is ~260px
   wide and ~790px tall, level with the map and its "Also near" row); the
   map takes the rest of the width. 380px is roughly the card body under
   the player plus the sticky offset. */
.bs-layout {
  --bs-card-w: clamp(260px, calc((100svh - 380px) * 9 / 16), 320px);
  display: grid;
  grid-template-columns: minmax(0, 1fr) var(--bs-card-w);
  gap: var(--space-xl);
  margin-top: var(--space-2xl);
  text-align: left;
  align-items: start;
}

/* The card rides beside the map as you scroll past a tall map. */
.bs-panel {
  position: sticky;
  top: var(--space-lg);
}

/* ── The map ── */

.bm-map {
  border: 1px solid var(--canvas-border);
  border-radius: var(--radius-xl);
  overflow: hidden;
  background: var(--canvas-white);
  box-shadow: var(--shadow-md);
}

.bm-frame {
  position: relative;
  aspect-ratio: 1 / 1;
  overflow: hidden;
}

/* pan-x pan-y: one finger stays the page's (it scrolls), pinch is
   withheld from the browser so two fingers reach the map's own handler.
   See the header comment in BuildMap.tsx. */
.bm-svg {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  touch-action: pan-x pan-y;
  cursor: grab;
  user-select: none;
  -webkit-user-select: none;
  -webkit-tap-highlight-color: transparent;
}

.bm-svg.is-dragging {
  cursor: grabbing;
}

.bm-land {
  fill: var(--canvas-white);
}

.bm-ground {
  opacity: 0;
  transition: opacity var(--transition-slow);
}

.bm-ground.is-ready {
  opacity: 1;
}

/* The ground scales; its lines must not. */
.bm-ground path {
  vector-effect: non-scaling-stroke;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.bm-place {
  fill: var(--canvas-light-gray);
}

.bm-county {
  fill: none;
  stroke: var(--canvas-border);
  stroke-width: 1;
  stroke-dasharray: 5 4;
}

.bm-water {
  fill: var(--brand-primary-light);
}

.bm-river {
  fill: none;
  stroke: var(--brand-primary-light);
  stroke-width: 2;
}

.bm-road-minor {
  fill: none;
  stroke: var(--canvas-border);
  stroke-width: 1;
}

.bm-road-highway {
  fill: none;
  stroke: var(--canvas-border);
  stroke-width: 2.5;
}

/* The interstate is cased: a darker edge with the ground's white down
   the middle, so it reads as the big road without a new colour. */
.bm-road-major-casing {
  fill: none;
  stroke: color-mix(in srgb, var(--canvas-border) 70%, var(--text-muted));
  stroke-width: 6;
}

.bm-road-major {
  fill: none;
  stroke: var(--canvas-white);
  stroke-width: 3;
}

/* Labels are drawn in screen space at a fixed size; the halo keeps them
   legible over roads and the town tint. */
.bm-label {
  font-family: var(--font-body);
  paint-order: stroke;
  stroke: var(--canvas-white);
  stroke-width: 3px;
  stroke-linejoin: round;
  pointer-events: none;
}

.bm-label-major {
  font-size: var(--text-base);
  font-weight: 700;
  fill: var(--text-primary);
}

.bm-label-pin-town {
  font-size: var(--text-sm);
  font-weight: 600;
  fill: var(--text-secondary);
}

.bm-label-town {
  font-size: var(--text-xs);
  fill: var(--text-muted);
}

.bm-label-water {
  font-size: var(--text-xs);
  font-style: italic;
  fill: var(--brand-primary);
  stroke: none;
}

.bm-shield {
  pointer-events: none;
}

.bm-shield rect {
  fill: var(--canvas-white);
  stroke: var(--canvas-border);
  stroke-width: 1;
}

.bm-shield-I rect {
  stroke: color-mix(in srgb, var(--canvas-border) 70%, var(--text-muted));
}

.bm-shield text {
  font-family: var(--font-body);
  font-size: var(--text-xs);
  font-weight: 700;
  fill: var(--text-muted);
  text-anchor: middle;
}

/* ── Pins ──
   Each pin is three nested groups: the outer one is positioned by the
   script, .bm-pin-drop carries the entrance, .bm-pin-body the hover and
   selected lift. SVG transform-origin defaults to the group's own 0,0,
   which is the pin's centre, so the scales below grow from the middle.
   Radii are set by the script from the zoom (pinRadius), so nothing here
   sizes a circle; it only scales them.

   Only the selected pin is active. Every other pin is passive: a calm dot
   with no halo and no pulse, solid for a finished pool, a hollow ring for
   one being built. Both are the brand colour, so the two read apart by
   shape on any tenant's brand rather than by two shades that can be close.
   The selected state moves on --bs-motion-ms / --bs-motion-ease, the clock
   the phone card row and the map's fly-to share (BuildShowcase), so the
   pin, the card and the map land together. */

.bm-pin {
  cursor: pointer;
  outline: none;
}

.bm-pin.is-clustered {
  visibility: hidden;
  pointer-events: none;
}

.bm-pin-hit {
  fill: transparent;
}

.bm-pin-body {
  transition: transform var(--bs-motion-ms, 250ms) var(--bs-motion-ease, ease);
}

.bm-pin-halo {
  fill: var(--brand-primary);
  opacity: 0;
  transform: scale(0.5);
  transition:
    opacity var(--bs-motion-ms, 250ms) var(--bs-motion-ease, ease),
    transform var(--bs-motion-ms, 250ms) var(--bs-motion-ease, ease);
}

.bm-pin-dot {
  fill: var(--brand-primary);
  stroke: var(--canvas-white);
  stroke-width: 2;
  transition: stroke var(--transition-fast);
}

/* Building: a ring, not a dot. */
.bm-pin.is-progress .bm-pin-dot {
  fill: var(--canvas-white);
  stroke: var(--brand-primary);
  stroke-width: 2.5;
}

/* Desktop hover: a light preview of the selected state. */
@media (hover: hover) {
  .bm-pin:not(.is-active):hover .bm-pin-body {
    transform: scale(1.2);
  }

  .bm-pin:not(.is-active):hover .bm-pin-halo {
    opacity: 0.12;
    transform: scale(0.8);
  }
}

/* Selected: lifted, larger, haloed, with a shadow under it. */
.bm-pin.is-active .bm-pin-body {
  transform: translateY(-3px) scale(1.4);
}

.bm-pin.is-active .bm-pin-halo {
  opacity: 0.22;
  transform: scale(1);
}

.bm-pin.is-active .bm-pin-dot {
  stroke-width: 2.5;
  filter: drop-shadow(0 2px 2px color-mix(in srgb, var(--page-text-primary) 35%, transparent));
}

.bm-pin.is-active.is-progress .bm-pin-dot {
  stroke-width: 3;
}

.bm-pin:focus-visible .bm-pin-dot {
  stroke: var(--text-primary);
}

/* Building now, and selected: a ring that keeps breathing out from the
   pin. Passive building pins don't pulse; only the selected one is loud. */
.bm-pin-pulse {
  fill: var(--brand-primary);
  opacity: 0;
}

.bm-pin.is-active .bm-pin-pulse {
  animation: bm-pulse 2.2s ease-out infinite;
}

@keyframes bm-pulse {
  0%   { transform: scale(1);   opacity: 0.45; }
  100% { transform: scale(3.2); opacity: 0; }
}

/* Zoomed right out: pins close enough to touch become a count bubble. */
.bm-cluster {
  cursor: pointer;
  outline: none;
}

.bm-cluster-dot {
  fill: var(--brand-primary);
  stroke: var(--canvas-white);
  stroke-width: 2;
  filter: drop-shadow(0 1px 2px color-mix(in srgb, var(--page-text-primary) 30%, transparent));
  transition: transform var(--transition-fast);
}

.bm-cluster-count {
  fill: var(--text-inverse);
  font-family: var(--font-body);
  font-size: var(--text-xs);
  font-weight: 700;
  text-anchor: middle;
  pointer-events: none;
}

@media (hover: hover) {
  .bm-cluster:hover .bm-cluster-dot {
    transform: scale(1.1);
  }
}

.bm-cluster:focus-visible .bm-cluster-dot {
  stroke: var(--text-primary);
}

/* Entrance: hidden until the map scrolls into view, then each pin drops
   in with an overshoot, north to south (--i is set per pin). */
.bm-svg:not(.is-revealed) .bm-pin-drop {
  opacity: 0;
}

.bm-svg.is-revealed:not(.is-entered) .bm-pin-drop {
  animation: bm-drop 560ms cubic-bezier(0.34, 1.56, 0.64, 1) both;
  animation-delay: calc(var(--i, 0) * 70ms);
}

@keyframes bm-drop {
  0%   { opacity: 0; transform: translateY(-28px) scale(0.4); }
  55%  { opacity: 1; }
  100% { opacity: 1; transform: none; }
}

/* ── Map chrome: zoom buttons, edge chips, the gesture hint, legend ── */

.bm-controls {
  position: absolute;
  top: var(--space-sm);
  right: var(--space-sm);
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
}

.bm-control {
  width: 36px;
  height: 36px;
  display: grid;
  place-items: center;
  padding: 0;
  border: 1px solid var(--canvas-border);
  border-radius: var(--radius-md);
  background: var(--canvas-white);
  color: var(--text-secondary);
  box-shadow: var(--shadow-sm);
  cursor: pointer;
  transition: background var(--transition-fast), color var(--transition-fast),
    transform var(--transition-fast), opacity var(--transition-base);
}

.bm-control:hover:not(:disabled) {
  background: var(--canvas-light-gray);
  color: var(--text-primary);
}

.bm-control:active:not(:disabled) {
  transform: scale(0.94);
}

.bm-control:disabled {
  opacity: 0.45;
  cursor: default;
}

/* Reset only appears once there is something to reset. */
.bm-control.is-idle {
  opacity: 0;
  transform: scale(0.8);
  pointer-events: none;
}

/* Points at a town whose pins are out of frame; anchored to the edge it
   sits on, so it never hangs off the map. */
.bm-chip {
  position: absolute;
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  padding: var(--space-xs) var(--space-sm);
  border: 1px solid var(--canvas-border);
  border-radius: 999px;
  background: var(--canvas-white);
  box-shadow: var(--shadow-md);
  font-family: var(--font-body);
  font-size: var(--text-xs);
  font-weight: 600;
  color: var(--text-primary);
  white-space: nowrap;
  cursor: pointer;
  animation: bm-fade-in var(--transition-base) both;
}

.bm-chip.is-up    { transform: translate(-50%, 0); }
.bm-chip.is-down  { transform: translate(-50%, -100%); }
.bm-chip.is-left  { transform: translate(0, -50%); }
.bm-chip.is-right { transform: translate(-100%, -50%); }
.bm-chip.is-down svg  { transform: rotate(180deg); }
.bm-chip.is-left svg  { transform: rotate(-90deg); }
.bm-chip.is-right svg { transform: rotate(90deg); }

.bm-chip:hover {
  border-color: var(--brand-primary-light);
}

@keyframes bm-fade-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

.bm-hint {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-lg);
  text-align: center;
  background: color-mix(in srgb, var(--page-text-primary) 55%, transparent);
  color: var(--page-text-inverse);
  font-family: var(--font-body);
  font-size: var(--text-md);
  font-weight: 600;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--transition-base);
}

.bm-hint.is-visible {
  opacity: 1;
}

.bm-legend {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-xs) var(--space-lg);
  padding: var(--space-sm) var(--space-md);
  border-top: 1px solid var(--canvas-border);
  background: var(--canvas-white);
  font-size: var(--text-xs);
  color: var(--text-secondary);
}

.bm-legend span {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
}

.bm-legend-dot {
  box-sizing: border-box;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--brand-primary);
}

/* Matches the map: a hollow ring for a build under way. */
.bm-legend-dot.is-progress {
  background: var(--canvas-white);
  border: 2px solid var(--brand-primary);
}

.bm-legend .bm-legend-hint {
  margin-left: auto;
  color: var(--text-muted);
}

/* ── The build card ── */

.bs-card {
  background: var(--canvas-white);
  border: 1px solid var(--canvas-border);
  border-radius: var(--radius-xl);
  box-shadow: var(--shadow-lg);
  overflow: hidden;
  text-align: left;
}

.bs-card.is-entering {
  animation: bs-card-in 420ms cubic-bezier(0.2, 0.8, 0.2, 1) both;
}

@keyframes bs-card-in {
  from { opacity: 0; transform: translateY(12px) scale(0.98); }
  to   { opacity: 1; transform: none; }
}

/* 9:16 like every build video and its poster (extract-posters.sh), so
   the frame does not move when the player replaces the poster. */
.bs-media {
  position: relative;
  width: 100%;
  aspect-ratio: 9 / 16;
  overflow: hidden;
  background: var(--page-text-primary);
}

/* In flow, block, 100% of the 9:16 box, the way .vt-video does it.
   Absolutely positioned, iOS Safari laid it out at its intrinsic size:
   tucked into the top-left corner, then overflowing the frame and
   stretching the card once metadata arrived. */
.bs-video {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
  background: var(--page-text-primary);
}

/* The frame as a play/pause surface once a story is on screen. */
.bs-tap {
  position: absolute;
  inset: 0;
  z-index: 1;
  display: grid;
  place-items: center;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

/* Sound, top right under the segments (muted, it spells itself out);
   speed sits alone at the bottom right. */
.bs-ctrls {
  position: absolute;
  right: var(--space-sm);
  top: calc(var(--space-sm) + 14px);
  z-index: 2;
  display: flex;
  gap: var(--space-xs);
}

.bs-ctrl {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  height: 32px;
  min-width: 32px;
  padding: 0 8px;
  border: 0;
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.45);
  color: var(--canvas-white);
  font-family: var(--font-body);
  font-size: var(--text-xs);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  cursor: pointer;
  -webkit-backdrop-filter: blur(4px);
  backdrop-filter: blur(4px);
}

.bs-speed {
  position: absolute;
  right: var(--space-sm);
  bottom: var(--space-sm);
  z-index: 2;
  min-width: 42px;
}

.bs-sound.is-muted {
  padding: 0 12px 0 10px;
}

/* A 44px touch target around each 32px pill. */
.bs-ctrl::after {
  content: '';
  position: absolute;
  inset: -6px;
}

.bs-poster {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  padding: 0;
  border: 0;
  background: none;
  overflow: hidden;
  cursor: pointer;
}

.bs-poster.is-still {
  cursor: default;
}

.bs-poster img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform var(--transition-slow);
}

/* Switching clips cross-fades the posters in the same box: the previous
   one stays underneath (BuildCard's Poster) until the new one has loaded
   and faded in over it. */
.bs-poster-img {
  position: absolute;
  inset: 0;
}

.bs-poster-img.is-top {
  opacity: 0;
  transition: transform var(--transition-slow), opacity var(--transition-slow);
}

.bs-poster-img.is-top.is-loaded {
  opacity: 1;
}

button.bs-poster::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, transparent 50%, color-mix(in srgb, var(--page-text-primary) 60%, transparent));
}

button.bs-poster:hover img {
  transform: scale(1.03);
}

.bs-play {
  position: absolute;
  z-index: 1;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
}

button.bs-poster:hover .bs-play,
button.bs-poster:focus-visible .bs-play {
  transform: translate(-50%, -50%) scale(1.08);
}

/* Several videos: a segment per video along the top (stories style)
   and ‹ › at the sides. The scrim behind the segments is the same literal
   rgba exception .vt-play-scrim makes for text over footage. */
.bs-segs {
  position: absolute;
  inset: 0 0 auto;
  z-index: 2;
  display: flex;
  gap: 3px;
  padding: var(--space-sm) var(--space-sm) var(--space-lg);
  background: linear-gradient(rgba(0, 0, 0, 0.35), transparent);
  pointer-events: none;
}

.bs-seg {
  position: relative;
  flex: 1;
  height: 3px;
  border-radius: 2px;
  overflow: hidden;
  background: rgba(255, 255, 255, 0.4);
}

/* Played segments are full, the playing one fills with the video
   (BuildCard sets scaleX from the player's timeupdate, a few times a
   second; the linear transition smooths the steps between them). */
.bs-seg-fill {
  position: absolute;
  inset: 0;
  background: var(--canvas-white);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 260ms linear;
}

.bs-nav {
  position: absolute;
  top: 50%;
  z-index: 2;
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  margin-top: -20px;
  border: 0;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.4);
  color: var(--canvas-white);
  cursor: pointer;
  -webkit-backdrop-filter: blur(4px);
  backdrop-filter: blur(4px);
  transition: background-color var(--transition-fast), transform var(--transition-fast);
}

/* A 44px touch target around the 40px disc. */
.bs-nav::after {
  content: '';
  position: absolute;
  inset: -4px;
}

.bs-nav:active {
  transform: scale(0.92);
}

@media (hover: hover) {
  .bs-nav:hover {
    background: rgba(0, 0, 0, 0.6);
  }
}

.bs-nav.is-prev {
  left: var(--space-sm);
}

.bs-nav.is-next {
  right: var(--space-sm);
}

.bs-duration {
  position: absolute;
  z-index: 1;
  right: var(--space-sm);
  bottom: var(--space-sm);
  padding: calc(var(--space-xs) / 2) var(--space-xs);
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--page-text-primary) 60%, transparent);
  color: var(--page-text-inverse);
  font-size: var(--text-xs);
  font-weight: 600;
}

.bs-card-body {
  padding: var(--space-lg);
}

.bs-card-place {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--space-sm);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-muted);
}

.bs-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  flex: none;
  padding: calc(var(--space-xs) / 2) var(--space-sm);
  border-radius: 999px;
  background: var(--brand-primary);
  color: var(--text-inverse);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.bs-chip.is-progress {
  background: var(--status-warning-bg);
  color: var(--status-warning);
}

.bs-chip.is-progress::before {
  content: '';
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
}

.bs-card-title {
  font-family: var(--font-display);
  font-weight: var(--font-display-weight);
  font-size: calc(var(--text-xl) * var(--font-display-scale));
  text-transform: var(--font-display-transform, none);
  letter-spacing: var(--font-display-tracking, normal);
  line-height: 1.2;
  color: var(--text-primary);
  margin: var(--space-sm) 0 0;
}

/* The stage strip, labelled "Build progress": the page's five "How it
   works" steps as a segmented track with a short name under each. A head
   line spells out how far the build has got ("Step 3 of 5", the step it
   is up to named in bold below, or "Finished" with a tick). Segments are filled up to that step; a step
   with footage carries a play glyph and is a button, and the step whose
   video is in the player is solid brand. Each whole column (segment and
   name) is the tap target, not the thin bar. */
.bs-caption {
  margin-top: var(--space-sm);
}

/* What the clip shows: a line or two, never more, so every card in the
   row keeps the same rhythm. */
.bs-caption-desc {
  margin: 0;
  font-size: var(--text-sm);
  line-height: 1.45;
  color: var(--text-secondary);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* When it was filmed: understated, on its own line. */
.bs-caption-date {
  margin: var(--space-xs) 0 0;
  font-size: var(--text-xs);
  letter-spacing: 0.04em;
  color: var(--text-muted);
}

/* ── Desktop: the nearest builds under the map ── */

.bs-near {
  margin-top: var(--space-lg);
}

.bs-near-head {
  margin-bottom: var(--space-sm);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-muted);
}

.bs-near-row {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-sm);
}

.bs-near-item {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  padding: var(--space-xs);
  border: 1px solid var(--canvas-border);
  border-radius: var(--radius-md);
  background: var(--canvas-white);
  font-family: var(--font-body);
  font-size: var(--text-xs);
  line-height: 1.35;
  color: var(--text-secondary);
  text-align: left;
  cursor: pointer;
  transition: border-color var(--transition-fast), transform var(--transition-fast), box-shadow var(--transition-fast);
}

.bs-near-item:hover {
  border-color: var(--brand-primary-light);
  box-shadow: var(--shadow-sm);
  transform: translateY(-1px);
}

.bs-near-item img {
  flex: none;
  width: 44px;
  height: 60px;
  border-radius: var(--radius-sm);
  object-fit: cover;
}

.bs-near-item strong {
  display: block;
  font-size: var(--text-sm);
  color: var(--text-primary);
}

/* The /consult thank-you's links to the tenant's other funnels. Their own
   card, not .option-card: that one's hover is the off-white of the band
   these sit on, so a touched card vanished into the section. These stay
   white and lift instead. */
.thx-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-md);
  max-width: 640px;
  margin: 0 auto;
  text-align: left;
}

/* Two up once a desktop-shaped preview can still be read at half width. */
@media (min-width: 560px) {
  .thx-grid:not(.is-single) {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

.thx-grid.is-single {
  max-width: 420px;
}

.thx-card {
  display: flex;
  flex-direction: column;
  overflow: hidden;
  background: var(--canvas-white);
  border: 1px solid var(--canvas-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  color: inherit;
  text-decoration: none;
  transition: transform var(--transition-base), box-shadow var(--transition-base), border-color var(--transition-base);
  -webkit-tap-highlight-color: transparent;
}

@media (hover: hover) {
  .thx-card:hover {
    transform: translateY(-2px);
    box-shadow: var(--shadow-lg);
    border-color: var(--brand-primary-light);
  }
}

.thx-card:active {
  transform: scale(0.98);
  border-color: var(--brand-primary);
}

/* The landing's desktop above-the-fold (1440x900, so 16:10), under a
   slim browser bar so it reads as a page, not a photo. */
.thx-card-shot {
  position: relative;
  display: block;
  aspect-ratio: 16 / 10;
  overflow: hidden;
  padding-top: 14px;
  border-bottom: 1px solid var(--canvas-border);
  background: var(--canvas-white);
}

.thx-card-shot::before {
  content: '';
  position: absolute;
  inset: 0 0 auto;
  height: 14px;
  border-bottom: 1px solid var(--canvas-border);
  background:
    radial-gradient(circle, var(--canvas-border) 2.5px, transparent 3px) 8px 50% / 10px 10px no-repeat,
    radial-gradient(circle, var(--canvas-border) 2.5px, transparent 3px) 19px 50% / 10px 10px no-repeat,
    radial-gradient(circle, var(--canvas-border) 2.5px, transparent 3px) 30px 50% / 10px 10px no-repeat,
    var(--canvas-off-white);
}

.thx-card-shot img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: top center;
}

.thx-card-body {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: var(--space-sm) var(--space-md) var(--space-md);
}

.thx-card-title {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-xs);
  font-weight: 600;
  color: var(--text-primary);
}

.thx-card-title svg {
  flex-shrink: 0;
  color: var(--brand-primary);
}

.thx-card-desc {
  font-size: var(--text-sm);
  line-height: 1.4;
  color: var(--text-secondary);
}

/* The /consult sample-build picker: one card per row on a phone, where
   full-width cards read at full size, then 2x2. The gap leaves room for
   the 'Most common' badge on a card's top edge. */
.consult-sample-grid {
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-md);
}

@media (min-width: 560px) {
  .consult-sample-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

/* "Most common": a pill on the card's top edge, as pricing tables do. */
.consult-sample-badge {
  position: absolute;
  top: 0;
  left: 50%;
  transform: translate(-50%, -50%);
  padding: 3px 10px;
  border-radius: 999px;
  background: var(--brand-primary);
  color: var(--text-inverse);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  white-space: nowrap;
}

.consult-sample-grid .option-card.has-badge {
  border-color: var(--brand-primary-light);
}

/* The selected card's check mark sits on its corner, half over the
   border like a badge: inside the card it ran over the centred names
   ("Backyard Classic"), and leaving room for it wrapped them. */
.consult-sample-grid .option-card.selected::after {
  top: -9px;
  right: -9px;
}

/* The fine print under the picker: a footnote, not a subhead. */
.consult-sample-note {
  max-width: 420px;
  margin: var(--space-md) auto 0;
  text-align: center;
  font-size: var(--text-xs);
  line-height: 1.5;
  color: var(--text-muted);
}

/* The /consult sample-build picker: the price range under the name. */
.consult-sample-range {
  margin: 2px 0 var(--space-xs);
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--brand-primary);
  font-variant-numeric: tabular-nums;
}

/* The showcase sits a layer above the section after it, so the phone
   row's card shadows, which reach past this section's bottom edge, paint
   over the next section's background instead of under it. */
.bs-section {
  position: relative;
  z-index: 1;
}

/* ── Phone: map, then every build as a swipeable row ── */

.bs-row {
  display: none;
}

@media (max-width: 640px) {
  .bs-layout {
    grid-template-columns: 1fr;
    gap: var(--space-md);
    margin-top: var(--space-xl);
  }

  .bm-control {
    width: 40px;
    height: 40px;
  }

  .bm-legend {
    gap: var(--space-xs) var(--space-md);
  }

  /* Bleeds to the screen edges so the neighbours peek in on both sides.
     Native momentum and a mandatory centred snap do the swiping; the
     script only reads where it came to rest (BuildShowcase). The side
     padding is exactly the peek, so the first and last cards centre like
     every other one and the peek never changes along the row.
     --bs-row-card: wide, with a sliver of each neighbour peeking. No
     height cap any more: on iOS, with Safari's bars, a cap tied to svh
     made every card narrow (Thomas: "a bit too narrow"). The card may
     run a little taller than the screen; width won. The row spans the
     viewport, so vw is its own width. */
  .bs-row {
    --bs-row-card: min(84vw, 360px);
    display: flex;
    gap: var(--space-md);
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    overscroll-behavior-x: contain;
    touch-action: pan-x pan-y;
    scrollbar-width: none;
    /* A sideways scroller clips everything at its own box, shadows
       included, so the box is padded past the reach of --shadow-lg
       (0 16px 40px: ~24px above a card, ~56px below), and that padding
       is taken back off the margins, so the layout is as if it weren't
       there: the cards end where any section's content ends. The shadow
       tail that now crosses into the next section paints above it,
       because .bs-section is lifted a layer (below). */
    --bs-row-pad-top: 28px;
    --bs-row-pad-bottom: 60px;
    margin: calc(var(--space-md) - (var(--bs-row-pad-top) - var(--space-xs))) calc(var(--space-md) * -1)
      calc(var(--bs-row-pad-bottom) * -1);
    padding: var(--bs-row-pad-top) calc((100vw - var(--bs-row-card)) / 2) var(--bs-row-pad-bottom);
    text-align: left;
  }

  /* The row scrolling itself to a tapped pin's card: snapping waits until
     it lands, so it doesn't pull every frame of the scroll to a card. */
  .bs-row.is-steering {
    scroll-snap-type: none;
  }

  .bs-row::-webkit-scrollbar {
    display: none;
  }

  /* min-width: 0 all the way down: a flex item's automatic minimum is its
     content's min-content width; Crawford's old row of eight nowrap clip
     chips (547px) made WebKit grow that one card past the rest. */
  .bs-row-item {
    position: relative;
    flex: 0 0 var(--bs-row-card);
    min-width: 0;
    scroll-snap-align: center;
  }

  .bs-row .bs-card,
  .bs-row .bs-card-body {
    min-width: 0;
    max-width: 100%;
  }

  /* The centred card sits forward of the rest: full size and a deeper
     shadow, the others at 95%. Both are driven by --focus (0–1), which
     BuildShowcase writes from the scroll position every frame, so the
     card comes forward WITH the swipe rather than after it settles. No
     transitions: the scroll is the animation. Transform and opacity
     only, so nothing re-lays out mid-gesture. */
  .bs-row-item::before {
    content: '';
    position: absolute;
    inset: 0;
    border-radius: var(--radius-xl);
    box-shadow: var(--shadow-lg);
    opacity: var(--focus, 0);
    transform: scale(calc(0.95 + 0.05 * var(--focus, 0)));
    pointer-events: none;
  }

  .bs-row .bs-card {
    position: relative;
    height: 100%;
    box-shadow: var(--shadow-sm);
    transform: scale(calc(0.95 + 0.05 * var(--focus, 1)));
  }

  .bs-row .bs-card-body {
    padding: var(--space-md);
  }

  .bs-row .bs-card-title {
    font-size: calc(var(--text-lg) * var(--font-display-scale));
  }

}

@media (prefers-reduced-motion: reduce) {
  .bm-pin.is-active .bm-pin-pulse {
    animation: none;
    opacity: 0.25;
    transform: scale(1.9);
  }

  .bm-svg.is-revealed:not(.is-entered) .bm-pin-drop,
  .bs-card.is-entering,
  .bm-chip {
    animation: none;
  }

  .bm-ground,
  .bm-pin-body,
  .bm-pin-halo,
  .bm-hint,
  .bm-control,
  .bs-poster img,
  .bs-seg-fill {
    transition: none;
  }
}

/* ─────────────────────────────────────────────────────────
   FEASIBILITY QUICK QUESTIONS  —  /feasibility step 3

   A tabbed card: a tinted header band carrying four tabs, and below it
   one question at a time on plain ground.

   The first pass filled the whole thing and gave it a brand-coloured
   left edge, borrowed from .reveal-panel. That edge means "this
   unrolled out of the card above it" — true in the estimator's
   cascades, false here, where nothing produced this panel. Filled and
   edged, it read as a heavy box sitting on the page for no reason. Now
   the fill is confined to the 44px tab band, so the frame reads as
   chrome around the question rather than as another surface, and the
   option tiles inside look like the tiles everywhere else in the funnel.

   The tab strip carries the progress, so its states have to be
   unmistakable at a glance: pending is an outlined number, active is
   brand-outlined with a sliding underline, done is a filled brand disc
   with a check that pops in. Done is deliberately the loudest of the
   three — watching three discs fill is the reason the step feels short.
   ───────────────────────────────────────────────────────── */

.quick-questions {
  width: 100%;
  border: 1px solid var(--canvas-border);
  border-radius: var(--radius-xl);
  overflow: hidden;
}

/* Equal columns rather than content width: it keeps the indicator's
   position pure CSS (index × 100% of one column), so it needs no
   measurement and survives a resize or a per-tenant type scale that
   changes every label's width. */
.qq-tabs {
  position: relative;
  display: grid;
  grid-template-columns: repeat(var(--qq-count), 1fr);
  background: var(--canvas-off-white);
  border-bottom: 1px solid var(--canvas-border);
}

.qq-tabs-indicator {
  position: absolute;
  left: 0;
  bottom: -1px;
  height: 2px;
  width: calc(100% / var(--qq-count));
  background: var(--brand-primary);
  transform: translateX(calc(var(--qq-index) * 100%));
  /* An explicit duration and curve rather than --transition-base: the
     slight overshoot is the whole point of the movement, and the
     transition tokens bundle their own easing. */
  transition: transform 320ms cubic-bezier(0.34, 1.35, 0.64, 1), opacity var(--transition-fast);
}

.qq-tabs-indicator.is-hidden {
  opacity: 0;
}

.qq-tab {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-sm);
  padding: var(--space-md) var(--space-sm);
  background: none;
  border: none;
  font-family: var(--font-body);
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--text-muted);
  cursor: pointer;
  transition: color var(--transition-fast), background var(--transition-fast);
  outline: none;
}

.qq-tab:hover {
  color: var(--text-secondary);
}

.qq-tab.is-active {
  background: var(--canvas-white);
  color: var(--text-primary);
}

.qq-tab.is-done {
  color: var(--text-secondary);
}

.qq-tab:focus-visible {
  outline: 2px solid var(--brand-primary);
  outline-offset: -2px;
}

.qq-tab-mark {
  display: grid;
  place-items: center;
  flex-shrink: 0;
  width: 20px;
  height: 20px;
  border: 1.5px solid var(--canvas-border);
  border-radius: 50%;
  font-size: var(--text-xs);
  font-weight: 600;
  color: var(--text-muted);
  transition: all var(--transition-fast);
}

.qq-tab.is-active .qq-tab-mark {
  border-color: var(--brand-primary);
  color: var(--brand-primary);
}

.qq-tab.is-done .qq-tab-mark {
  background: var(--brand-primary);
  border-color: var(--brand-primary);
  color: var(--text-inverse);
}

.qq-tab-check {
  width: 10px;
  height: 8px;
  animation: qq-pop var(--transition-base) cubic-bezier(0.34, 1.56, 0.64, 1);
}

.qq-tab-label {
  white-space: nowrap;
}

/* Height is set inline from the panel's measured content so a
   four-option question swapping for a five-option one glides. */
.qq-body {
  overflow: hidden;
  transition: height 300ms cubic-bezier(0.4, 0, 0.2, 1);
}

.qq-panel {
  padding: var(--space-lg);
  animation: qq-enter var(--transition-base) ease-out;
}

.qq-fieldset {
  border: none;
  padding: 0;
  margin: 0;
  width: 100%;
}

.qq-legend {
  display: block;
  width: 100%;
  padding: 0;
  margin-bottom: var(--space-md);
  font-size: var(--text-md);
  font-weight: 500;
  line-height: 1.4;
  color: var(--text-primary);
}

.qq-options {
  display: grid;
  gap: var(--space-sm);
  width: 100%;
}

/* Icon, heading, description — the .time-slot-card shape from the
   booking step, on the .option-card base so selection styling stays in
   one place. The description is what lets someone answer without going
   outside to measure, so it is load-bearing rather than decorative;
   DESIGN_SYSTEM.md's "no icons on selection cards" is knowingly set
   aside here for the same reason it is on the booking step's slots. */
.qq-option {
  flex-direction: row;
  align-items: center;
  gap: var(--space-md);
  /* Right padding clears the selected badge below, so a two-line
     description never runs underneath it. */
  padding: var(--space-md) var(--space-xl) var(--space-md) var(--space-md);
}

/* .option-card puts its check in the top-right corner, which is right
   for a tall card and wrong for a row — here it centres against the
   icon and the heading instead. */
.qq-option.selected::after {
  top: 50%;
  right: var(--space-md);
  transform: translateY(-50%);
}

/* Multi-select questions have to read as multi-select BEFORE anyone has
   tried tapping two tiles, so their mark is drawn empty rather than
   waiting on .selected, and it is squared off — round means "one of
   these", square means "as many as apply", and that convention is worth
   more here than novelty. Same 18px box in the same place, so a single-
   and a multi-question panel still line up. */
.qq-option.is-multi::after {
  content: '';
  position: absolute;
  top: 50%;
  right: var(--space-md);
  transform: translateY(-50%);
  width: 18px;
  height: 18px;
  border: 1.5px solid var(--canvas-border);
  border-radius: var(--radius-sm);
  /* background-color, not the background shorthand: the check glyph
     arrives as a background-image from .option-card.selected::after and
     must survive this rule, which sits later in the file at equal
     specificity. */
  background-color: var(--canvas-white);
  transition: border-color var(--transition-fast), background-color var(--transition-fast);
}

.qq-option.is-multi.selected::after {
  border-color: var(--brand-primary);
  background-color: var(--brand-primary);
}

/* The native control stays in the accessibility tree and keeps the
   label association; the card's own selected state is the visible
   affordance. Not display:none — that would take it out of the tree. */
.qq-option-input {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

.qq-option:focus-within {
  border-color: var(--brand-primary);
  box-shadow: var(--shadow-sm);
}

.qq-option-icon {
  display: grid;
  place-items: center;
  flex-shrink: 0;
  width: 40px;
  height: 40px;
  background: var(--canvas-off-white);
  border-radius: var(--radius-md);
  color: var(--text-secondary);
  transition: all var(--transition-fast);
}

.qq-option.selected .qq-option-icon {
  background: var(--brand-primary-light);
  color: var(--brand-primary);
}

.qq-option-text {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

/* Tighter than the stacked cards this class was written for: two rows
   sitting against a 40px icon want to read as one block. */
.qq-option .option-card-desc {
  margin-top: 0;
}

.qq-footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-top: var(--space-lg);
}

.qq-link {
  padding: 0;
  background: none;
  border: none;
  font-family: var(--font-body);
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--text-muted);
  cursor: pointer;
  transition: color var(--transition-fast);
}

.qq-link:hover {
  color: var(--brand-primary);
}

.qq-advance {
  padding: var(--space-sm) var(--space-md);
  background: var(--canvas-white);
  border: 1.5px solid var(--brand-primary);
  border-radius: var(--radius-md);
  font-family: var(--font-body);
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--brand-primary);
  cursor: pointer;
  transition: all var(--transition-fast);
}

.qq-advance:hover {
  background: var(--brand-primary);
  color: var(--text-inverse);
}

/* Closing state. Not a dead end — the copy points back at the tabs,
   which stay live, because every answer here is optional and changing
   one has to stay cheap. */
.qq-complete {
  text-align: center;
  padding: var(--space-xl) var(--space-lg);
}

.qq-complete-mark {
  display: inline-grid;
  place-items: center;
  width: 32px;
  height: 32px;
  margin-bottom: var(--space-sm);
  background: var(--brand-primary);
  border-radius: 50%;
  color: var(--text-inverse);
}

.qq-complete-mark .qq-tab-check {
  width: 14px;
  height: 11px;
}

.qq-complete-title {
  font-size: var(--text-md);
  font-weight: 500;
  color: var(--text-primary);
  margin-bottom: var(--space-xs);
}

.qq-complete-body {
  font-size: var(--text-sm);
  font-weight: 300;
  color: var(--text-secondary);
}

@keyframes qq-enter {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: translateY(0); }
}

@keyframes qq-pop {
  from { transform: scale(0.4); opacity: 0; }
  to   { transform: scale(1); opacity: 1; }
}

@media (max-width: 640px) {
  /* Four labels plus four marks across 390px. The label is what gives
     up room first: it drops to the smallest step and the gap closes,
     which keeps "Utilities" on one line without a scrolling strip. */
  .qq-tab {
    gap: var(--space-xs);
    padding: var(--space-sm) var(--space-xs);
    font-size: var(--text-xs);
  }

  .qq-tab-mark {
    width: 18px;
    height: 18px;
  }

  .qq-panel {
    padding: var(--space-md);
  }

  .qq-option {
    gap: var(--space-sm);
  }

  .qq-option-icon {
    width: 36px;
    height: 36px;
  }

  .qq-complete {
    padding: var(--space-lg) var(--space-md);
  }
}

@media (prefers-reduced-motion: reduce) {
  .flip-face {
    transition: none;
  }

  .qq-tabs-indicator,
  .qq-body,
  .qq-tab-mark,
  .qq-option-icon,
  .qq-option.is-multi::after {
    transition: none;
  }

  .qq-panel,
  .qq-tab-check {
    animation: none;
  }
}

/* ═══════════════════════════════════════════════════════════
   FEASIBILITY REPORT  —  /report/[token]

   A read-and-print document, not a funnel step, so it sets its own
   measure rather than reusing .page-content's centred column. Print
   rules matter as much as screen ones here: the whole point of the
   page is that someone prints it and takes it to a designer.
   ═══════════════════════════════════════════════════════════ */

/* Full width so sections can band the whole viewport; the 780px measure
   moves to .report-inner, which every section carries. Same shape as
   .page-section / .page-section-inner on the landing pages. */
.report {
  width: 100%;
  padding: 0 0 var(--space-3xl);
}

.report-inner {
  max-width: 780px;
  margin: 0 auto;
  padding: 0 var(--space-lg);
}

/* Banding is derived from render order rather than assigned per section.
   Four of the sections inside this wrapper are conditional — no settled
   signals, no findings, nobody to call, no snapshot rows — so a hand-set
   background is a guess about what precedes it, and guessing wrong puts
   two off-white bands back to back. nth-of-type counts only the sections
   that actually rendered, so the stripe re-derives whenever one drops
   out. Odd is off-white because the sections above this wrapper are
   white. The rule itself lives with .page-section's under PAGE SECTIONS:
   the two are one surface and take one palette. */

/* The score section. It opens the page, so it carries less top padding
   than a section mid-scroll: .report-section's 64px above an eyebrow
   that is the first thing under the site header reads as a gap rather
   than a start. This is the top padding the old .report-header wrapper
   supplied, kept now that the wrapper is gone and the score has moved
   down into the settled section.

   Compounded with .report-section deliberately: that rule sets `padding`
   as a shorthand further down this file, and at equal specificity the
   later shorthand would put the 64px straight back. */
.report-section.report-score-section {
  padding-top: var(--space-xl);
}

/* The score and the property card are two objects of the same section,
   so they sit closer to each other than either does to the section
   head above them (.report-section-head's --space-xl). Adjacent-sibling
   rather than a margin on the panel: the card drops out entirely on a
   report with no settled signals, and a trailing margin under the score
   would then push the band below it out of rhythm. */
/* Keyed on the score panel not being first, rather than on whatever
   happens to precede it. The previous form was `.estimate-card +
   .score-panel`, which silently stopped matching the moment that card
   was wrapped in .flip — the gap did not shrink, it ceased to exist,
   and the two panels came out touching. An adjacent-sibling selector
   naming the thing before it is a rule that breaks on the next
   rewrap; this one does not care what the sibling is.

   It collapses with .report-section-head's own bottom margin, so a
   report with no property card gets the same 40px rather than 64. */
/* Separates the score from the property card above it.
   
   The :not(:first-child) still covers the flat case, where the panel is a
   direct child of report-inner. Inside a flip it is the first child of its
   own face, so the selector stopped matching and the two cards butted
   together — the margin belongs to the flip container there. */
.report-score-section .score-panel:not(:first-child),
.report-score-section .score-flip {
  margin-top: var(--space-xl);
}

/* A progress bar was tried here and was the clearer mistake: filling
   toward 100 says "you are most of the way there", which is not what a
   feasibility score means — it is a read on how much of the site is
   already understood, not progress toward a goal. Do not bring it back.

   What the score did need was a counterweight. Alone in a centred
   column it was a big number over a lot of nothing; paired with the
   summary it has something to sit against and the panel reads as one
   object rather than three sparse ones. */
.score-panel {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: center;
  gap: var(--space-xl);
  padding: var(--space-xl);
  border-radius: var(--radius-xl);
  /* --brand-primary rather than --brand-accent: this and the
     consultation CTA are the two filled panels on the page and they have
     to be the same colour. On tenants where accent and primary differ,
     matching the CTA matters more than the name of the token. */
  background: var(--brand-primary);
  color: var(--text-inverse);
  text-align: left;
}

/* The narrator can return a report with no verdict. Rather than leave
   half the panel empty, the score takes the width and re-centres. */
.score-panel.is-solo {
  grid-template-columns: 1fr;
  justify-items: center;
  text-align: center;
}

.score-figure {
  display: flex;
  align-items: baseline;
  gap: var(--space-xs);
  padding-right: var(--space-xl);
  border-right: 1px solid color-mix(in srgb, var(--text-inverse) 25%, transparent);
  font-family: var(--font-display);
  text-transform: var(--font-display-transform, none);
  letter-spacing: var(--font-display-tracking, normal);
  font-weight: var(--font-display-weight);
  line-height: 1;
}

.score-panel.is-solo .score-figure {
  padding-right: 0;
  border-right: none;
}

/* Well past --text-hero: the number is the reason the page exists, not
   merely the largest text on it. Numerals at display size need the
   tracking pulled in, or the digits read as separated. */
.report-score-value {
  font-size: calc(var(--text-hero) * var(--font-display-scale) * 1.8);
  letter-spacing: -0.03em;
  color: var(--text-inverse);
}

.report-score-max {
  font-size: var(--text-xl);
  color: var(--text-inverse);
  opacity: 0.6;
}

/* The consultation card's pairing — display face over light body copy —
   one step down in size. An uppercase brand eyebrow was tried here and
   fought the number for attention; a quiet headline sits under it
   instead. */
.score-summary-title {
  margin: 0 0 var(--space-sm);
  font-family: var(--font-display);
  text-transform: var(--font-display-transform, none);
  letter-spacing: var(--font-display-tracking, normal);
  font-size: calc(var(--text-lg) * var(--font-display-scale));
  font-weight: var(--font-display-weight);
  line-height: 1.2;
  color: var(--text-inverse);
}

.score-summary p {
  margin: 0;
  font-size: var(--text-base);
  font-weight: 300;
  line-height: 1.6;
  color: var(--text-inverse);
  opacity: 0.85;
}

/* The estimator's CTA is a <button> that opens a picker; on the report
   it is a link to the booking anchor, so it needs the bits a button got
   for free. */
a.consultation-btn {
  display: inline-block;
  text-decoration: none;
}


/* Padding, not margin: the band's ground has to reach the edges of the
   section, and a margin would leave white gutters between stripes. */
.report-section {
  padding: var(--space-2xl) 0;
}

/* The estimator reveal page's section header, brought over to the
   report: centred eyebrow above a display-face headline, with the
   section's own container beneath. .headline-section supplies the face,
   size and centring — this only has to group them and clear the
   document-style margin. */
.report-section-head {
  text-align: center;
  margin-bottom: var(--space-xl);
}

/* margin-block, NOT the margin shorthand. .headline-section centres
   itself with max-width:560px plus auto side margins, so a blanket
   `margin: 0` pins that 560px box to the left edge — the text then
   centres inside the box rather than inside the section, and lands
   ~85px left of everything else. Only the block axis is ours to clear. */
.report-section-head h2 {
  margin-block: 0;
}

/* .subheadline carries no measure of its own — the landing pages cap it
   inline at each call site. Left uncapped in the report's 732px column
   it runs as one sprawling line under a headline that wraps at 560,
   which is what made the two look unrelated. Capped here once rather
   than per section.

   margin-left/right, never the `margin` shorthand: the shorthand would
   zero the block margins these paragraphs get from context. */
.report .subheadline {
  max-width: 560px;
  margin-left: auto;
  margin-right: auto;
}

.report-section ul {
  padding-left: var(--space-lg);
  line-height: 1.7;
  color: var(--text-primary);
}

/* One card per scored deduction. */
/* The finding itself — icon, title, and what we found — always visible.
   Everything below it tabs. See components/report/FindingCard.tsx for
   why the dt/dd ladder went away. */
.finding-head {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  margin-bottom: var(--space-md);
}

/* Deliberately neutral rather than warning-tinted. Six amber discs down
   a report whose whole argument is "a hard site is not a dead end"
   would undercut the copy. */
.finding-icon {
  display: grid;
  place-items: center;
  flex-shrink: 0;
  width: 40px;
  height: 40px;
  background: var(--canvas-off-white);
  border-radius: var(--radius-md);
  color: var(--text-secondary);
}

.finding-head h3 {
  font-size: var(--text-md);
  margin: 0;
}

.finding-lead {
  margin: 0;
  font-size: var(--text-base);
  line-height: 1.6;
  color: var(--text-primary);
}

/* One bounded unit holding every finding, tabs down the left.
   Rail and panel share grid row 1, so the unit is as tall as whichever
   is taller and the shorter one simply carries white space — which is
   the whole reason the rail is a column. Across the top it had to wrap
   once the narrator returned more than about seven cards, and a second
   row of tabs reads as a mistake; a column just gets longer. */
/* Boundless, to match the calls list below it. This was a bordered card
   with a filled rail, which on a tinted band came out as three nested
   surfaces — band, card, rail — for content that needs none of them.
   What structure it needs is one hairline between the index and the
   text, the same job the connector does in the calls gutter. */
.findings {
  display: grid;
  grid-template-columns: minmax(0, 180px) minmax(0, 1fr);
  gap: var(--space-xl);
}

.findings-tabs {
  grid-column: 1;
  grid-row: 1;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  border-right: 1px solid var(--canvas-border);
}

.findings-tab {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  padding: var(--space-sm) var(--space-md) var(--space-sm) 0;
  background: none;
  border: none;
  font-family: var(--font-body);
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--text-muted);
  text-align: left;
  cursor: pointer;
  transition: color var(--transition-fast);
  outline: none;
}

.findings-tab svg {
  flex-shrink: 0;
}

.findings-tab:hover {
  color: var(--text-secondary);
}

/* The brand bar sits on the tab's inner edge, directly against the rail
   hairline, so the marker reads as part of the same rule rather than as
   a second line beside it. */
.findings-tab.is-active {
  color: var(--brand-primary);
  box-shadow: inset -2px 0 0 var(--brand-primary);
}

.findings-tab:focus-visible {
  outline: 2px solid var(--brand-primary);
  outline-offset: -2px;
}

.findings-tab-label {
  line-height: 1.3;
}


.finding-panel {
  grid-column: 2;
  grid-row: 1;
  display: none;
  animation: qq-enter var(--transition-base) ease-out;
}

.finding-panel.is-active {
  display: block;
}

/* Supporting material under the finding. Tinted blocks were tried here
   and came out — a panel inside a panel inside a band is one nesting
   level too many, and the boxes were doing work the eyebrow labels
   already do. Plain text, separated by space alone. */
.finding-detail {
  margin: var(--space-lg) 0 0;
}

/* The wrapper stays in the markup — dl > div > (dt, dd) is what keeps
   the description-list semantics — but carries no box of its own now,
   only the space between one part and the next. Not a grid with a gap:
   that would space the label from its own paragraph as widely as it
   spaces one part from another. */
.finding-block + .finding-block {
  margin-top: var(--space-lg);
}

/* These labels are eyebrows, and DESIGN_SYSTEM.md gives eyebrows
   --brand-accent. The lucide icons stroke on currentColor, so colouring
   the dt carries the icon with it. */
.finding-detail dt {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  margin: 0;
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--brand-accent);
}

.finding-detail dt svg {
  flex-shrink: 0;
}

.finding-detail dd {
  margin: var(--space-xs) 0 0;
  font-size: var(--text-base);
  line-height: 1.6;
  color: var(--text-primary);
}

/* Per-card provenance. This line is what separates a sourced finding
   from a generated blurb, so it stays visible rather than tucked away. */
.finding-source {
  margin: var(--space-md) 0 0;
  padding-top: var(--space-sm);
  border-top: 1px solid var(--canvas-border);
  font-size: var(--text-xs);
  color: var(--text-muted);
}


/* Framed and capped like every other section's content, rather than a
   bare table running the full column. The measure matters more here than
   elsewhere: label/value pairs stretched across 732px leave the eye to
   cross a gap of whitespace to pair them up. */
.snapshot-table {
  width: 100%;
  max-width: 620px;
  margin: 0 auto;
  border: 1px solid var(--canvas-border);
  border-radius: var(--radius-xl);
  border-collapse: separate;
  border-spacing: 0;
  overflow: hidden;
  background: var(--canvas-white);
  font-size: var(--text-sm);
}

.snapshot-table th,
.snapshot-table td {
  text-align: left;
  padding: var(--space-md) var(--space-lg);
  border-bottom: 1px solid var(--canvas-border);
  vertical-align: top;
}

/* Rounded corners plus a trailing rule reads as a dropped line. */
.snapshot-table tr:last-child th,
.snapshot-table tr:last-child td {
  border-bottom: none;
}

.snapshot-table th {
  width: 42%;
  font-weight: 400;
  color: var(--text-secondary);
  border-right: 1px solid var(--canvas-border);
}

.snapshot-table td {
  color: var(--text-primary);
  font-weight: 500;
}

/* No longer a panel of its own. It is the last band in the stripe and
   takes the fall special's closing shape — eyebrow, headline, one line,
   two actions at equal weight — so it reads as the end of the document
   rather than as a box tacked to the bottom of it. */
.report-cta {
  text-align: center;
}

/* Taking the report away is a document action, so it lives with the
   document — under the score, above the disclaimer — rather than beside
   the booking button, where it read as a competing offer. Quiet on
   purpose: it is a utility, not a second call to action. */
.report-doc-actions {
  display: flex;
  justify-content: center;
  margin-top: var(--space-lg);
}

.report-pdf-btn {
  display: inline-flex;
  align-items: center;
  gap: var(--space-sm);
  padding: var(--space-sm) var(--space-lg);
  border: 1px solid var(--canvas-border);
  border-radius: var(--radius-md);
  background: var(--canvas-white);
  font-family: var(--font-body);
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--text-secondary);
  cursor: pointer;
  text-decoration: none; /* Rails: it is a link to the PDF now, not a button */
  transition: all var(--transition-fast);
}

.report-pdf-btn:hover {
  border-color: var(--brand-primary);
  color: var(--brand-primary);
}




/* Sits under the score it qualifies. Muted and capped — it is a
   disclaimer, not a paragraph of the report, but it has to be legible
   enough that nobody can say it was hidden. */
.report-disclaimer {
  max-width: 620px;
  margin: var(--space-lg) auto 0;
  font-size: var(--text-xs);
  line-height: 1.6;
  color: var(--text-muted);
  text-align: center;
}

@media (max-width: 600px) {
  .report { padding: 0 0 var(--space-2xl); }
  .report-inner { padding: 0 var(--space-md); }
  .report-section.report-score-section { padding-top: var(--space-lg); }
  .report-section { padding: var(--space-xl) 0; }
  .report-score-number { font-size: var(--text-3xl); }
  .snapshot-table th { width: 50%; }
  .snapshot-table th,
  .snapshot-table td { padding: var(--space-sm) var(--space-md); }

  /* Three tabs of real words do not fit across a phone on one line, and
     truncating them costs the meaning. Stacking the icon over the label
     buys the width back without abbreviating anything. */
  /* The rail stays a rail. Laying it down into a horizontal scroller
     pushed tabs off the side of the viewport, which hides findings
     behind a gesture people miss — the same objection that ruled out
     wrapping on desktop. Narrower column, icon over label, and the
     labels wrap rather than the row scrolling. */
  .findings {
    grid-template-columns: 72px minmax(0, 1fr);
    gap: var(--space-md);
  }

  .findings-tab {
    flex-direction: column;
    align-items: center;
    gap: var(--space-xs);
    padding: var(--space-sm) var(--space-xs) var(--space-sm) 0;
    font-size: var(--text-xs);
    line-height: 1.2;
    text-align: center;
  }

  /* Sized down from the desktop 20px here rather than by prop, so the
     rail can be narrow without the icon setting a floor on it. */
  .findings-tab svg {
    width: 18px;
    height: 18px;
  }

  .findings-tab-label {
    /* Long signal names — "Water table", "Rear yard" — take two lines
       here rather than being truncated. */
    overflow-wrap: anywhere;
  }

  /* 1.8x hero overruns 358px once a tenant's display scale is above 1.
     Stacked, with the rule moving from the score's right edge to its
     underside. */
  .score-panel {
    grid-template-columns: 1fr;
    gap: var(--space-lg);
    padding: var(--space-lg);
  }

  .report-score-value {
    font-size: calc(var(--text-hero) * var(--font-display-scale) * 1.3);
  }

  .score-figure {
    justify-content: center;
    padding: 0 0 var(--space-lg);
    border-right: none;
    border-bottom: 1px solid color-mix(in srgb, var(--text-inverse) 25%, transparent);
  }


  .finding-head {
    gap: var(--space-sm);
  }

  .finding-icon {
    width: 36px;
    height: 36px;
  }
}

/* Print: this page is meant to leave the screen. Drop the interactive
   chrome, flatten the CTA panel, and keep cards off page breaks. */
@media print {
  .report { max-width: none; padding: 0; }
  .report-inner { max-width: none; padding: 0; }
  .report-bands > section { background: none; }
  .report-cta { padding: var(--space-md) 0; }
  .report-doc-actions { display: none; }

  /* Backgrounds drop when printing, so the brand panel would print as
     white text on white paper. Flattened, it still carries the ask. */
  .consultation-card {
    background: none;
    padding: 0;
    text-align: left;
  }
  .consultation-card-headline { color: var(--text-primary); }
  .consultation-card-sub { color: var(--text-secondary); opacity: 1; }
  .consultation-btn { display: none; }

  /* Backgrounds drop when printing, so a filled panel would print as
     white text on white paper — and this panel carries the score. It
     reverts to the outlined version on paper. */
  .score-panel {
    background: none;
    border: 1px solid var(--canvas-border);
    color: var(--text-primary);
  }
  .report-score-value { color: var(--brand-primary); }
  .report-score-max { color: var(--text-muted); opacity: 1; }
  .score-summary-title { color: var(--text-primary); }
  .score-summary p { color: var(--text-secondary); opacity: 1; }
  .score-figure { border-right-color: var(--canvas-border); }

  /* Paper has no other side. Both faces print, stacked, and the control
     that turned the card over goes. */
  .flip {
    display: block;
    perspective: none;
  }
  .flip-face {
    transform: none !important;
    backface-visibility: visible;
  }
  .flip-back {
    margin-top: var(--space-lg);
  }
  .flip-notch {
    display: none;
  }

  .finding-panel { break-inside: avoid; page-break-inside: avoid; }

  /* Tabs are a screen affordance and paper cannot click. Every panel is
     in the DOM at all times precisely so this can un-hide the lot, each
     under the label its tab was carrying. Without these three rules the
     printed report silently loses two thirds of every finding. */
  .findings { display: block; }
  .findings-tabs { display: none; }
  .finding-panel { display: block; animation: none; }
  .finding-detail { gap: var(--space-md); }
  .finding-panel + .finding-panel { margin-top: var(--space-xl); }
}


/* ─────────────────────────────────────────────────────────
   PDF THUMBNAIL

   Page one of an uploaded document, rendered client-side. Sits in the
   filename chip where a generic file icon used to be — the icon told
   the homeowner nothing, and confirming they picked the survey rather
   than the deed is the one check only they can perform.
   ───────────────────────────────────────────────────────── */

.pdf-thumb {
  width: 48px;
  height: 62px;
  flex-shrink: 0;
  object-fit: cover;
  object-position: top center;
  background: var(--canvas-white);
  border: 1px solid var(--canvas-border);
  border-radius: 3px;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}

/* Occupies the same box while pdf.js loads, so the chip does not jump. */
.pdf-thumb-loading {
  background: linear-gradient(
    90deg,
    var(--canvas-light-gray) 25%,
    var(--canvas-off-white) 50%,
    var(--canvas-light-gray) 75%
  );
  background-size: 200% 100%;
  animation: skeleton-shimmer 1.5s infinite;
}

/* ─────────────────────────────────────────────────────────
   UPLOAD QUEUE

   One row per file in a multi-file drop. Exists because the previous
   single "Uploading…" gave a six-photo batch no way to distinguish slow
   from stuck, and no way to see which file of the six had failed.
   ───────────────────────────────────────────────────────── */

.upload-queue {
  list-style: none;
  margin: var(--space-sm) 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.upload-queue-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  padding: var(--space-xs) var(--space-sm);
  border-radius: 4px;
  font-size: var(--text-xs);
  background: var(--canvas-off-white);
}

/* Long phone filenames must not push the status off the row. */
.upload-queue-name {
  color: var(--text-secondary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}

.upload-queue-status {
  flex-shrink: 0;
  color: var(--text-muted);
}

.upload-queue-item.uploading .upload-queue-status {
  color: var(--brand-primary);
}

.upload-queue-item.done .upload-queue-status {
  color: #2E7D5B;
}

.upload-queue-item.failed {
  background: var(--status-error-bg, #FDF2F2);
}

.upload-queue-item.failed .upload-queue-status {
  color: var(--status-error, #B4241F);
  white-space: normal;
}

/* ─────────────────────────────────────────────────────────
   REPORT — WHAT THE DATA SETTLES

   The report used to be structurally incapable of good news: a signal
   that cost no points produced no card. This section is the answer to
   that, and it must not be mistaken for another list of problems — so
   it borrows the estimator's .estimate-card whole, with the property
   standing where the pool model stands and each settled signal filling
   an option cell. Reusing the card someone has already seen priced is
   the point: it reads as an inventory of what you have, not as findings.
   ───────────────────────────────────────────────────────── */

/* The settled card and the property snapshot are two faces of one card.
   Both faces sit in the SAME grid cell, so the container is as tall as
   the taller of the two and turning it over cannot make the page jump —
   the alternative is measuring both and animating a height, which fights
   the rotation for the same 600ms.

   Each face rotates on its own rather than a shared inner wrapper: it is
   one transform per element and no third div in the markup. */
.flip {
  display: grid;
  perspective: 1600px;
}

.flip-face {
  grid-area: 1 / 1;
  backface-visibility: hidden;
  transition: transform 600ms cubic-bezier(0.4, 0.1, 0.2, 1);
}

/* Both faces stretch to the grid row, which is as tall as the taller of
   them, and each card fills its face. That gives one elastic child per
   side something to grow into: the aerial on the front, the notch's own
   top margin on the back. */
.flip-face > .estimate-card {
  height: 100%;
  display: flex;
  flex-direction: column;
}

.flip-back .flip-notch {
  margin-top: auto;
}

.flip-back {
  transform: rotateY(180deg);
}

/* Front leaves anticlockwise, back arrives clockwise — turning both the
   same way makes the card look like it passes through itself. */
.flip.is-flipped .flip-front {
  transform: rotateY(-180deg);
}

.flip.is-flipped .flip-back {
  transform: rotateY(0);
}

/* Closes the card, in the slot the estimator puts its price total in.
   Part of the face rather than a control sitting under the card, so it
   turns over with everything else — a button that stays put while the
   thing it acts on rotates reads as belonging to the page instead of to
   the card. */
.flip-notch {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-sm);
  width: 100%;
  padding: var(--space-md);
  border: none;
  border-top: 1px solid var(--canvas-border);
  background: var(--canvas-off-white);
  font-family: var(--font-body);
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--text-secondary);
  cursor: pointer;
  transition: background var(--transition-fast), color var(--transition-fast);
}

.flip-notch:hover {
  background: color-mix(in srgb, var(--brand-accent) 8%, transparent);
  color: var(--brand-primary);
}

.flip-notch svg {
  flex-shrink: 0;
  color: var(--brand-accent);
  transition: transform var(--transition-base);
}

.flip-notch:hover svg {
  transform: rotate(180deg);
}

.flip-notch:focus-visible {
  outline: 2px solid var(--brand-primary);
  outline-offset: -2px;
}

/* Inside the card the table supplies rows, not a frame — the card is
   already the frame. */
.estimate-card .snapshot-table {
  max-width: none;
  margin: 0;
  border: none;
  border-radius: 0;
}

/* Close the last row.
   
   The standalone table draws a border around itself and clears the final
   row's underline so the two do not double up. Inside the card the frame
   is gone, so that cleared underline was the table's only bottom edge —
   fine while the rows ran flush into the flip control, which carries its
   own top border, and wrong now that the aerial's floor makes this the
   shorter face. The figures ended in mid-air above the dead space.
   
   Restored here only, where the frame is absent. The two strokes can only
   coincide on a card with no space left over, which the taller front face
   makes vanishingly rare, and costs 1px if it happens. */
.estimate-card .snapshot-table tr:last-child td,
.estimate-card .snapshot-table tr:last-child th {
  border-bottom: 1px solid var(--canvas-border);
}

/* An odd number of settled signals otherwise leaves the last row half
   empty; the last one spans the row instead. */
.settled-grid .estimate-feature-cell:last-child:nth-child(odd) {
  grid-column: 1 / -1;
  border-right: 0;
}

/* Light positive marking. The tick gets its own column rather than
   sitting inside the label line: inline, it read as a bullet that had
   drifted into the text, and the description below started at a
   different left edge from the title above it. Given a column, both
   lines share one text edge and the mark reads as deliberate.

   Still the only coloured thing in the cell — tinting the whole cell
   green as well read as a status banner, and these are not alerts, they
   are the things already gone right. */
.settled-cell {
  display: flex;
  /* Top, not centre. These began as short two-column bites where centring
     looked right; they are now full-width rows of two or three lines, and
     a centred 24px mark floats in the middle of a tall cell with nothing
     to relate to. Aligned to the top it reads as a tick against the line
     it belongs to. */
  align-items: flex-start;
  gap: var(--space-md);
  padding-block: var(--space-lg);
}

/* Optically on the first line rather than flush with its box top. */
.settled-mark {
  margin-top: 1px;
}

/* The estimator's sizes are for a dense two-column feature grid. This
   grid is one column and the detail line now carries a measured figure,
   so both steps up: the heading to the body size, the detail to what the
   rest of the report calls small rather than fine print. */
.settled-cell .estimate-feature-name {
  font-size: var(--text-base);
  line-height: 1.4;
}

.settled-cell .estimate-feature-desc {
  font-size: var(--text-sm);
  line-height: 1.5;
  margin-top: 4px;
}

.settled-mark {
  display: grid;
  place-items: center;
  flex-shrink: 0;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  background: color-mix(in srgb, var(--status-success) 12%, transparent);
  color: var(--status-success);
}

/* min-width:0 so a long unbroken title wraps inside the cell instead of
   forcing the grid column wider. */
.settled-text {
  min-width: 0;
}

/* Closes the card where the estimator puts its price total. Cropped
   rather than shown square: this is the property as a frame on the card,
   not a map to read — the funnel's reveal step already gave them the
   full aerial.

   No fixed aspect ratio and no minimum. It is the elastic part of the
   front face, so it absorbs whatever height the back face needs and the
   two sides come out level without either being measured.

   `flex-basis: 0` with `min-height: 0` matters: with an intrinsic basis
   the aerial contributed its own height to the front's min-content, the
   front became the taller face, and the back stretched to match — which
   put a field of white space under the measurements, below content that
   had already ended. Contributing nothing, it lets the measurements set
   the height and takes what is left. */
/* The aerial absorbs whatever height the card has left over, which worked
   while the snapshot face was the taller of the two. The settled rows are
   bigger now and the front face outgrew the back, leaving nothing to
   absorb — `flex: 1 1 0` collapsed the image to a sliver.
   
   A floor fixes it. The image is the one part of this card that is about
   THEIR property rather than about the data, and it has to be big enough
   to recognise a garden in. It still grows to fill a taller card; it can
   no longer shrink below something worth looking at. Whichever face ends
   up shorter gets dead space at the bottom, which is the right trade —
   both faces share one grid cell, so they must match in height or the
   flip jumps. */
.settled-aerial {
  /* basis 0, NOT auto. With `auto` the base size is the image's intrinsic
     height — 640px at card width — so it balloons and min-height never
     applies. Basis 0 means it only ever absorbs space the card has left
     over, and min-height is then the floor it cannot shrink past. The two
     together are the whole dial: 280 minimum, grows to fill. */
  flex: 1 1 0;
  min-height: 280px;
  overflow: hidden;
  border-top: 1px solid var(--canvas-border);
  background: var(--canvas-light-gray);
}

.settled-aerial img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}


/* ─────────────────────────────────────────────────────────
   FEASIBILITY LOADING SCREEN

   The analyse pipeline runs 25-35s: public datasets, a vision pass and
   a narration pass, in sequence. That is far too long for a spinner, so
   the wait is narrated instead — a rotating quip, a milestone stepper
   that advances on real progress, and a quote to read while the last
   stage finishes.

   Shares the visual language of .step-milestone above rather than
   redefining it, but not the markup: this stepper carries a label under
   every dot, which the funnel's own progress bar does not.
   ───────────────────────────────────────────────────────── */

.loading-screen {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-2xl);
  min-height: 60vh;
  width: 100%;
  max-width: 640px;
  margin: 0 auto;
  padding: var(--space-2xl) var(--space-md);
  text-align: center;
}

/* Fixed height so a one-line quip swapping for a two-line one does not
   shunt the stepper down mid-animation. */
.loading-quip {
  font-family: var(--font-display);
  text-transform: var(--font-display-transform, none);
  letter-spacing: var(--font-display-tracking, normal);
  font-weight: var(--font-display-weight);
  font-size: calc(var(--text-xl) * var(--font-display-scale));
  line-height: 1.25;
  color: var(--text-primary);
  min-height: 2.5em;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* The stage change is the one thing in here worth hearing. The quip and
   the quotes rotate every few seconds and are aria-hidden, so this is
   what a screen reader gets instead of a metronome. */
.loading-sr {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.milestone-stepper {
  position: relative;
  width: 100%;
}

/* Inset by half a column each side, because the row below holds SIX
   equal-width nodes: the first and last dot centres sit at 100/12 and
   1100/12 per cent, and the line has to start and end on them rather
   than on the row's edges.
   Tied to LOADER_STEPS.length in LoadingScreen.tsx, which computes the
   node positions from the same count. Changing one without the other
   leaves the bar landing beside the dots instead of on them. */
.milestone-line {
  position: absolute;
  left: 8.3333%;
  right: 8.3333%;
  top: 6px;
}



.milestone-line-track {
  height: 2px;
  width: 100%;
  background: var(--canvas-border);
  border-radius: 2px;
  overflow: hidden;
}

/* Width is driven by GSAP, not CSS — see LoadingScreen.tsx. Starting at
   0 matters: the first paint must not show a full bar before the
   animation takes over. */
.milestone-line-fill {
  height: 100%;
  width: 0;
  background: var(--brand-primary);
  border-radius: 2px;
}

.milestone-dots-row {
  position: relative;
  display: flex;
  align-items: flex-start;
  width: 100%;
}

.milestone-node {
  flex: 1 1 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-sm);
  min-width: 0;
}

.milestone-dot-wrap {
  height: 14px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.milestone-dot {
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: var(--canvas-white);
  border: 2px solid var(--canvas-border);
  transition: background 0.35s ease, border-color 0.35s ease,
              width 0.35s ease, height 0.35s ease, box-shadow 0.35s ease;
}

.milestone-node.past .milestone-dot {
  background: var(--brand-primary);
  border-color: var(--brand-primary);
}

/* A RING, NOT A DISC, and that is the whole distinction.
   `past` and `active` were both a solid brand dot differing only in size
   and a faint halo, so on the first four nodes the reader told them
   apart by where the bar had stopped rather than by the dot itself. On
   the fifth there is no bar left to read, so the running stage was
   indistinguishable from a finished one — narration spent fifteen
   seconds looking complete. Hollow reads as "here now", filled reads as
   "done", and it holds on every node without depending on the bar. */
.milestone-node.active .milestone-dot {
  width: 16px;
  height: 16px;
  background: var(--canvas-white);
  border-color: var(--brand-primary);
  border-width: 3px;
  box-shadow: 0 0 0 4px rgba(0, 0, 0, 0.07);
  animation: milestone-pulse 1.9s ease-in-out infinite;
}

/* Slow, and only on the halo — the dot itself must not change size or it
   drags the two-pixel track it sits on. */
@keyframes milestone-pulse {
  0%, 100% { box-shadow: 0 0 0 4px rgba(0, 0, 0, 0.07); }
  50%      { box-shadow: 0 0 0 8px rgba(0, 0, 0, 0.03); }
}

.milestone-label {
  font-size: var(--text-xs);
  line-height: 1.3;
  color: var(--text-muted);
  padding: 0 var(--space-xs);
  transition: color 0.35s ease;
}

.milestone-node.past .milestone-label {
  color: var(--text-secondary);
}

.milestone-node.active .milestone-label {
  color: var(--text-primary);
  font-weight: 600;
}

/* Fixed height for the same reason as the quip: quotes crossfade in
   place, and two lines following one must not move the page. */
.loading-quote-section {
  min-height: 4.5em;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: var(--space-xs);
}

.loading-quote-text {
  font-size: var(--text-base);
  font-style: italic;
  line-height: 1.5;
  color: var(--text-secondary);
}

.loading-quote-attr {
  font-size: var(--text-sm);
  color: var(--text-muted);
}

/* Five labels will not sit side by side on a phone. Keep every dot —
   the stepper still reads as progress — and show only the label for
   the stage actually running. */
@media (max-width: 640px) {
  .loading-screen {
    gap: var(--space-xl);
  }

  .milestone-node .milestone-label {
    display: none;
  }

  .milestone-node.active .milestone-label {
    display: block;
    position: absolute;
    top: 28px;
    left: 0;
    right: 0;
    white-space: nowrap;
  }

  .milestone-dots-row {
    padding-bottom: var(--space-xl);
  }
}

/* The dot growth and the label colour are transitions this file owns.
   The bar fill and both crossfades are GSAP tweens and no stylesheet can
   reach them — LoadingScreen.tsx asks the same query from the other side
   and swaps the text without animating it. */
@media (prefers-reduced-motion: reduce) {
  .milestone-dot,
  .milestone-label {
    transition: none;
  }

  /* The ring still marks the running stage; only the breathing stops. */
  .milestone-node.active .milestone-dot {
    animation: none;
  }

  .analyst-panel {
    transition: none;
  }
}

/* ─────────────────────────────────────────────────────────
   THE ANALYST — the chat panel on the hosted report

   A floating launcher and a panel, both fixed to the bottom right of
   the viewport. Nothing here introduces a colour: the panel is the
   report's own card — white on --canvas-border, brand only where the
   report already uses brand, which is the CTA and the focus ring.

   Desktop-first, like the rest of this file. The phone rule at the
   bottom takes the panel full-screen, because a 380px box floating in a
   360px viewport is a box with no page left around it.
   ───────────────────────────────────────────────────────── */

.analyst-launcher {
  position: fixed;
  right: var(--space-lg);
  bottom: var(--space-lg);
  z-index: 60;
  display: inline-flex;
  align-items: center;
  gap: var(--space-sm);
  padding: var(--space-md) var(--space-lg);
  font-family: var(--font-body);
  font-size: var(--text-base);
  font-weight: 600;
  color: var(--text-inverse);
  background: var(--brand-primary);
  border: none;
  border-radius: 999px;
  box-shadow: var(--shadow-lg);
  cursor: pointer;
  transition: background var(--transition-fast), transform var(--transition-fast);
}

.analyst-launcher:hover {
  background: var(--brand-primary-hover);
  transform: translateY(-1px);
}

/* Offset outwards, not inwards. The launcher sits on the page rather
   than inside a card, so a -2px ring would be swallowed by the fill. */
.analyst-launcher:focus-visible {
  outline: 2px solid var(--brand-primary);
  outline-offset: 3px;
}

.analyst-panel {
  position: fixed;
  right: var(--space-lg);
  bottom: calc(var(--space-lg) + 64px);
  z-index: 61;
  display: flex;
  flex-direction: column;
  width: 380px;
  /* Bounded against the viewport, not the content: an answer that runs
     long must scroll the log, never push the composer off screen. */
  max-height: min(560px, calc(100vh - var(--space-2xl) - 64px));
  background: var(--canvas-white);
  border: 1px solid var(--canvas-border);
  border-radius: var(--radius-xl);
  box-shadow: var(--shadow-xl);
  overflow: hidden;
  animation: analyst-rise var(--transition-base);
}

@keyframes analyst-rise {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: translateY(0); }
}

.analyst-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-md);
  padding: var(--space-md) var(--space-lg);
  border-bottom: 1px solid var(--canvas-border);
  background: var(--canvas-off-white);
}

.analyst-title {
  font-family: var(--font-display);
  text-transform: var(--font-display-transform, none);
  letter-spacing: var(--font-display-tracking, normal);
  font-size: calc(var(--text-md) * var(--font-display-scale));
  font-weight: var(--font-display-weight-strong);
  color: var(--text-primary);
}

.analyst-sub {
  font-size: var(--text-xs);
  color: var(--text-secondary);
}

.analyst-close {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  color: var(--text-secondary);
  background: none;
  border: none;
  border-radius: var(--radius-sm);
  cursor: pointer;
}

.analyst-close:hover {
  color: var(--text-primary);
  background: var(--canvas-light-gray);
}

.analyst-close:focus-visible {
  outline: 2px solid var(--brand-primary);
  outline-offset: -2px;
}

.analyst-log {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  /* Stops the scroll chaining to the report the moment the log hits an
     end. Without it a flick inside the panel keeps going into the page
     behind, and on iOS it rubber-bands the whole document. */
  overscroll-behavior: contain;
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  padding: var(--space-lg);
}

.analyst-msg {
  max-width: 88%;
  padding: var(--space-sm) var(--space-md);
  font-size: var(--text-sm);
  line-height: 1.55;
  border-radius: var(--radius-lg);
  /* The model streams plain prose with real line breaks in it; without
     this they collapse and a three-point answer arrives as one run-on. */
  white-space: pre-wrap;
}

.analyst-msg-assistant {
  align-self: flex-start;
  color: var(--text-primary);
  background: var(--canvas-light-gray);
  border-bottom-left-radius: var(--radius-sm);
}

.analyst-msg-user {
  align-self: flex-end;
  color: var(--text-inverse);
  background: var(--brand-primary);
  border-bottom-right-radius: var(--radius-sm);
}

.analyst-thinking {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  padding: 0 var(--space-md);
}

.analyst-thinking span[aria-hidden] {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--text-muted);
  animation: analyst-pulse 1.1s ease-in-out infinite;
}

.analyst-thinking span[aria-hidden]:nth-child(2) { animation-delay: 0.15s; }
.analyst-thinking span[aria-hidden]:nth-child(3) { animation-delay: 0.3s; }

.analyst-thinking-label {
  margin-left: var(--space-xs);
  font-size: var(--text-xs);
  color: var(--text-muted);
}

@keyframes analyst-pulse {
  0%, 100% { opacity: 0.3; }
  50%      { opacity: 1; }
}

.analyst-error {
  padding: var(--space-sm) var(--space-md);
  font-size: var(--text-sm);
  line-height: 1.5;
  color: var(--status-error);
  background: var(--status-error-bg);
  border-radius: var(--radius-md);
}

.analyst-openers {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
  padding: 0 var(--space-lg) var(--space-md);
}

.analyst-opener {
  padding: var(--space-xs) var(--space-sm);
  font-family: var(--font-body);
  font-size: var(--text-xs);
  color: var(--text-secondary);
  background: var(--canvas-white);
  border: 1px solid var(--canvas-border);
  border-radius: 999px;
  cursor: pointer;
  transition: border-color var(--transition-fast), color var(--transition-fast);
}

.analyst-opener:hover {
  color: var(--text-primary);
  border-color: var(--text-muted);
}

.analyst-opener:focus-visible {
  outline: 2px solid var(--brand-primary);
  outline-offset: 1px;
}

.analyst-form {
  display: flex;
  align-items: flex-end;
  gap: var(--space-sm);
  padding: var(--space-md) var(--space-lg);
  border-top: 1px solid var(--canvas-border);
  background: var(--canvas-white);
}

/* The field is labelled for a screen reader and captioned by its own
   placeholder for everyone else. A visible label above a one-line
   composer is a row of chrome that says what the box already says. */
.analyst-label {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.analyst-input {
  flex: 1;
  min-height: 40px;
  max-height: 120px;
  padding: var(--space-sm) var(--space-md);
  font-family: var(--font-body);
  font-size: var(--text-sm);
  line-height: 1.5;
  color: var(--text-primary);
  background: var(--canvas-white);
  border: 1px solid var(--canvas-border);
  border-radius: var(--radius-md);
  resize: none;
}

.analyst-input::placeholder {
  color: var(--text-muted);
}

.analyst-input:focus {
  outline: none;
  border-color: var(--brand-primary);
}

.analyst-input:focus-visible {
  outline: 2px solid var(--brand-primary);
  outline-offset: -2px;
}

.analyst-send {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 40px;
  height: 40px;
  color: var(--text-inverse);
  background: var(--brand-primary);
  border: none;
  border-radius: var(--radius-md);
  cursor: pointer;
  transition: background var(--transition-fast);
}

.analyst-send:hover:not(:disabled) {
  background: var(--brand-primary-hover);
}

.analyst-send:disabled {
  background: var(--canvas-border);
  color: var(--text-muted);
  cursor: default;
}

.analyst-send:focus-visible {
  outline: 2px solid var(--brand-primary);
  outline-offset: 2px;
}

/* A floating box is a desktop idea. On a phone the panel is the page:
   full bleed, square corners, and the launcher hidden behind it so the
   two cannot overlap. */
@media (max-width: 768px) {
  /* FULL BLEED, ALWAYS — the contents move, the panel does not.
     iOS does not shrink the layout viewport when the keyboard opens, so
     inset:0 alone left the composer pinned behind the keyboard. Sizing
     the panel to the VISUAL viewport instead fixed that and introduced a
     worse one: a strip of the report showing between the panel's bottom
     edge and the keyboard, because Safari's bottom toolbar sits between
     the two and is in neither measurement.
     So the panel keeps the whole screen and an opaque background, and
     padding-bottom lifts the log and the composer clear of the keyboard.
     Nothing can show through something that is never smaller than the
     screen. --analyst-kb is written by AnalystChat.tsx. */
  .analyst-panel {
    inset: 0;
    width: auto;
    max-height: none;
    border: none;
    border-radius: 0;
    /* One expression for both states. With the keyboard up its height
       wins; with it down there is no keyboard and the home indicator
       does, so the composer clears whichever is actually there. */
    padding-bottom: max(var(--analyst-kb, 0px), env(safe-area-inset-bottom));
    /* iOS animates the keyboard over roughly a quarter second and reports
       its own intermediate heights, so this is a floor under the gaps
       between those reports rather than an animation of its own. Short
       enough that the composer is never visibly behind the keyboard. */
    transition: padding-bottom 0.12s ease-out;
  }

  .analyst-log {
    padding: var(--space-md);
  }

  .analyst-form {
    padding: var(--space-md);
  }

  .analyst-msg {
    max-width: 92%;
  }

  .analyst-launcher {
    right: var(--space-md);
    bottom: var(--space-md);
  }
}

/* The rise and the thinking dots are decoration. The dots also carry a
   label, so removing the animation costs nothing a reader needed. */
@media (prefers-reduced-motion: reduce) {
  .analyst-panel {
    animation: none;
  }

  .analyst-launcher {
    transition: none;
  }

  .analyst-launcher:hover {
    transform: none;
  }

  .analyst-thinking span[aria-hidden] {
    animation: none;
    opacity: 0.6;
  }
}

/* A fixed launcher over a printed report is a blue pill stamped across
   the page it is floating on. */
@media print {
  .analyst-launcher,
  .analyst-panel {
    display: none;
  }
}

/* ─────────────────────────────────────────────────────────
   SURVEY RE-REVIEW — components/report/SurveyReReview.tsx

   The section that invites a plat upload on the report itself and then
   shows what the plat changed.

   Desktop-first like the rest of this file: the full layout is the base
   rule and the 640px query is where the three-column row becomes a
   stack.

   NO NEW COLOURS. A lower score is --status-warning rather than
   --status-error, deliberately: a plat that reveals an easement has not
   failed at anything, it has told the homeowner something true before
   an excavator arrived. Red would frame the most valuable outcome this
   feature has as a fault.
   ───────────────────────────────────────────────────────── */

/* The survey offer, on the back of the score card.
   
   It has been a bordered block inside a band, a bare section, a banded
   section at the foot, and a separate card underneath — all of them
   answering the wrong question. It is not a section at all: it is the
   other side of the score, because the number is capped at 90 for want of
   a plat and "here is your score" and "here is how to lift the ceiling"
   are two faces of one statement.
   
   So it carries only its own padding. The card, the ground, the border
   and the column all come from the flip face it sits in, which is the
   same .estimate-card treatment as the property card above. */
.re-review {
  padding: var(--space-xl);
}

@media (max-width: 768px) {
  .re-review {
    padding: var(--space-lg) var(--space-md);
  }
}

/* A row that animates its own height, without anyone measuring it.
   
   grid-template-rows going 0fr to 1fr is the one way to transition to a
   content height CSS does not know in advance — max-height with a guessed
   ceiling either clips a long dropzone or eases against a number far
   above the real height, which reads as a lag before anything moves.
   
   The inner element carries the overflow. Without it the content spills
   out of a zero-height row instead of being clipped by it. */
.re-review-fold {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows var(--transition-base, 260ms)
    cubic-bezier(0.4, 0.1, 0.2, 1);
}

.re-review-fold.is-open {
  grid-template-rows: 1fr;
}

.re-review-fold-inner {
  overflow: hidden;
  /* Fades slightly behind the height so the content is not readable while
     the row is still too short to hold it. */
  opacity: 0;
  transition: opacity 180ms ease 40ms;
}

.re-review-fold.is-open > .re-review-fold-inner {
  opacity: 1;
}

@media (prefers-reduced-motion: reduce) {
  .re-review-fold,
  .re-review-fold-inner {
    transition: none;
  }
}

/* The score flip, which differs from the property one above it.
   
   There, each face wraps an .estimate-card and that card is the flex
   column the notch pushes against. Here the face's children ARE the
   content and the notch, so the face itself has to be the column — scoped
   to .score-flip rather than added to .flip-face, because changing the
   shared class would reach the property card too. */
.score-flip .flip-face {
  display: flex;
  flex-direction: column;
}

.score-flip .flip-face > .score-panel,
.score-flip .flip-face > .re-review {
  flex: 1 1 auto;
}

/* THE FACE IS THE CARD, so the notch is part of it.
   
   .score-panel carries its own radius-xl. With the notch as a sibling
   below it, the fill rounded off above a square-topped band and the two
   read as separate objects with a seam between them — which looked like
   the band had failed to attach. The property flip does not have this
   problem because .estimate-card owns the radius and overflow, and its
   notch sits inside.
   
   So the face takes the radius and clips, and the panel gives its own up.
   Nothing about the fill or the layout changes; the corners just belong
   to the container that also holds the band. */
.score-flip .flip-face {
  border-radius: var(--radius-xl);
  overflow: hidden;
}

.score-flip .flip-face > .score-panel {
  border-radius: 0;
}

/* The back face needs the card the front gets from .score-panel's own
   brand fill: border and shadow to match the property card above, and
   white ground so the survey offer reads as paper. */
.score-flip .flip-back {
  background: var(--canvas-white);
  border: 1px solid var(--canvas-border);
  box-shadow: var(--shadow-lg);
}

.re-review-title {
  text-align: center;
  margin: 0 0 var(--space-sm);
  font-family: var(--font-display);
  text-transform: var(--font-display-transform, none);
  letter-spacing: var(--font-display-tracking, normal);
  font-weight: var(--font-display-weight-strong);
  font-size: calc(var(--text-xl) * var(--font-display-scale));
  color: var(--text-primary);
}

.re-review-lead {
  max-width: var(--max-content);
  /* auto, not 0 — a max-width block with a zero side margin sits left
     however its text is aligned. */
  margin: 0 auto var(--space-lg);
  text-align: center;
  font-size: var(--text-base);
  line-height: 1.6;
  color: var(--text-secondary);
}

.re-review-actions {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-sm);
  margin-top: var(--space-lg);
}

.re-review-note {
  max-width: var(--max-content);
  margin: 0 auto;
  text-align: center;
  font-size: var(--text-sm);
  line-height: 1.55;
  color: var(--text-muted);
}

/* ── The forty-second wait ───────────────────────────────── */

.re-review-working {
  display: flex;
  align-items: flex-start;
  gap: var(--space-md);
  margin-top: var(--space-lg);
  padding: var(--space-md);
  background: var(--canvas-white);
  border: 1px solid var(--canvas-border);
  border-radius: var(--radius-md);
  font-size: var(--text-sm);
  color: var(--text-primary);
}

.re-review-working strong {
  display: block;
  margin-bottom: var(--space-xs);
}

.re-review-spinner {
  flex: none;
  width: 20px;
  height: 20px;
  margin-top: 2px;
  border: 2px solid var(--canvas-border);
  border-top-color: var(--brand-primary);
  border-radius: 50%;
  animation: re-review-spin 900ms linear infinite;
}

@keyframes re-review-spin {
  to { transform: rotate(360deg); }
}

.re-review-error {
  margin-top: var(--space-md);
  padding: var(--space-sm) var(--space-md);
  background: var(--status-error-bg);
  color: var(--status-error);
  border-radius: var(--radius-sm);
  font-size: var(--text-sm);
  line-height: 1.55;
}

/* ── The result ──────────────────────────────────────────── */

/* Focus is moved here programmatically when a run finishes, and a
   programmatic focus on a tabindex="-1" element does not match
   :focus-visible in Chrome. Hence :focus, which does. */
.re-review-result:focus {
  outline: 2px solid var(--brand-primary);
  outline-offset: 4px;
  border-radius: var(--radius-sm);
}

.re-review-result:empty {
  display: none;
}

.re-review-score {
  display: flex;
  align-items: baseline;
  gap: var(--space-sm);
  margin-top: var(--space-lg);
  font-family: var(--font-display);
  text-transform: var(--font-display-transform, none);
  letter-spacing: var(--font-display-tracking, normal);
}

.re-review-score-from {
  font-size: var(--text-2xl);
  color: var(--text-muted);
  text-decoration: line-through;
}

.re-review-score-arrow {
  font-size: var(--text-xl);
  color: var(--text-muted);
}

.re-review-score-to {
  font-size: var(--text-3xl);
  font-weight: var(--font-display-weight-strong);
  color: var(--text-primary);
}

.re-review-score-to[data-direction="up"] { color: var(--status-success); }
.re-review-score-to[data-direction="down"] { color: var(--status-warning); }

.re-review-score-cap {
  font-family: var(--font-body);
  font-size: var(--text-sm);
  color: var(--text-muted);
}

/* The sentence that keeps a raised ceiling from reading as a discovery.
   Given its own rule because it is the one paragraph in the section a
   reader must not skim past. */
.re-review-split {
  margin: var(--space-md) 0 0;
  padding: var(--space-md);
  background: var(--canvas-white);
  border-left: 3px solid var(--brand-primary);
  border-radius: var(--radius-sm);
  font-size: var(--text-sm);
  line-height: 1.6;
  color: var(--text-secondary);
}

.re-review-group {
  margin-top: var(--space-lg);
}

.re-review-group-title {
  margin: 0 0 var(--space-sm);
  font-size: var(--text-sm);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-muted);
}

.re-review-group[data-tone="settled"] .re-review-group-title { color: var(--brand-primary); }
.re-review-group[data-tone="worse"] .re-review-group-title { color: var(--status-warning); }

.re-review-list {
  display: grid;
  gap: var(--space-xs);
  margin: 0;
  padding: 0;
  list-style: none;
}

.re-review-item {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-xs) var(--space-sm);
  padding: var(--space-sm) var(--space-md);
  background: var(--canvas-white);
  border: 1px solid var(--canvas-border);
  border-radius: var(--radius-sm);
  font-size: var(--text-sm);
  line-height: 1.5;
}

/* `order` rather than DOM order: the source line is full width and must
   wrap BELOW the points column, but it is written before it in the
   markup so a screen reader hears the authority before the arithmetic. */
.re-review-item-signal {
  order: 1;
  flex: 0 0 14rem;
  font-weight: 600;
  color: var(--text-primary);
}

.re-review-item-move {
  order: 2;
  flex: 1 1 12rem;
  color: var(--text-secondary);
}

.re-review-item-move strong {
  font-weight: 600;
  color: var(--text-primary);
}

.re-review-was {
  color: var(--text-muted);
}

.re-review-item-points {
  order: 3;
  flex: none;
  margin-left: auto;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.re-review-item-points[data-direction="up"] { color: var(--status-success); }
.re-review-item-points[data-direction="down"] { color: var(--status-warning); }

.re-review-item-source {
  order: 4;
  flex: 1 1 100%;
  font-size: var(--text-xs);
  color: var(--text-muted);
}

.re-review-sr {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

@media (max-width: 640px) {
  /* Nothing to shrink now that the card treatment is gone —
     report-section owns the spacing at every width. */

  /* The signal name takes its own line rather than a 14rem column that
     no longer fits, so the before → after pair stays on one line. */
  .re-review-item-signal { flex: 1 1 100%; }
  .re-review-item-move { flex: 1 1 auto; }
  .re-review-item-points { margin-left: 0; }

  .re-review-score-from,
  .re-review-score-arrow { font-size: var(--text-lg); }
  .re-review-score-to { font-size: var(--text-2xl); }
}

/* No substitute animation. The elapsed-seconds counter in the working
   copy is what tells someone the page has not hung, and it exists so
   this rule can be a plain removal rather than a gentler wobble. */
@media (prefers-reduced-motion: reduce) {
  .re-review-spinner { animation: none; }
}

/* This page is printed and handed to a designer. What the survey changed
   belongs on paper; a dropzone and a spinner do not. */
@media print {
  .re-review-actions,
  .re-review-working,
  .re-review-spinner { display: none !important; }

  .re-review {
    background: none;
    border: 1px solid #ddd;
    break-inside: avoid;
  }
}


/* ─────────────────────────────────────────────────────────
   Analyst replies: markdown, and the inline booking control.

   The panel used to print the model's raw output, so a reply that bolded
   the figure that mattered showed **asterisks** and a three-option answer
   showed hyphens. See components/report/rich-text.tsx — it returns React
   elements, so none of this is styling a string we injected as HTML.
   ───────────────────────────────────────────────────────── */
.analyst-msg p {
  margin: 0 0 var(--space-sm);
}

.analyst-msg p:last-child {
  margin-bottom: 0;
}

.analyst-msg ul,
.analyst-msg ol {
  margin: 0 0 var(--space-sm);
  padding-left: 1.15em;
}

.analyst-msg li {
  margin-bottom: 2px;
}

.analyst-msg li:last-child {
  margin-bottom: 0;
}

.analyst-msg strong {
  font-weight: 600;
}

.analyst-msg code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  /* The same 0.92 this used to express as an em, written against the
     token its parent is set in. Identical rendering, and it now flows
     through --font-body-scale like every other size in the file — an em
     inherits the SCALED parent, which is right until a rule between the
     two changes size and the ratio silently moves. */
  font-size: calc(var(--text-sm) * 0.92);
  padding: 1px 4px;
  border-radius: var(--radius-sm);
  background: var(--canvas-off-white);
}

.analyst-msg a {
  color: var(--brand-primary);
  text-decoration: underline;
  text-underline-offset: 2px;
}

/* Sized as a control rather than a link: it is the one thing in the panel
   that leaves the conversation, and a homeowner should be able to see
   that before they click it.
   
   Scoped under .analyst-msg deliberately. `.analyst-msg a` above is (0,1,1)
   and a bare `.analyst-book` is (0,1,0), so the link rule won and painted
   the label brand-primary on a brand-primary button — a blue box with
   invisible text. Both rules shipped in the same commit. */
.analyst-msg .analyst-book {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  margin-top: var(--space-xs);
  padding: 8px 14px;
  border-radius: var(--radius-md);
  background: var(--brand-primary);
  color: var(--canvas-white);
  font-family: var(--font-body);
  font-size: var(--text-sm);
  font-weight: 500;
  text-decoration: none;
  transition: background var(--transition-fast);
}

.analyst-msg .analyst-book:hover {
  background: var(--brand-primary-hover, var(--brand-primary));
}

.analyst-msg .analyst-book:focus-visible {
  outline: 2px solid var(--brand-primary);
  outline-offset: 2px;
}

/* Quiet text beside the close control, not a pill.
   
   It was a bordered box labelled "New", which read as a call to action
   competing with the close button and said nothing about what it did.
   "Clear chat" is unambiguous, and matching .analyst-close's borderless
   treatment puts it where it belongs in the hierarchy: a utility in the
   panel chrome, not a thing to click. */
.analyst-reset {
  padding: 4px 6px;
  margin-right: var(--space-xs);
  border: none;
  background: none;
  font-family: var(--font-body);
  font-size: var(--text-xs);
  font-weight: 400;
  color: var(--text-muted);
  cursor: pointer;
  border-radius: var(--radius-sm);
  transition: color var(--transition-fast);
}

.analyst-reset:hover {
  color: var(--text-primary);
}

.analyst-reset:focus-visible {
  outline: 2px solid var(--brand-primary);
  outline-offset: 2px;
}

/* ─────────────────────────────────────────────────────────
   FEASIBILITY — SELLING POINTS GRID (.fsp-*)

   components/FeasibilitySellingPoints.tsx. Deliberately the same object
   as the fall special's .fs-why-item — white card on the off-white band,
   circular brand-tinted icon disc, heading over body — because this is
   one product and the two landing pages should read as siblings rather
   than as two agencies' work.

   Flex, not grid, and the difference matters here: the cards carry
   copy of visibly different lengths, and `align-items: stretch` on a
   flex row equalises the row without forcing every row to the height of
   the tallest card on the page. A fixed 3-track grid would also strand
   the layout if the copy ever went to five points or seven.
   ───────────────────────────────────────────────────────── */

.fsp-grid {
  display: flex;
  flex-wrap: wrap;
  /* max-width is .fs-why-grid's, not chosen independently: this section
     is the fall special's sibling and the giveaway when it is not is
     width. At 960px the cards come out 304px against that section's
     233px and the page reads loose beside it, even though the headline
     and subheadline above match — .page-section-inner is identical on
     both pages, so the grid was the only thing making this one sprawl.

     The gap stays tighter than .fs-why-grid's 40px. These cards carry
     three or four lines of body copy rather than two, so they are taller
     than the fall special's; the wider gutter that suits a short card
     reads as a hole between these. */
  gap: var(--space-lg);
  max-width: 780px;
  margin: var(--space-2xl) auto 0;
  text-align: center;
}

/* Three per row: a third of the track minus its share of the two gaps.
   `flex-grow: 0` so a widowed last card cannot stretch to fill the row —
   with six cards there is never one, but a tenant override that drops a
   point should degrade to a third-width card, not a banner. */
.fsp-card {
  flex: 0 1 calc((100% - 2 * var(--space-lg)) / 3);
  background: var(--canvas-white);
  border: 1px solid var(--canvas-border);
  border-radius: var(--radius-xl);
  padding: var(--space-lg);
  transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
}

.fsp-card:hover {
  border-color: var(--brand-primary-light);
  box-shadow: var(--shadow-sm);
}

/* Centred over centred text, the same disc-above-heading arrangement as
   .fs-why-icon — this section is the fall special's sibling and reading
   as one is the whole point of it. */
.fsp-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  margin: 0 auto var(--space-md);
  border-radius: 50%;
  background: var(--brand-primary-light);
  color: var(--brand-primary);
}

/* h3, so the section reads h1 (hero) → h2 (section) → h3 (card) with no
   level skipped. .fs-why-item styles an h4 by element; this is a class,
   so the level can change without the type scale following it. */
.fsp-heading {
  font-family: var(--font-body);
  font-size: var(--text-base);
  font-weight: 600;
  line-height: 1.35;
  color: var(--text-primary);
  margin-bottom: 6px;
}

.fsp-body {
  font-size: var(--text-sm);
  color: var(--text-secondary);
  font-weight: 300;
  line-height: 1.6;
}

/* Desktop-first, matching the rest of this file. One column below the
   tablet break, the same jump .fs-why-grid makes: three columns of
   three-line body copy stop being columns and start being a squeeze
   well before the text itself wraps badly. */
@media (max-width: 768px) {
  .fsp-grid {
    gap: var(--space-md);
    margin-top: var(--space-xl);
  }

  .fsp-card {
    flex-basis: 100%;
  }
}

/* The hover lift is decoration, and the card is not interactive — so
   under reduced motion there is nothing to animate to. */
@media (prefers-reduced-motion: reduce) {
  .fsp-card {
    transition: none;
  }
}

/* ─────────────────────────────────────────────────────────
   TOUCH: THE 16px FLOOR ON FORM CONTROLS

   iOS Safari zooms the page when a field is focused whose text is under
   16px, and it does not zoom back out. The body ramp starts at 15px
   (--text-base) and several controls sit at 13px (--text-sm), so every
   field in every funnel triggered it: tap an input, the page jumps to
   ~110% and the layout is wrecked for the rest of the session.

   Scoped to coarse pointers so the desktop type scale is untouched —
   this is a platform workaround, not a design decision, and it should
   not leak into a design it is not fixing.

   max(), not a flat 16px, because --font-body-scale is tenant-settable
   and a tenant who has scaled the ramp up must not have their fields
   scaled back down to the floor.

   Element selectors AND the class list: the classes are (0,1,0) and
   would beat a bare `input`, and anything class-less still needs the
   floor. Placed last in the file so the cascade resolves in its favour.
   ───────────────────────────────────────────────────────── */
@media (pointer: coarse) {
  input,
  select,
  textarea,
  .form-input,
  .iw-form-input,
  .intake-price-input,
  .outdoor-qty-input,
  .admin-price-input,
  .analyst-input,
  .consultation-picker select {
    font-size: max(16px, var(--text-base));
  }
}

/* ─────────────────────────────────────────────────────────
   REPORT — WHAT NOTHING SETTLED

   Sits under the finding cards, inside the same section, and must not
   read as one of them. A finding is a problem on this property; this is
   an edge of the report, and the reader has been told it cost them no
   points. So: no icon, no border colour, no card chrome — a tinted note
   with a heading, closer to a footnote than to a warning.
   ───────────────────────────────────────────────────────── */
.report-unknowns {
  margin-top: var(--space-xl);
  padding: var(--space-lg);
  border: 1px solid var(--canvas-border);
  border-radius: var(--radius-lg);
  /* Whatever it is sitting on. It renders in two places now — under the
     score on a white ground, after the findings cards on whichever ground
     the band alternation gave that section — and a fixed fill is wrong in
     one of them whichever one is picked. The border is what makes it a
     card; the fill was only ever separating it from white. */
  background: transparent;
  text-align: left;
}

.report-unknowns-title {
  font-family: var(--font-body);
  font-size: var(--text-base);
  font-weight: 600;
  color: var(--text-primary);
  margin-bottom: var(--space-xs);
}

.report-unknowns-text {
  font-size: var(--text-sm);
  font-weight: 300;
  line-height: 1.65;
  color: var(--text-secondary);
}

/* ─────────────────────────────────────────────────────────
   FEASIBILITY — THE SCHEDULE PAGE

   Ported from the AI rendering tool's /book page so the two read as one
   company's work: a centred hero, one raised card, testimonials under
   it. What is new here is the card having two faces — the reference asks
   for the details on the page, and this asks for them on the far side of
   a slide, so the reader meets one short ask at a time.
   ───────────────────────────────────────────────────────── */
.book-page {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 100%;
  padding: var(--space-2xl) var(--space-xl);
  gap: var(--space-2xl);
  /* The off-slide face lives at translateX(100%) of a full-width track.
     Without this the document scrolls sideways to reach it. */
  overflow-x: hidden;
}

.book-hero {
  text-align: center;
  max-width: 640px;
}

.book-headline { margin-bottom: var(--space-md); }

/* The heading on a card's second face: the section headline at a size
   that sits inside a card rather than over a page. */
.book-face-title {
  font-family: var(--font-display);
  text-transform: var(--font-display-transform, none);
  letter-spacing: var(--font-display-tracking, normal);
  font-size: calc(var(--text-lg) * var(--font-display-scale));
  font-weight: var(--font-display-weight);
  line-height: 1.25;
  color: var(--text-primary);
  margin: 0 0 var(--space-xs);
}

.book-sub {
  max-width: 560px;
  margin: 0 auto;
}

.book-card {
  width: 100%;
  max-width: 780px;
  background: var(--canvas-white);
  border: 1px solid var(--canvas-border);
  border-radius: var(--radius-xl);
  box-shadow: var(--shadow-lg);
  padding: var(--space-xl);
  text-align: left;
}

/* Height is set from the active face in JS and animated here. The two
   faces have genuinely different heights and the calendar's own changes
   with the month, so there is no honest value to hard-code. */
.book-viewport {
  position: relative;
  overflow: hidden;
  transition: height var(--transition-base);
}

.book-track {
  display: flex;
  align-items: flex-start;
  width: 200%;
  transition: transform var(--transition-base);
}

.book-track.is-who { transform: translateX(-50%); }

.book-face {
  width: 50%;
  flex: 0 0 50%;
}

.book-section-header {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  margin-bottom: var(--space-lg);
  color: var(--brand-primary);
}

.book-section-title {
  font-family: var(--font-body);
  font-size: var(--text-base);
  font-weight: 600;
  color: var(--text-primary);
}

.book-period-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-md);
  margin-bottom: var(--space-lg);
}

.book-period {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  padding: var(--space-md) var(--space-lg);
  border-radius: var(--radius-lg);
  border: 1.5px solid var(--canvas-border);
  background: var(--canvas-white);
  cursor: pointer;
  transition: all var(--transition-fast);
  text-align: left;
}

.book-period:not(.selected):hover {
  border-color: var(--brand-primary);
  background: var(--canvas-off-white);
}

.book-period.selected {
  border-color: var(--brand-primary);
  background: var(--brand-primary-light);
  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
}

.book-period-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 42px;
  height: 42px;
  border-radius: var(--radius-md);
  background: var(--canvas-light-gray);
  color: var(--text-muted);
}

.book-period.selected .book-period-icon {
  background: var(--brand-primary-light);
  color: var(--brand-primary);
}

.book-period.selected .book-period-label { color: var(--brand-primary); }

.book-period-text { display: flex; flex-direction: column; }

.book-period-label {
  font-size: var(--text-base);
  font-weight: 600;
  color: var(--text-primary);
  line-height: 1.3;
}

.book-period-sub {
  font-size: var(--text-sm);
  color: var(--text-muted);
  margin-top: 2px;
}

.book-cta-row {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-sm);
}

.book-cta-hint {
  font-size: var(--text-sm);
  color: var(--text-muted);
  text-align: center;
}

.consent-text {
  font-size: var(--text-xs);
  color: var(--text-muted);
  line-height: 1.6;
  margin-top: var(--space-md);
  text-align: center;
}

.confirmation-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 56px;
  height: 56px;
  margin: 0 auto var(--space-lg);
  border-radius: 50%;
  background: var(--brand-primary-light);
  color: var(--brand-primary);
}

@media (max-width: 640px) {
  /* The page's own 40px gutters plus the card's 40px padding spent 160px
     of a 390px screen on margin — the calendar was being squeezed into
     less than half the width it had to work with. The gutter comes down
     to 16px and the card's padding with it, which is where the width
     actually goes. */
  .book-page {
    padding: var(--space-xl) var(--space-md);
    gap: var(--space-xl);
  }

  .book-card { padding: var(--space-lg) var(--space-md); }

  /* Two slot cards side by side stop being cards at this width. */
  .book-period-grid { grid-template-columns: 1fr; }
}

/* The slide is decoration; the face change is not. Under reduced motion
   the card cuts to the other face and resizes with it. */
@media (prefers-reduced-motion: reduce) {
  .book-viewport,
  .book-track,
  .book-period {
    transition: none;
  }
}

/* ─────────────────────────────────────────────────────────
   AI DESIGN FUNNEL  (/design)

   The render funnel folded in from ai-pool-tool. It reuses the
   estimator's option cards, buttons and step chrome; what is here is
   only what the render flow needs and nothing else has — the upload
   surface, the before/after reveal, and the waiting state.
   ───────────────────────────────────────────────────────── */

/* ── Upload ── */

/* The AI tool's own values, ported rather than re-derived: a first pass
   at this rewrote them from memory and drifted on every line — thinner
   border, tighter padding, white ground, and an active state that went
   paler where the original goes to full brand colour. */
.upload-zone {
  width: 100%;
  padding: 48px 32px;
  background: var(--canvas-off-white);
  border: 2px dashed var(--canvas-border);
  border-radius: var(--radius-lg);
  text-align: center;
  cursor: pointer;
  transition: all var(--transition-base);
  outline: none;
}

/* Hover, drag-over and keyboard focus are one state: the brand border on
   the brand tint, lifted. A drop target should look unmistakably live
   the moment a file is over it. */
.upload-zone:hover,
.upload-zone:focus-visible,
.upload-zone.drag-over {
  border-color: var(--brand-primary);
  background: var(--brand-primary-light);
  box-shadow: var(--shadow-md);
}

.upload-zone[aria-disabled='true'] {
  cursor: default;
  opacity: 0.7;
}

.upload-icon {
  display: flex;
  justify-content: center;
  margin-bottom: var(--space-md);
  opacity: 0.5;
}

.upload-label {
  font-family: var(--font-body);
  font-size: var(--text-base);
  font-weight: 500;
  color: var(--text-primary);
  margin-bottom: var(--space-xs);
}

.upload-sub {
  font-size: var(--text-sm);
  color: var(--text-muted);
}

.upload-processing {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-sm);
  color: var(--text-secondary);
}

.upload-preview {
  display: block;
  width: 100%;
  max-height: 280px;
  object-fit: cover;
  border-radius: var(--radius-md);
}

.upload-change {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  margin-top: var(--space-sm);
  font-size: var(--text-sm);
  color: var(--text-muted);
}

.upload-error {
  margin-top: var(--space-sm);
  padding: 10px 14px;
  background: var(--status-error-bg);
  border: 1px solid var(--status-error);
  border-radius: var(--radius-md);
  font-size: var(--text-sm);
  color: var(--status-error);
}

/* The photo-optional secondary CTA on the /design landing page (spec
   2026-09-12 §2). Same quiet-card shape as .outdoor-offer-card — a
   footnote next to the upload zone's primary ask, not a competing one. */
.design-start {
  /* Both ways into the funnel in one framed object. The dashed upload
     zone sits inside a plain white card; the description path is a
     footer under a hairline, so the hero has one box and the trust
     line beneath is visibly something else. */
  width: 100%;
  padding: var(--space-lg);
  background: var(--canvas-white);
  border: 1px solid var(--canvas-border);
  border-radius: var(--radius-xl);
  box-shadow: var(--shadow-md);
}

.design-start-alt {
  margin-top: var(--space-lg);
  text-align: center;
}

/* The continue button, in the alternative's slot once a photo is in. */
.design-start-go {
  display: flex;
  justify-content: center;
  margin-top: var(--space-lg);
}

/* "── or ──" on a hairline. */
.design-start-or {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  margin-bottom: var(--space-md);
  font-size: var(--text-xs);
  font-weight: 500;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-muted);
}

.design-start-or::before,
.design-start-or::after {
  content: '';
  flex: 1;
  height: 1px;
  background: var(--canvas-border);
}

.design-describe-cta-title {
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--text-secondary);
  margin-bottom: 2px;
}

.design-describe-cta-sub {
  font-size: var(--text-xs);
  color: var(--text-muted);
  font-weight: 300;
  line-height: 1.5;
  max-width: 420px;
  margin: 0 auto var(--space-sm);
}

.design-start-alt .btn-link {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 4px;
  background: none;
  border: 0;
  font-family: var(--font-body);
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--brand-primary);
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-color: color-mix(in srgb, var(--brand-primary) 35%, transparent);
  transition: text-decoration-color var(--transition-base);
}

.design-start-alt .btn-link:hover,
.design-start-alt .btn-link:focus-visible {
  text-decoration-color: var(--brand-primary);
  outline: none;
}

@media (max-width: 640px) {
  .design-start { padding: var(--space-md); }
}

/* ── Before / after reveal ── */

.ba-container {
  position: relative;
  width: 100%;
  /* 16:9 as the original. The render itself comes back 4:3 and is
     cover-cropped into this, which is how the AI tool always showed it. */
  aspect-ratio: 16 / 9;
  overflow: hidden;
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  background: var(--canvas-light-gray);
  cursor: ew-resize;
  user-select: none;
  touch-action: pan-y;
}

.ba-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  pointer-events: none;
}

.ba-after-clip {
  position: absolute;
  inset: 0;
}

.ba-divider {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 2px;
  /* On the photograph, not on a surface: white whatever band it sits in. */
  background: #fff;
  transform: translateX(-1px);
  pointer-events: none;
}

.ba-handle {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 40px;
  height: 40px;
  border-radius: 50%;
  /* On the photograph, not on a surface: white whatever band it sits in. */
  background: #fff;
  color: #1a1a1a;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.25);
  pointer-events: auto;
  cursor: ew-resize;
}

.ba-handle:focus-visible {
  outline: 2px solid var(--brand-primary);
  outline-offset: 2px;
}

.ba-label {
  position: absolute;
  bottom: var(--space-sm);
  padding: 4px 10px;
  border-radius: var(--radius-sm);
  background: rgba(0, 0, 0, 0.55);
  color: #fff;
  font-size: var(--text-xs);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  pointer-events: none;
}

/* The after-clip keeps the LEFT of the divider, so the render is what
   sits under the left label. These were once the other way round, which
   labelled the render "BEFORE" on the landing page and on the funnel's
   own reveal step. */
.ba-label-after  { left: var(--space-sm); }
.ba-label-before { right: var(--space-sm); }

/* The reveal's frame, as distinct from the landing's showcase pairs.
   The render comes back 4:3 and this is the one place it should be seen
   uncropped; the landing pairs stay 16:9 as the original showed them.
   Sized to the original's .render-image-frame: full width of the wide
   column, heavier shadow, because it is the page. */
.ba-container.is-render {
  aspect-ratio: 4 / 3;
  max-width: var(--max-wide);
  box-shadow: var(--shadow-xl);
}

/* The landing page's slider grid. Two across on a desktop, one on a
   phone; the minmax is what a slider needs to be draggable with a thumb. */
.ba-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  gap: var(--space-xl);
  margin-top: var(--space-lg);
  text-align: left;
}

/* The line under everything: the render is a concept, not a plan. */
.disclaimer-bar {
  width: 100%;
  background: var(--canvas-off-white);
  border-top: 1px solid var(--canvas-border);
  padding: var(--space-md) var(--space-xl);
  text-align: center;
  font-size: var(--text-xs);
  color: var(--text-muted);
  line-height: 1.6;
}

/* Shared spinner for the upload and render waits. */
.spin {
  animation: design-spin 1s linear infinite;
}

@keyframes design-spin {
  to { transform: rotate(360deg); }
}

@media (prefers-reduced-motion: reduce) {
  .spin { animation: none; }
}


/* ─────────────────────────────────────────────────────────
   DESIGN HUB  (/design/[token], components/design/hub/DesignHubView.tsx)

   The estimator's results page with the render opening the "What's
   included" card. EstimateResults exposes the slots (a media block
   above the header, a block straight after the card) and the hub fills
   them: the 4:3 frame, edge to edge, with whatever the run is right now
   and the version menu in its corner, then the adjust row with the
   add-photo drawer. The number above the card is the only headline;
   nothing here competes with it.
   ───────────────────────────────────────────────────────── */

/* ── Version menu: one pill in the frame's top-right corner ── */

/* A native select dressed as a pill and floated over the picture. White
   on a photo needs a shadow to hold its edge; on the loader's off-white
   the same shadow is what separates it. Above the slider's images (the
   handle is a sibling, so a click here never starts a drag). */
.hub-version {
  /* The slider labels' glass, so the frame has one visual language:
     a dark translucent pill, blurred, white type. "Version" then one
     segment per run; the active segment is the light one. */
  position: absolute;
  top: var(--space-sm);
  right: var(--space-sm);
  z-index: 2;
  display: inline-flex;
  align-items: center;
  gap: 2px;
  padding: 3px 4px 3px 10px;
  border-radius: var(--radius-xl);
  background: rgba(0, 0, 0, 0.55);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  color: var(--text-inverse);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
}

.hub-version-label {
  margin-right: 6px;
  font-size: var(--text-xs);
  font-weight: 500;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  opacity: 0.85;
}

.hub-version-tab {
  position: relative;
  min-width: 26px;
  height: 26px;
  padding: 0 8px;
  border: 0;
  border-radius: var(--radius-xl);
  background: transparent;
  color: var(--text-inverse);
  font-family: var(--font-body);
  font-size: var(--text-xs);
  font-weight: 600;
  line-height: 1;
  cursor: pointer;
  transition: background var(--transition-base), color var(--transition-base);
}

.hub-version-tab:hover { background: rgba(255, 255, 255, 0.18); }

.hub-version-tab.is-active {
  background: var(--canvas-white);
  color: var(--text-primary);
}

.hub-version-tab:focus-visible {
  outline: 2px solid var(--canvas-white);
  outline-offset: 1px;
}

/* A run still rendering: a small pulse on its segment. */
.hub-version-dot {
  position: absolute;
  top: 3px;
  right: 3px;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--brand-primary);
  animation: hub-dot-pulse 1.6s ease-in-out infinite;
}

.hub-version-tab.is-active .hub-version-dot { background: var(--brand-primary); }

@keyframes hub-dot-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.35; }
}

@media (prefers-reduced-motion: reduce) {
  .hub-version-dot { animation: none; }
}

/* ── The stage ── */

/* Where the results page draws its divider. The results body keeps the
   narrow column, so the stage breaks out of it to the reveal's wide
   width: the render is the one thing on the page that earns the room.
   Centred on the column with the usual left-50% / translate pair; the
   width is capped by the viewport less the page's own padding so it
   never runs past the edge on a middling window. */
.hub-stage {
  position: relative;
  left: 50%;
  width: min(var(--max-wide), calc(100vw - 2 * var(--space-xl)));
  margin: var(--space-xl) 0 var(--space-2xl);
  transform: translateX(-50%);
}

/* One 4:3 box for every state of the run, framed the way the reveal
   step frames the render: the reveal's corners and its shadow. A grid so
   the loader and the failed panel centre in it; the slider and the image
   fill it. The box keeps its ratio, but height is auto, so a failed
   panel that needs more room on a phone grows it rather than spilling
   out. */
.hub-frame {
  position: relative;
  /* Above the drawer tucked under it, so the shadow falls on the tab. */
  z-index: 2;
  display: grid;
  place-items: center;
  width: 100%;
  aspect-ratio: 4 / 3;
  overflow: hidden;
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-xl);
  background: var(--canvas-off-white);
}

/* The slider brings its own radius and shadow; in here the frame has
   them, and the slider only needs to fill it. */
.hub-frame .ba-container.is-render {
  width: 100%;
  max-width: none;
  border-radius: 0;
  box-shadow: none;
}

.hub-image {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* The line under the frame naming where it came from — load-bearing
   for a described run, which is a concept and says so. */
.hub-caption {
  margin: var(--space-md) 0 0;
  font-size: var(--text-sm);
  color: var(--text-secondary);
  text-align: center;
}

/* The checker's "not in your quote" fold: a quiet line with an info
   mark, opening to one sentence. Same register as the redo line below
   it; a disclaimer, not an alert. */
.hub-extras {
  margin: var(--space-sm) auto 0;
  max-width: 560px;
  font-size: var(--text-xs);
  color: var(--text-secondary);
}

.hub-extras summary {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  list-style: none;
  cursor: pointer;
  padding: 4px 6px;
  color: var(--text-secondary);
}

.hub-extras summary::-webkit-details-marker { display: none; }

/* The line wraps on a phone; a flex item's wrapped text is left-aligned
   by default, which put it off-centre under a centred frame. */
.hub-extras-text {
  text-align: center;
  min-width: 0;
}

.hub-extras summary:hover { color: var(--text-primary); }

.hub-extras-chevron {
  color: var(--text-muted);
  transition: transform var(--transition-base);
}

.hub-extras[open] .hub-extras-chevron { transform: rotate(180deg); }

.hub-extras p {
  margin: var(--space-xs) 0 0;
  padding: var(--space-sm) var(--space-md);
  border-radius: var(--radius-md);
  background: var(--canvas-off-white);
  color: var(--text-secondary);
  line-height: 1.5;
  text-align: center;
}

/* "Not quite right?" — one muted line under the stage. Quieter than the
   drawer handle above it: a redo is the exception, not the next step. */
.hub-redo {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-xs);
  margin: var(--space-sm) 0 0;
  text-align: center;
}

.hub-redo-row {
  display: inline-flex;
  align-items: center;
  gap: 2px;
}

.hub-redo-sep { color: var(--text-muted); }

.hub-redo-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 6px;
  background: none;
  border: 0;
  font-family: var(--font-body);
  font-size: var(--text-xs);
  font-weight: 500;
  color: var(--text-muted);
  cursor: pointer;
  transition: color var(--transition-fast);
}

.hub-redo-btn:hover,
.hub-redo-btn:focus-visible {
  color: var(--brand-primary);
  outline: none;
}

.hub-redo-btn:disabled { cursor: default; opacity: 0.6; }

.hub-redo-error {
  font-size: var(--text-xs);
  color: var(--status-error);
}

/* ── The wait, inside the frame ── */

/* The frame's off-white with a slow diagonal shimmer over it: alive, but
   quiet enough to sit next to a price. */
.hub-loader {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-md);
  width: 100%;
  height: 100%;
  padding: var(--space-lg);
  text-align: center;
  overflow: hidden;
}

.hub-loader::before {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(110deg, transparent 35%, var(--canvas-white) 50%, transparent 65%);
  background-size: 250% 100%;
  opacity: 0.7;
  animation: hub-shimmer 3.2s ease-in-out infinite;
  pointer-events: none;
}

@keyframes hub-shimmer {
  0%   { background-position: 120% 0; }
  100% { background-position: -30% 0; }
}

.hub-loader > * {
  position: relative;
}

.hub-loader-ring {
  width: 28px;
  height: 28px;
  border-radius: 50%;
  border: 2px solid var(--brand-primary-light);
  border-top-color: var(--brand-primary);
  animation: design-spin 1s linear infinite;
}

.hub-loader-bar {
  width: 160px;
  max-width: 100%;
  height: 3px;
  border-radius: 3px;
  background: var(--canvas-border);
  overflow: hidden;
}

/* Width is driven by GSAP; it starts at 0 so the first paint never
   shows a full bar. */
.hub-loader-fill {
  width: 0;
  height: 100%;
  border-radius: 3px;
  background: var(--brand-primary);
}

.hub-loader-milestone {
  margin-top: calc(-1 * var(--space-sm));
  font-size: var(--text-xs);
  color: var(--text-muted);
}

/* ── Failed: calm, centred in the frame, the retry and the phone side by side ── */

.hub-failed {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-sm);
  width: 100%;
  padding: var(--space-lg);
  color: var(--text-secondary);
  text-align: center;
}

.hub-failed-title {
  font-size: var(--text-base);
  font-weight: 500;
  color: var(--text-primary);
  margin: 0;
}

.hub-failed-body {
  font-size: var(--text-sm);
  line-height: 1.6;
  max-width: 42ch;
  margin: 0;
}

.hub-failed-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--space-sm) var(--space-md);
  margin-top: var(--space-xs);
}

/* btn-ghost is a <button> everywhere else; as a tel: link it needs the
   underline off. */
.hub-failed-actions a.btn-ghost {
  text-decoration: none;
}

.hub-error {
  font-size: var(--text-sm);
  color: var(--status-error);
  margin: 0;
}

/* ── After the card: the adjust row ──

   Shared. The design hub puts a link into its returning walk here, and
   the estimator's results screen puts the button that opens an edit of
   a finished estimate (db/018) in the same place, on the same slot of
   EstimateResults. Named for the card it sits under rather than for
   whichever page got there first. */

.estimate-after {
  width: 100%;
  margin-top: var(--space-md);
}

/* One line under the card: a single ghost control. On the hub it is a
   link, and btn-ghost is a <button> everywhere else, so as a link it
   needs the underline off. */
.estimate-adjust {
  display: flex;
  justify-content: center;
  margin: 0;
  font-size: var(--text-sm);
  color: var(--text-secondary);
  text-align: center;
}

.estimate-adjust a.btn-ghost {
  text-decoration: none;
}

/* At the hub's render cap the row is one sentence instead of a button. */
.estimate-cap {
  display: block;
  max-width: 46ch;
  margin: 0 auto;
  line-height: 1.5;
}

/* ── The add-photo drawer, hanging under the render ── */

/* The pool model card's photo drawer, on the render: a tab tucked under
   the frame's bottom edge (the frame sits above it, and its shadow falls
   on it), narrower than the frame so it reads as hanging from it, with
   the bottom corners only. The handle is the drawer's bottom edge, so it
   travels: closed, it is all that shows; open, the body has slid out
   above it and pushed it down. */
.hub-drawer {
  position: relative;
  z-index: 1;
  width: calc(100% - 2 * var(--space-2xl));
  margin: calc(-1 * var(--radius-lg)) auto 0;
  padding-top: var(--radius-lg);
  background: var(--canvas-off-white);
  border: 1px solid var(--canvas-border);
  border-top: none;
  border-radius: 0 0 var(--radius-lg) var(--radius-lg);
  overflow: hidden;
}

/* The upload surface is off-white on its own; on the drawer's off-white
   it goes to the card's white so the dashed target still reads as one. */
.hub-drawer .upload-zone {
  background: var(--canvas-white);
}

/* The handle: one line in brand colour, the chevron turning when open. */
.hub-drawer-toggle {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-xs);
  width: 100%;
  padding: var(--space-sm) var(--space-md);
  background: none;
  border: none;
  font-family: var(--font-body);
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--brand-primary);
  cursor: pointer;
  outline: none;
  transition: background var(--transition-fast), color var(--transition-fast);
}

.hub-drawer-toggle:hover {
  background: color-mix(in srgb, var(--brand-accent) 8%, transparent);
  color: var(--brand-primary-hover);
}

.hub-drawer-toggle:focus-visible {
  outline: 2px solid var(--brand-primary);
  outline-offset: -2px;
}

.hub-drawer-chevron {
  flex-shrink: 0;
  transition: transform var(--transition-base);
}

.hub-drawer.is-open .hub-drawer-chevron {
  transform: rotate(180deg);
}

/* Height is driven by GSAP (0 ⇄ auto), as the model card's drawer is. */
.hub-drawer-body {
  height: 0;
  overflow: hidden;
}

.hub-drawer-inner {
  padding: var(--space-lg);
}

.hub-drawer-lead {
  margin: 0 0 var(--space-md);
  font-size: var(--text-sm);
  color: var(--text-secondary);
  line-height: 1.5;
  text-align: center;
}

.hub-drawer-cta {
  display: flex;
  justify-content: center;
  margin-top: var(--space-md);
}

@media (max-width: 640px) {
  /* The column is the phone's width less its padding; the stage takes
     it as it is. */
  .hub-stage {
    left: auto;
    width: 100%;
    margin: var(--space-lg) 0 var(--space-xl);
    transform: none;
  }

  .hub-drawer {
    width: calc(100% - 2 * var(--space-lg));
  }

  .hub-drawer-inner {
    padding: var(--space-md);
  }
}

@media (prefers-reduced-motion: reduce) {
  .hub-loader::before { animation: none; }
  .hub-loader-ring { animation: none; }
  .hub-drawer-chevron { transition: none; }
}


/* ─────────────────────────────────────────────────────────
   404 ON A TENANT THAT EXISTS  (app/not-found.tsx)

   The header, footer and brand tokens come from the root layout, so
   this is only the centred column between them. Sized to sit at rest
   in the viewport rather than clinging to the top, which is what makes
   a short page read as designed instead of truncated.
   ───────────────────────────────────────────────────────── */

.error-page {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  width: 100%;
  max-width: var(--max-content, 680px);
  margin: 0 auto;
  min-height: 52vh;
  padding: var(--space-2xl) 0;
}

.error-page-headline {
  margin-bottom: var(--space-sm);
}

.error-page-body {
  max-width: 46ch;
  margin-bottom: var(--space-xl);
}

.error-page-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--space-md);
}

/* btn-primary is a <button> everywhere else; as a link it needs the
   underline off. */
.error-page-actions a.btn-primary {
  text-decoration: none;
}

.error-page-alt {
  margin-top: var(--space-md);
  font-size: var(--text-sm);
  color: var(--text-muted);
}

.error-page-alt a {
  color: var(--brand-primary);
  text-decoration: none;
  border-bottom: 1px solid transparent;
  transition: border-color var(--transition-fast);
}

.error-page-alt a:hover {
  border-bottom-color: var(--brand-primary);
}

/* Small print under the footer row — the design funnel's render
   disclaimer. Sized to the row's text and muted a step further. */
.site-footer-note {
  max-width: 68ch;
  margin: var(--space-md) auto 0;
  font-size: var(--text-xs);
  line-height: 1.6;
  color: var(--text-muted);
  text-align: center;
}
