@import url('/shared/brand-fonts.css?v=20260823-venue-card-tap');


:root {
  /* ----- Type roles -----
     Three faces, one job each. Two of them are expressive and are used as
     accents; the sans is the wall they hang on.

       --font-display  The Seasons. Hero headlines and major section titles.
       --font-script   Beautifully Delicious. Handwritten accents and the
                       occasional emotional phrase — a line at a time, never
                       a paragraph.
       --font-ui       DM Sans. Body copy, navigation, buttons, forms,
                       captions and data. The default: it is set on <body>
                       and inherited, so a new element gets it for free and
                       only a display/script element needs to say so.

     --font-script falls back to the DISPLAY serif rather than to generic
     `cursive`. The BD files are missing (see above), and `cursive` resolves
     to Comic Sans on Windows and Android — a novelty face is a far worse
     failure than simply looking like the rest of the brand. */
  --font-display: 'The Seasons', Cochin, 'Cormorant Garamond', Georgia, serif;
  --font-script: 'Beautifully Delicious', 'Snell Roundhand', 'The Seasons', Cochin, Georgia, serif;
  /* The club's own sans, for a line of brand voice. Falls through to the UI
     stack rather than a generic: if BD Sans fails, DM Sans is the right
     substitute — the line stays a sans and stays legible. */
  --font-accent: 'BD Sans', 'DM Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
  --font-ui: 'DM Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;

  /* ----- McDowell Caddie Club — Sandstone Brand Identity Kit -----
     Warm desert palette: Adobe Red, Aged Bronze, Sand Beige on a
     textured-stone backdrop. Variable NAMES are unchanged so every
     existing selector re-themes automatically; only the values moved
     from the old navy kit to the sandstone kit. */

  /* Brand palette (from the kit) */
  --adobe:     #B87333;             /* Adobe Red   */
  --bronze:    #7F6A4F;             /* Aged Bronze */
  --sand-beige:#F5E8D1;             /* Sand Beige  */

  /* LIGHT theme — sand-beige cards + dark bronze text on a sandstone
     backdrop, per the brand kit's business-card surfaces. */

  /* Surfaces */
  --bg:        #E7D6B8;             /* light sand (page, behind stone) */
  --panel:     #FBF4E6;            /* near-white cream card */
  --panel-2:   #F3E7D2;            /* lifted sand */
  --border:    rgba(127,106,79,0.34); /* Aged Bronze @ 34% */
  --border-2:  rgba(127,106,79,0.62);
  --divider:   rgba(60,42,24,0.12);

  /* Text — dark on light */
  --text:      #2E2114;             /* dark espresso (primary text) */
  --ink:       #2E2114;             /* alias for paper-aesthetic clarity */
  --cream:     #2E2114;             /* primary text (was light; now dark) */
  --on-accent: #F8EFDE;             /* light text on dark/accent surfaces */
  --muted:     #6E5A40;             /* bronze-taupe, readable on cream */

  /* Brand accent */
  --accent:        #B87333;          /* Adobe Red */
  --accent-dark:   #8A551F;          /* Darker terracotta (hover) */
  --accent-soft:   rgba(184,115,51,0.14);
  --link:          #8A551F;          /* darker adobe for links on light */
  --sand:          #E8DDC8;          /* Desert Sand */

  /* Status */
  --forest:    #5E5A3A;              /* warm olive (success) */
  --forest-2:  #43401F;
  --warn:      #B98328;              /* darker amber, readable on light */
  --danger:    #B5482E;              /* terracotta-red, readable on light */

  /* Radii (brandkit) */
  --radius-sm: 10px;
  --radius-md: 16px;
  --radius-lg: 24px;

  /* Shadows (soft warm) */
  --shadow-card:     0 14px 34px rgba(120,85,50,0.18);
  --shadow-elevated: 0 22px 50px rgba(120,85,50,0.26);

  /* Motion */
  --ease:     cubic-bezier(0.22, 0.61, 0.36, 1);
  --duration: 220ms;
  /* Shared motion system — one vocabulary for every transition so the app
     feels physically consistent. --ease-spring adds a gentle overshoot for
     presses/pops; the duration scale keeps fast/base/slow in proportion. */
  --ease-spring: cubic-bezier(0.34, 1.42, 0.5, 1);
  --dur-1: 130ms;
  --dur-2: 220ms;
  --dur-3: 340ms;

  /* Textured LIGHT sandstone backdrop. Layer order (top → bottom):
       1. soft edge vignette
       2. real photo — drop a tileable JPG at frontend/shared/sandstone.jpg
          and it takes over automatically; until then it 404s and the CSS
          layers below render
       3. coarse cloud mottle (visible stone variation)
       4. fine grain
       5. warm light base gradient */
  /* Copper texture — primary CTA surface (file picker, big buttons). */
  --copper:
    linear-gradient(180deg, rgba(255,238,200,0.20), rgba(0,0,0,0.18)),
    url("/shared/copper.jpg");
  --copper-size: 100% 100%, cover;
  --hairline: rgba(60, 42, 24, 0.12);
  --paper: #FBF4E6;
  --paper-soft: #F3E7D2;

  /* Soft inset field wells — form fields read as scooped OUT of the cream
     card surface (a tone deeper, with a faint inner shadow) instead of
     stark white boxes pasted ON it. No border; focus is a copper glow. */
  --field-bg:        rgba(58, 38, 20, 0.055);
  --field-bg-hover:  rgba(58, 38, 20, 0.08);
  --field-bg-focus:  rgba(58, 38, 20, 0.028);
  --field-inset:     inset 0 1px 3px rgba(58, 38, 20, 0.09);
  --field-ring:      rgba(184, 115, 51, 0.26);
  --field-radius:    12px;

/* Cast-bronze plate palette — every card / panel / consent box is
   a "548 house-number plate": dark burnished surface, polished
   copper frame, raised copper text. */
  --plate-dark:      #2B1F14;
  --plate-darker:    #1C140C;
  --plate-glow:      rgba(255, 200, 140, 0.08);
  --copper-bright:   #E4A87E;
  --copper-light:    #D4986B;
  --copper-mid:      #B87333;
  --copper-deep:     #8A551F;
  --copper-soft-on-dark: rgba(212, 152, 107, 0.65);

  --stone:
    url("/shared/sandstone.jpg"),
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='600' height='600'%3E%3Cfilter id='s'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.014' numOctaves='4' seed='7' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23s)' opacity='0.24'/%3E%3C/svg%3E"),
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23g)' opacity='0.10'/%3E%3C/svg%3E"),
    linear-gradient(#A88466, #A88466);
  --stone-size: cover, 600px 600px, 160px 160px, cover;
}

/* The body.ios-interface experiment (iOS nav bar + bottom tab bar replacing
   the topbar) was removed in 20260729-killios: the tab bar routed only
   #locker, and hiding the topbar killed the working hamburger drawer. The
   standard .topbar + .nav-drawer is the single navigation pattern again. */

/* No web-font import: the brand ships its own faces (The Seasons,
   Beautifully Delicious, BD Sans, DM Sans) as local @font-face, declared in
   brand-fonts.css and imported at the top of this file. Inter and Cormorant
   Garamond were fetched here; neither is used now, so the render-blocking
   third-party request goes with them. */

* { box-sizing: border-box; }
/* Snap to the screen: iOS Safari ignores overflow-x:hidden for touch
   panning, so the page could still drift sideways toward any element that
   poked past the edge. `clip` (Safari 16+) actually forbids the pan;
   `hidden` stays as the fallback for older engines. touch-action on the
   root limits background-originating gestures to vertical — inner
   scrollers (maps, tab bars) keep their own touch behavior. */
/* The status-bar allowance, named once because four different top-bar rules
   need it and they drifted apart the last time it was written out longhand.

   The page is drawn under the status bar in both runtimes -- viewport-fit=cover
   in a browser, and ios.contentInset "never" in the Capacitor shell -- so the
   top bar holds itself clear, once, here.

   It was briefly zeroed for the shell, when the shell used contentInset
   "always". That setting insets the WKWebView's SCROLL CONTENT, which does
   clear the page, but it also guarantees a scrollbar: a UIScrollView's
   scrollable range is content + insets - bounds, and this page is exactly
   bounds tall, so the inset IS the scroll. No CSS can reach that. The shell
   moved to "never" and the allowance came back here where it can be seen. */
:root { --safe-top: env(safe-area-inset-top, 0px); }

html, body {
  margin: 0; padding: 0; height: 100%;
  max-width: 100%;
  overflow-x: hidden;
  overflow-x: clip;
  /* `manipulation`, NOT `pan-y`: a document-level pan-y can be intersected
     into descendant gestures on iOS Safari and veto the decks' horizontal
     paging. Sideways document scroll is already impossible (overflow-x: clip),
     so all we actually want here is to kill double-tap zoom — which is exactly
     what manipulation does, while leaving panning on both axes to the decks. */
  touch-action: manipulation;
  /* No document-level rubber-band / pull-to-refresh: the app owns its own
     scrollers, so the page itself should never drag or bounce. */
  overscroll-behavior: none;
}
/* Form controls do NOT inherit font-family from their ancestors — the UA
   stylesheet gives button/input/select/textarea its own default (Arial on
   most engines). Without this reset the brand font stops at the edge of every
   control, so any button or field without an explicit rule silently renders a
   third typeface. This is what makes "The Seasons and Beautifully Delicious
   only" true rather than approximately true. */
button, input, select, textarea, optgroup {
  font-family: inherit;
}
/* Same leak, different UA default: code/pre/kbd/samp are given a monospace
   family by the browser, not by us. Note this trades away digit and column
   alignment in the few places that showed keys and JSON config — the brand
   has no monospace face, so tabular-nums is the closest substitute. */
code, pre, kbd, samp {
  font-family: inherit;
  font-variant-numeric: tabular-nums;
}
body {
  font-family: var(--font-ui);
  /* Bone limestone backdrop (warm cream solid). The MMR ridge is drawn by
     THREE cascading, seamlessly-tiling silhouette rows in body::after (see
     below): darkest in front, fading progressively lighter toward the back,
     all swaying gently LEFT<->RIGHT in unison (front travels most for a soft
     parallax). Horizontal only -- a calm, clean horizon, never a churn.
     Color is copper-deep (#8A551F). Reduced-motion freezes it.
     Landing and onboarding splashes still use the rich stone via
     var(--stone); only the signed-in chrome gets this calm surface. */
  background-color: #F2E8D4;
  color: var(--text);
  font-size: 14px;
  line-height: 1.5;
  letter-spacing: 0.01em;
}

/* ---- Living MMR horizon: THREE cascading ridge rows in one fixed layer.
   Front row is darkest and lowest; each row behind is lighter and its crest
   sits a little higher, so the peaks recede up the horizon exactly like real
   distant ranges. Every tile is authored seamless (left-edge height ==
   right-edge height). One slow ease-in-out keyframe sways all three
   horizontally in unison -- the front travels most, the back least, for a
   gentle parallax -- and NOTHING moves vertically. Layer order in
   background-image is front -> mid -> back (first paints on top), so a near
   ridge correctly occludes the fainter one behind it. Fixed + pointer-events
   none: it never intercepts input; reduced-motion freezes the sway. */
body::after {
  content: "";
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  height: 220px;
  z-index: -1;
  pointer-events: none;
  background-repeat: repeat-x, repeat-x, repeat-x;
  background-position: 0 bottom, 0 bottom, 0 bottom;
  /* Render each seamless tile SCALED DOWN uniformly (~0.43× the authored
     220px) so the whole ridge sits low — the tallest (back) crest lands ~72px
     above the bottom, just under the top of the persona switcher FAB
     (bottom:18px + 56px tall). Widths scale with the height (300→130 etc.) so
     the peaks keep their original steepness instead of stretching into wide,
     flat mesas. */
  background-size: 130px 95px, 138px 95px, 155px 95px;
  background-image:
    /* front — darkest, lowest broad ridge (opacity 0.15). Each tile's left and
       right edges sit on a short FLAT plateau at the same height, so the
       repeat-x seam lands on a horizontal segment and never shows as a vertical
       line cutting a slope. */
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='220' viewBox='0 0 300 220' preserveAspectRatio='none'%3E%3Cpath d='M0,166 L8,166 L54,150 L104,174 L150,150 L200,170 L250,154 L292,166 L300,166 L300,220 L0,220 Z' fill='%238A551F' opacity='0.15'/%3E%3C/svg%3E"),
    /* mid — copper at half strength (opacity 0.09) */
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='320' height='220' viewBox='0 0 320 220' preserveAspectRatio='none'%3E%3Cpath d='M0,116 L8,116 L44,100 L84,124 L120,98 L160,116 L200,102 L240,122 L280,104 L312,116 L320,116 L320,220 L0,220 Z' fill='%238A551F' opacity='0.09'/%3E%3C/svg%3E"),
    /* back — faintest, highest crest (opacity 0.05) */
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='360' height='220' viewBox='0 0 360 220' preserveAspectRatio='none'%3E%3Cpath d='M0,70 L9,70 L49,55 L94,78 L135,52 L180,70 L225,56 L270,76 L315,58 L351,70 L360,70 L360,220 L0,220 Z' fill='%238A551F' opacity='0.05'/%3E%3C/svg%3E");
  animation: mmr-sway 34s ease-in-out infinite;
}
/* Gentle left<->right sway. Front (first value) travels most; back least.
   background-position-y stays `bottom` on every stop, so it is purely
   horizontal. */
@keyframes mmr-sway {
  0%, 100% { background-position: -15px bottom, -10px bottom, -5px bottom; }
  50%      { background-position:  15px bottom,  10px bottom,  5px bottom; }
}
@media (prefers-reduced-motion: reduce) {
  body::after { animation: none; }
}

h1, h2, h3, .serif { font-family: var(--font-display); font-weight: 600; letter-spacing: 0.02em; }

/* `dvh` matches the visible viewport on mobile as the URL bar shows/hides,
   so the app shell snaps to the current screen instead of overflowing past
   the address bar. `vh` is the fallback for older browsers. */
.app-shell { display: grid; grid-template-rows: auto 1fr; height: 100vh; height: 100dvh; }

/* ---- Landing (pre-auth) ---- */
/* Landing — marketing splash over the warm sandstone backdrop.
   Cinematic vignette via :before; stacked column composes shield,
   wordmark, tagline, phone, CTA, and the secondary portal links. */
.landing {
  position: fixed;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  /* Cream limestone surface matching the in-app body. Stone texture
     dropped so the landing reads as a single calm canvas under the
     intro video and into the signed-in app. */
  background-color: #F2E8D4;
  background-image: none;
  z-index: 50;
  /* Tall content (long phone OTP error states, etc.) can scroll
     instead of getting clipped by flex centering. */
  overflow-y: auto;
  box-sizing: border-box;
}
/* Top + bottom flex spacers: with .landing-inner naturally-sized
   between them, the inner column centers vertically and the
   .landing-copyright (a normal-flow flex child at the very bottom)
   never gets overlapped. Replaces the older absolute-positioning +
   padding-bottom reservation, which got fragile every time a new
   form / error field added height to the inner. */
.landing::before,
.landing::after {
  content: '';
  flex: 1 1 auto;
  min-height: 0;
  pointer-events: none;
}

/* Somewhere for the keyboard scroll to go.
   The member hero is taller than the space above a phone keyboard, so the
   field sits under it and we let the browser scroll it into view. But
   .landing is position: fixed; inset: 0, which is sized from the LAYOUT
   viewport — and iOS doesn't shrink that when the keyboard opens. The card
   still exactly fills it, scrollHeight == clientHeight, and there is no
   scroll for the browser to perform, so Safari shifts the visual viewport
   instead: the fixed layer slides and fights the composition.
   Growing the bottom spacer while a field has focus gives the container
   real scroll range. It extends the scrollable area BELOW the card rather
   than touching the card itself, so nothing re-lays-out and the stack does
   not jump — the page simply becomes scrollable at the moment it needs to
   be. Browsers without :has() just keep today's behaviour.

   Scoped to .member-landing deliberately. Only the member hero uses the
   .landing-card layout, where the card already fills the container and
   these spacers collapse to zero — so resizing one adds scroll range
   without moving anything. On the staff landings the spacers are what
   centres .landing-inner, and pinning the bottom one would shove their
   content down the screen on focus. */
.member-landing:has(.landing-input:focus)::after {
  flex: 0 0 auto;
  height: 46vh;
}
/* Leaves the field clear of the keyboard's top edge once scrolled to,
   rather than flush against it. */
.landing-field,
.auth-flip-mount {
  scroll-margin-bottom: 24px;
}
.landing-inner {
  position: relative;
  z-index: 1;
  width: 100%;
  max-width: 380px;
  padding: 32px 24px 28px;
  text-align: center;
  display: flex;
  flex-direction: column;
  align-items: center;
  /* Pinned-natural-height between the ::before / ::after flex spacers
     above; no auto margin needed. flex-shrink: 0 keeps the column from
     compressing into the spacers when content overflows. */
  flex-shrink: 0;
}
.landing-lockup {
  /* The full club lockup (crest + script McDowell + rule + Caddie Club caps)
     as ink-on-transparent art on the cream landing. Roughly square
     (~1727×1787), so width drives height: sized to fill the footprint the
     old shield + text wordmark stack had. White-label tenants never see it —
     tenant.js hides the art and unhides the text wordmark on their paint. */
  width: 100%;
  max-width: 460px;
  height: auto;
  display: block;
  margin: 0 auto 26px;
  /* No filter until the PNG is fully decoded: computing drop-shadow from a
     partially-decoded alpha channel during the intro fade left "noise" in
     the artwork's corners on first land (clean after refresh, when the image
     comes decoded from cache). app.js adds .shadow-ready after
     img.decode() resolves. */
}
/* ---- Member hero: centred on the wordmark ----------------------------
   The two-line wordmark holds the optical centre of the card, and the rest
   of the hero distributes around it: slogan + crest share the half above,
   number + button share the half below. Both halves are `flex: 1 1 0`, so
   they are always exactly equal and the wordmark lands dead centre whatever
   the screen height — no magic offsets to go stale the next time something
   is resized.

   space-evenly inside each half rather than space-between: it puts equal air
   at the outer edges too, so the crest doesn't ride up against the wordmark
   while the slogan floats off the top of the card. */
.member-landing .landing-card[data-landing-card="hero"] .landing-inner {
  flex: 1 1 auto;
  justify-content: space-between;
  min-height: 0;
}
.member-landing .hero-top,
.member-landing .hero-bottom {
  /* `flex: 1 1 0` with `min-height: 0` made the halves exactly equal — and let
     them shrink BELOW their own content, which on a short screen spilled the
     crest down over the wordmark. Dead-centre is a nicety; a collision is a
     defect. Growing from a content basis keeps the halves equal wherever there
     is room and simply lets the taller one take what it needs, so the wordmark
     drifts off centre instead of being overlapped. */
  flex: 1 1 auto;
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: space-evenly;
}
/* Both carry .landing-lockup for the tenant hook, so both need to opt out of
   its sizing.

   EVERY rule for the crest sizes it by WIDTH and leaves height: auto. Never
   set both: the cascade merges a width from one rule with a height from
   another into a fixed box, and an <img> stretches to fill it. That is exactly
   what shipped — a width here plus a height in the short-screen branch below
   rendered the portrait shield at 1.6:1, squashed flat. */
.member-landing .landing-crest-link {
  flex: 0 0 auto;
  /* Scaled to 0.9 of the sizes that shipped (was clamp(132px, 46vw, 260px)).
     Patrick, 2026-08-19: "can we also scale down slightly the crest and
     mcdowell caddie club text?" — so BOTH marks take the same 0.9, here and on
     .landing-wordmark-art below. The factor has to be shared: the note further
     down pins crest:wordmark = 0.71 as the mock's defining proportion, and
     shrinking one mark alone is precisely how that ratio drifts. */
  width: clamp(119px, 41.4vw, 234px);
  height: auto;
  max-width: 88%;
  margin: 0;
}
/* The crest is a link now (to /about), so the sizing above moved from the
   <img> to the <a> that wraps it — the anchor is what hero-top lays out — and
   the image simply fills whatever box the anchor gets. Width only, height
   auto, exactly as the note above demands: the anchor and the image must never
   each contribute one axis, or the portrait shield renders stretched. */
.landing-crest-link {
  display: block;
  line-height: 0;          /* no descender gap under the image */
  border-radius: var(--radius-md);
  -webkit-tap-highlight-color: transparent;
}
.landing-crest {
  width: 100%;
  height: auto;
}
/* The only affordance a bare crest can carry: it answers a press. Kept to a
   scale rather than an underline or a border — the crest is the brand mark
   first and a control second, and anything drawn on top of it reads as damage
   to the logo. */
@media (prefers-reduced-motion: no-preference) {
  .landing-crest-link { transition: transform var(--dur-2) var(--ease-spring); }
}
.landing-crest-link:active { transform: scale(0.97); }
@media (hover: hover) {
  .landing-crest-link:hover { transform: scale(1.02); }
}
/* Keyboard reach. focus-visible only, so a tap never paints a ring. */
.landing-crest-link:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 6px;
}
.member-landing .landing-wordmark-art {
  flex: 0 0 auto;
  width: 100%;
  max-width: 100%;
  height: auto;
  margin: 0;
}
.landing-lockup.shadow-ready {
  /* Lighter drop-shadow to match the cream surface (was rgba(20,10,4,
     0.55) for the dark stone). */
  filter: drop-shadow(0 10px 22px rgba(110, 66, 32, 0.25));
}
.landing-wordmark {
  display: inline-block;
  font-family: var(--font-display);
  font-size: clamp(28px, 6.5vw, 42px);
  font-weight: 600;
  line-height: 1.04;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: #3A2614;
  margin: 0 auto 26px;
  padding: 24px 12px 18px;
  /* text-frame-landscape.png was a dark-on-stone ornament; on cream
     it reads as a heavy plate. Drop it; let the typography breathe.
     Subtle copper rule under the wordmark for the heritage cue. */
  background: none;
  border: 0;
  border-bottom: 1px solid rgba(138, 85, 31, 0.45);
  border-radius: 0;
  text-shadow: none;
}

/* The 'c' in 'McDowell' rendered as a small lowercase letter with a
   thin copper-textured bar underneath -- same copper as the
   Continue button, just a fraction the height. Inherits color from
   the wordmark but breaks out of the uppercase transform via
   text-transform: lowercase. */
.landing-wordmark .mc-c {
  position: relative;
  text-transform: lowercase;
  /* Sized + raised so the small c reads as a proper superior letter that fits
     WITHIN the cap band — its top aligns with the M/D cap line instead of
     poking above it, its foot a hair above the baseline. */
  font-size: 0.72em;
  letter-spacing: 0;
  vertical-align: 0.34em;
  margin: 0 0.06em 0 0.03em;
  padding-bottom: 0;
  line-height: 1;
}
/* Thin copper-textured hairline directly under the c — the ordinal-style
   underline of the superior letter. */
.landing-wordmark .mc-c::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 2px;
  background-color: #8A551F;
  background-image: var(--copper);
  background-size: var(--copper-size);
  background-position: center;
  border-radius: 1px;
  box-shadow: 0 1px 2px rgba(58, 38, 20, 0.30);
}
/* Overline above the crest on the Member landing — the positioning line,
   set in The Seasons (the brand's display serif, same family as the
   headings). Quiet but legible: it sits above the lockup and should read as
   a whispered premise rather than a banner, so it stays well under the
   wordmark — but The Seasons has a small x-height and fine strokes, and at
   13px on cream it was genuinely hard to read. Sized up to 16/18px, which
   is comfortable while still sitting clearly below the lockup.
   Sentence case, no uppercase transform — the ellipsis is doing the work
   and tracking it out would break the beat. */
.landing-overline {
  font-family: var(--font-display);
  /* Scales with the screen instead of sitting at a flat 16px. It was only
     226px of a 393px phone — the line is the first thing read and had room
     to spare. The ceiling is where it starts competing with the "Caddie
     Club" caps in the art below it. Held just under the width that would
     wrap "Uber…the second to last mile" onto a second line on a 390px
     handset — the joke only lands as one line. */
  /* Sized to fit the hero column on one line — the joke only lands unbroken.
     The column is 72vw now and the line runs ~14.1x the font size, so it has
     to sit under 72/14.1 = 5.1vw. Measured at 5.1 it still wrapped — that
     coefficient is the wrap point, not a safe value — so 4.7vw with headroom. */
  font-size: clamp(15px, 4.7vw, 24px);
  /* 700 resolves to the real The Seasons Bold face (theseasons-bd.otf), not a
     synthesised one — the same weight the Danny CTA uses, so the two pieces of
     brand type on the landing now match.

     This is the CEILING, not a choice. The Seasons ships exactly two faces —
     theseasons-reg.otf at usWeightClass 400 and theseasons-bd.otf at 700.
     There is no semibold. Asking for `font-weight: 600` would resolve, by the
     CSS font-matching rules, straight back to the 700 file: identical pixels,
     no change on screen. Anything that makes this line read heavier has to do
     it some other way, which is what the two declarations below are for. */
  font-weight: 700;
  font-style: normal;
  letter-spacing: 0.03em;
  line-height: 1.4;
  /* Near-black brown rather than the old #4A3220. The Seasons is a
     high-contrast display serif: its hairlines are genuinely thin at 15–24px,
     and on cream the eye reads thin strokes as light type. Contrast is the
     lever the typeface leaves us — this is most of the perceived weight gain,
     and it costs nothing. */
  color: #2E1C0E;
  /* And the rest of it. A hairline stroke in the same ink thickens every
     stroke uniformly, which is precisely the thing a missing semibold would
     have done. Kept at 0.3px deliberately: past ~0.5px the counters in the
     'e' and 'a' start to close up at 15px and it reads muddy instead of
     bold. Patrick, 2026-08-09: "a little too thin for that size." */
  -webkit-text-stroke: 0.3px #2E1C0E;
  margin: 0 0 16px;
  text-shadow: none;
}
@media (min-width: 720px) {
  .landing-overline { font-size: 18px; margin-bottom: 18px; }
}
.landing-tagline {
  /* The one line on a sign-in screen set in the club's own script, so it
     rhymes with the "McDowell" directly above it in the lockup art.

     REWRITTEN 2026-08-20, when the licensed Beautifully Delicious cut landed.
     Until then --font-script had no @font-face behind it and this fell through
     to Snell Roundhand or Georgia, and the rule was tuned for THAT: 13px,
     weight 500, 0.12em tracking, uppercase. Rendered with the real face those
     four values fight it -- a script's legibility lives in connected lowercase
     letterforms, and setting one in caps with a tenth of an em between the
     letters takes the joins apart. Measured at 390px/DPR 3 the old values gave
     "FILLED TABLES. BETTER NIGHTS." as a row of disconnected marks.

     So: sentence case, tracking off, and up to 20px. Same reasoning as the
     home greeting's subline -- this face has a very small x-height and needs
     the size a serif would not.

     Patrick, 2026-08-22: briefly moved to 700 on "make the script font
     heavier still" -- reverted the same day once he clarified he meant the
     GREETING subline (.home-greeting .greet-sub below), not this one. Back
     to Regular; nothing here was actually asking to change. */
  font-family: var(--font-script);
  font-size: 20px;
  font-style: normal;
  font-weight: 400;
  letter-spacing: 0.01em;
  line-height: 1.4;
  text-transform: none;
  color: #3A2614;
  margin: 0 0 26px;
  text-shadow: none;
}
/* Desktop: still single column, just larger scale and tighter rhythm */
@media (min-width: 720px) {
  .landing-inner { max-width: 460px; padding: 48px 32px; }
  .landing-lockup { width: 420px; max-width: 72%; margin-bottom: 26px; }
  .landing-tagline { font-size: 22px; margin-bottom: 30px; }
}
/* Caddie sign-in only. Patrick, 2026-08-22: "'drive the neighborhood' script
   needs to match the same subtitle script font weight/size" -- the subtitle
   script being .home-greeting .greet-sub, which he'd just had bumped to the
   real shipped Bold plus a 0.4px stroke at a size that scales with the
   viewport (see that rule, below). Copied verbatim rather than approximated,
   for the same reason that rule's own comment gives: this face's weights
   barely separate below the size where .greet-sub's own measurement pinned
   the difference (~46.8px) -- reusing its exact clamp() is what actually
   gets there, not a guess at a slightly-bigger fixed px.
   Scoped to .caddie-body only -- the ask named the Caddie login specifically,
   not the other three portals' sign-in taglines, and .caddie-body already
   scopes per-portal overrides elsewhere in this file (see the topbar rules).
   Overrides the desktop 22px bump above by specificity, at every width, on
   purpose: a fixed 22px would undo the whole point of matching a clamp().
   Verified in Chromium at 320/375/393/430px: "Drive the neighborhood."
   stays on one line at every width, unlike .greet-sub's 24 rotating lines
   which were measured against a wider column -- this one is safely shorter
   than the widest of those, so no new wrap check was needed beyond this. */
.caddie-body .landing-tagline {
  font-size: clamp(36px, 11.6vw, 48px);
  font-weight: 700;
  -webkit-text-stroke: 0.4px currentColor;
}
.landing-copyright {
  /* In-flow flex child sitting after the ::after spacer, so it never
     gets overlapped by .landing-inner — earlier absolute-positioning
     plus padding-bottom reservation was fragile every time a new
     form / error field grew the inner column. */
  position: static;
  flex-shrink: 0;
  width: 100%;
  margin-bottom: 12px;
  padding: 0 8px;
  font-family: var(--font-ui);
  font-size: clamp(8px, 2.4vw, 11px);
  letter-spacing: 0.02em;
  color: rgba(58, 38, 20, 0.5);
  text-align: center;
  /* Must wrap: with nowrap the entity + email line overflowed the right
     edge on phones, giving iOS a horizontal pan target (the page "drifted"
     sideways). Normal wrapping keeps every pixel inside the viewport. */
  white-space: normal;
  overflow-wrap: break-word;
  text-shadow: none;
}
.landing-copyright-mail {
  white-space: nowrap; /* keep the email itself on one line when it wraps */
  color: rgba(58, 38, 20, 0.68);
  text-decoration: none;
  border-bottom: 1px solid rgba(138, 85, 31, 0.3);
}
.landing-copyright-mail:hover {
  color: #5C3F26;
  border-bottom-color: rgba(138, 85, 31, 0.7);
}
.landing-inner .landing-other a { color: #5C3F26; }
.landing-inner .landing-other a:hover { color: #8A551F; }
.landing-inner .landing-other .dot { color: rgba(58, 38, 20, 0.45); }
.landing-inner .landing-hint { color: #5C3F26; }
.landing-inner .landing-error { color: #B0432D; }
/* First-visit consent screen — problem / solution / brand pitch + CTA.
   Cast-bronze 548 plate: dark surface, polished copper frame, embossed
   copper text. The "I'm in!" CTA stays full copper texture. */
#auth-step-consent {
  max-width: 360px;
  margin: 0 auto 22px;
  text-align: left;
  padding: 24px 24px 20px;
  background-color: var(--panel);
  background-image: none;
  border: 1px solid rgba(184, 115, 51, 0.25);
  border-left: 3px solid var(--copper);
  border-radius: 10px;
  color: var(--ink);
  box-shadow: 0 14px 34px rgba(120, 85, 50, 0.12);
}
#auth-step-consent .landing-btn { margin-top: 10px; }
.consent-block { margin-bottom: 20px; }
.consent-block:last-of-type { margin-bottom: 22px; }
.consent-h {
  font-family: var(--font-display);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--copper-dark);
  margin: 0 0 6px;
  text-shadow: none;
}
.consent-p {
  font-family: var(--font-ui);
  font-size: 19px;
  line-height: 1.4;
  color: var(--ink);
  margin: 0;
}

/* iPhone-first compression: shrink the lockup, sub, and consent plate
   so the entire consent screen fits inside the viewport without
   scrolling or pinch-zoom on phones. */
@media (max-width: 480px), (max-height: 800px) {
  .landing-inner { padding: 18px 16px 22px; }
  .landing-lockup { max-width: 240px; margin-bottom: 12px; }
  .landing-sub { font-size: 11px; letter-spacing: 0.32em; margin: 6px 0 14px; }
  #auth-step-consent { padding: 18px 20px 16px; margin-bottom: 14px; }
  .consent-block { margin-bottom: 14px; }
  .consent-block:last-of-type { margin-bottom: 16px; }
  .consent-h { font-size: 10px; letter-spacing: 0.22em; margin: 0 0 4px; }
  .consent-p { font-size: 16px; line-height: 1.32; }
  .landing-other { font-size: 11px; }
  #auth-step-consent .landing-btn { padding: 12px; min-height: 44px; font-size: 14px; }
}

.landing-sub {
  font-family: var(--font-ui);
  font-size: 14px;
  font-weight: 800;
  letter-spacing: 0.45em;
  text-transform: uppercase;
  color: var(--muted);
  margin: 14px 0 36px;
}
.landing-rule {
  width: 28px;
  height: 1px;
  background: var(--accent);
  margin: 0 auto 28px;
  border: 0;
}
.landing-input {
  width: 100%;
  background: var(--plate-darker);
  border: 1px solid var(--copper-mid);
  color: var(--copper-light);
  padding: 14px 16px;
  font-size: 20px;
  font-weight: 500;
  text-align: center;
  font-family: var(--font-ui);
  letter-spacing: 0.06em;
  margin-bottom: 20px;
  border-radius: 4px;
  transition: border-color var(--duration) var(--ease), box-shadow var(--duration) var(--ease);
}
.landing-input:focus { outline: none; border-color: var(--copper-bright); box-shadow: 0 0 0 3px rgba(228, 168, 126, 0.20); }
.landing-input::placeholder { color: var(--copper-soft-on-dark); letter-spacing: 0.2em; text-transform: uppercase; font-size: 13px; font-weight: 500; }

/* Unified landing — no field box at all. A bare copper hairline just wider
   than a phone number, centered, with the typewriter Easter egg on page hit
   (famous-numbers.js; types a rotating classic, deletes it) followed by the
   caret cue blinking dead center — iOS shows no native caret without a tap,
   so the fake one stands in. Underline warms + doubles on focus. */
.landing .landing-input {
  width: 19ch;
  max-width: calc(100vw - 96px);
  margin: 0 auto 20px;
  background: transparent;
  border: none;
  border-bottom: 1px solid rgba(138, 85, 31, 0.55);
  box-shadow: none;
  border-radius: 0;
  color: #3A2614;
  -webkit-text-fill-color: #3A2614;
  caret-color: var(--accent);
  font-size: 18px;
  font-weight: 400;
  letter-spacing: 0.04em;
  padding: 10px 4px 8px;
  text-align: center;
}
.landing-demo-value {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  color: #3A2614;
  font: 400 18px/1 var(--font-ui);
  letter-spacing: 0.04em;
  white-space: nowrap;
  pointer-events: none;
}
.landing .landing-input:hover { background: transparent; }
.landing .landing-input:focus {
  background: transparent;
  border-bottom-color: var(--copper-deep);
  box-shadow: 0 1px 0 0 var(--copper-deep);
}
/* iOS/Chrome contact autofill paints a yellow rectangle over transparent
   fields by default. Keep the bare underline and readable ink instead. */
.landing .landing-input:-webkit-autofill,
.landing .landing-input:-webkit-autofill:hover,
.landing .landing-input:-webkit-autofill:focus {
  -webkit-text-fill-color: #3A2614;
  caret-color: var(--accent);
  -webkit-box-shadow: 0 0 0 1000px #F2E8D4 inset, 0 1px 0 var(--copper-deep);
  box-shadow: 0 0 0 1000px #F2E8D4 inset, 0 1px 0 var(--copper-deep);
  border-bottom-color: var(--copper-deep);
}
/* While the caret cue is blinking the placeholder would sit underneath it —
   keep the field visually empty until the real caret takes over. */
.landing-field .landing-input.seed-hint::placeholder { color: transparent; }
.landing .landing-input::placeholder {
  color: #8A7560;
  letter-spacing: 0.16em;
  font-size: 12px;
  font-weight: 400;
}

/* Role picker on the unified landing — translucent plate (.landing
   .landing-input above) with a soft-copper dropdown chevron. */
.landing select.landing-role {
  font-size: 16px;
  letter-spacing: 0.06em;
  appearance: none;
  -webkit-appearance: none;
  background-image:
    linear-gradient(45deg, transparent 50%, rgba(212, 152, 107, 0.55) 50%),
    linear-gradient(135deg, rgba(212, 152, 107, 0.55) 50%, transparent 50%),
    linear-gradient(rgba(28, 20, 12, 0.38), rgba(28, 20, 12, 0.38));
  background-position:
    calc(100% - 18px) 50%,
    calc(100% - 12px) 50%,
    0 0;
  background-size: 6px 6px, 6px 6px, 100% 100%;
  background-repeat: no-repeat;
  padding-right: 36px;
  cursor: pointer;
}
.landing select.landing-role option { background: var(--plate-dark); color: var(--copper-light); }
/* Wrap around a seeded input so we can render a blinking caret cue that
   draws the eye to the field — only visible while the seed-hint class
   is still on the input (the JS strips it the moment the user focuses
   or types, at which point the real native caret takes over). */
/* Wrap around the landing underline. The LABEL is the real tap target:
   the visual field is a 17ch hairline (~170×40px — below the 44pt touch
   minimum, and misses read as 'can't type'), so the wrapper stretches to a
   forgiving 320×64 zone and tapping anywhere in it focuses the input. */
.landing-field {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  max-width: 320px;
  min-height: 64px;
  margin: 0 auto 20px;
  cursor: text;
  -webkit-tap-highlight-color: transparent;
}
.landing-field .landing-input { margin-bottom: 0; flex: 0 0 auto; }

/* Twilio SMS opt-in — visible next to the phone field, never collapsed. */
.sms-consent {
  margin: 12px auto 4px;
  max-width: 420px;
  text-align: left;
}
.sms-consent-notice {
  margin: 0 0 10px;
  font-size: 12px;
  line-height: 1.45;
  color: #3A2614;
  letter-spacing: 0;
  text-transform: none;
}
.sms-consent-notice a {
  color: #8A551F;
  text-decoration: underline;
}
.sms-consent-check {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  font-size: 13px;
  line-height: 1.4;
  color: #3A2614;
  cursor: pointer;
}
.sms-consent-check input {
  accent-color: #8A551F;
  width: 18px;
  height: 18px;
  margin-top: 2px;
  flex: 0 0 auto;
}
.landing-card[data-landing-card="hero"] .sms-consent {
  max-width: none;
  width: 100%;
}

/* Standing prompt above the single auth field. Deliberately a real, always-
   present line rather than a placeholder: a placeholder clipped at 17ch
   ("ENTER PHONE…") inside this centred field, and it would vanish the moment
   the member started typing — which is exactly when they might still be
   unsure what the field wants. UI role, because it is a form label. */
.landing-field-hint {
  display: block;
  font-family: var(--font-ui);
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: rgba(46, 33, 20, 0.52);
  /* Sits UNDER the rule the number is typed on, so it reads as a caption for
     that line rather than a label floating over an empty field. margin-TOP,
     therefore — it needs to hug the rule above it, not the button below. */
  margin-top: 8px;
  margin-bottom: 0;
  transition: opacity var(--duration, 0.2s) var(--ease, ease);
}
.landing-field .caret-cue {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 2px;
  height: 24px;
  margin: -12px 0 0 -1px;
  background: var(--accent);
  border-radius: 1px;
  pointer-events: none;
  display: none;
}
.landing-field .landing-input.seed-hint:not(.is-typing) ~ .caret-cue {
  display: block;
  animation: caret-blink 1.05s steps(2, end) infinite;
}
@keyframes caret-blink {
  0%, 50% { opacity: 1; }
  50.01%, 100% { opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  /* These members skip the typewriter (see runLandingEgg) but still get the
     cue — shown, just not blinking. */
  .landing-field .landing-input.seed-hint:not(.is-typing) ~ .caret-cue {
    animation: none;
    opacity: 1;
  }
}
.landing-btn {
  width: 100%;
  max-width: 280px;
  margin: 0 auto;
  display: block;
  background-image: var(--copper);
  background-size: var(--copper-size);
  background-repeat: no-repeat;
  color: #FFFFFF;
  text-shadow: 0 1px 2px rgba(40, 22, 6, 0.55);
  border: 1px solid rgba(40, 22, 6, 0.55);
  padding: 14px 22px;
  border-radius: 4px;
  font-family: var(--font-ui);
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  cursor: pointer;
  transition: filter var(--duration) var(--ease), background-image var(--duration) var(--ease), box-shadow var(--duration) var(--ease);
  min-height: 48px;
  /* Clean copper: dropped the inset bevel highlights + heavy drop shadow that
     read as a plasticky faux-3D button; a slim shadow keeps it lifted. */
  box-shadow: 0 2px 6px rgba(40, 22, 6, 0.16);
}
.landing-btn:hover { filter: brightness(1.06); box-shadow: 0 3px 9px rgba(40, 22, 6, 0.22); }
.landing-btn:disabled { opacity: 0.45; cursor: not-allowed; filter: saturate(0.55); }
.landing-hint {
  margin-top: 20px;
  font-size: 11px;
  color: var(--muted);
  letter-spacing: 0.05em;
}
.landing-error {
  margin-top: 16px;
  font-size: 12px;
  color: var(--danger);
  letter-spacing: 0.04em;
}
.landing-other {
  display: flex;
  justify-content: center;
  align-items: baseline;
  flex-wrap: nowrap;
  gap: 12px;
  margin-top: 40px;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.22em;
  text-transform: uppercase;
}
.landing-other a {
  color: var(--muted);
  text-decoration: none;
  font-weight: 700;
  white-space: nowrap;
  margin: 0;
}
.landing-other .dot { color: var(--muted); opacity: 0.55; }
.landing-other a:hover { color: var(--accent); }

.topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  position: relative;
  z-index: 90;
  padding: calc(10px + var(--safe-top)) calc(22px + env(safe-area-inset-right)) 10px calc(22px + env(safe-area-inset-left));
  /* Espresso band: the header separates crisply from the cream page body, and
     the reversed white-line crest (shield-white.png) reads at full contrast
     against it — the bronze mark is a mid-tone metal that muddied here. Also a
     frosted material: near-opaque gradient over a blur, so overscroll or
     standalone-PWA gaps read as smoked glass instead of tearing. */
  background: linear-gradient(180deg, rgba(36, 24, 17, 0.94) 0%, rgba(26, 16, 9, 0.96) 100%);
  -webkit-backdrop-filter: blur(14px) saturate(1.15);
  backdrop-filter: blur(14px) saturate(1.15);
  /* No copper hairline under the bar. The espresso band already separates
     from the cream page on its own, and the soft drop shadow below carries
     the edge — a drawn rule on top of both read as a seam. */
  box-shadow: 0 10px 30px rgba(10, 5, 2, 0.28);
}
.topbar .brand {
  display: flex;
  align-items: center;
  gap: 14px;
  text-decoration: none;
  color: inherit;
  /* Flex items floor at their content width by default, which would let a
     long wordmark shove the nav off the bar. Allowing the lockup to shrink
     puts the truncation in .name (below), where it degrades gracefully. */
  min-width: 0;
}
/* The crest is the drawer trigger wherever the inline links are hidden (see
   nav.js, which reads that same computed display rather than duplicating the
   breakpoint). A hairline caret is the entire affordance — enough that the
   lockup reads as tappable, well short of reinstating the icon we just
   removed. Above 1000px the links row is visible, so there is nothing to
   reveal and no caret is drawn. */
@media (max-width: 1000px) {
  .topbar .brand::after {
    content: '';
    flex: 0 0 auto;
    width: 6px;
    height: 6px;
    margin-left: 1px;
    border-right: 1.5px solid rgba(201, 161, 94, 0.7);
    border-bottom: 1.5px solid rgba(201, 161, 94, 0.7);
    transform: translateY(-2px) rotate(45deg);
    transition: transform var(--duration, 0.2s) var(--ease, ease);
  }
  .topbar .brand[aria-expanded='true']::after {
    transform: translateY(1px) rotate(-135deg);
  }
}
.topbar .brand .mark {
  /* The crest now LEADS the lockup rather than matching the wordmark art
     beside it (72-84px) — it is the mark people recognise at a glance on a
     phone. The bar sizes to its tallest child, so this grows the band by the
     difference rather than cropping the crest.

     Height-driven with width:auto, not a square box. The crest art is
     745x1038 (~0.72:1), so a square box left ~28% of its own width as dead
     space that the .brand gap then widened further — the crest read smaller
     than its box implied and sat further from the wordmark than intended. */
  height: 92px;
  width: auto;
  display: block;
  /* Soft lift so the reversed crest sits on the bar rather than in it. Lighter
     than the bronze mark's shadow — white line art needs separation, not the
     depth a mid-tone metal plaque wanted. */
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.4));
}
.topbar .brand .wordmark {
  display: flex;
  flex-direction: column;
  justify-content: center;
  line-height: 1;
  min-width: 0; /* see .brand — lets .name be the thing that truncates */
  /* No copper rule: the underline belonged to the type-set wordmark. The
     wordmark ART carries its own baseline and reads cleaner floating next
     to the crest. */
}
.topbar .brand .wordmark .name {
  font-family: var(--font-display);
  /* Scales with the bar instead of sitting at a fixed 20px, so the club name
     is as large as the width allows. Capped at 26px — past that it starts
     competing with the page's own headings. */
  font-size: clamp(22px, 2.4vw, 26px);
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  /* The club name is a wordmark, not a sentence — it breaks across two lines
     rather than shrinking, which reads as a layout bug. Cochin is Apple-only,
     so a Windows/Android visitor gets Georgia or worse at up to ~15% wider;
     on a narrow phone that lands as an ellipsis on one clean line instead of
     the two-line wrap this used to produce. */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  /* Warm ivory on the espresso bar — high contrast, reads as engraved gold. */
  color: #F1E4C9;
}
/* The club name as shipped art — the white script/serif wordmark on the
   dark bar. Parked next to the text .name (hidden), which tenant.js swaps
   back in when a licensee brand paints: they have no rights to the MCC
   artwork. Sized near crest height so the brand reads as one engraved
   lockup; aspect is ~5.1:1, so width follows. */
.topbar .brand .wordmark .name-img {
  height: clamp(72px, 7.2vw, 84px);
  width: auto;
  max-width: 100%;
  display: block;
  object-fit: contain;
  /* Same soft lift the reversed crest gets — white line art needs
     separation from the bar, not depth. */
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.4));
}
.topbar .brand .wordmark .sub {
  font-family: var(--font-ui);
  font-size: 9px;
  font-weight: 600;
  color: #C9A15E;
  letter-spacing: 0.34em;
  text-transform: uppercase;
  margin-top: 5px;
}
.topbar .links {
  display: flex;
  align-items: center;
  gap: 2px;
}
.topbar .links a {
  display: inline-flex;
  align-items: center;
  min-height: 36px;
  padding: 0 10px;
  border-radius: 8px;
  color: #D9C7A6;
  text-decoration: none;
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  transition: color 160ms ease, background-color 160ms ease;
}
.topbar .links a:hover { color: #EFC98F; background: rgba(184, 115, 51, 0.14); }
.topbar .links a:focus-visible,
.topbar .brand:focus-visible,
@media (prefers-reduced-motion: reduce) {
  .topbar .links a { transition: none; }
}


.nav-drawer {
  position: fixed;
  /* Anchored to the bar's MEASURED height, published as --topbar-h by nav.js.
     This was a hardcoded 64px, which silently stopped matching the moment the
     bar changed size — the crest resize pushed the bar to 102px on a phone and
     the drawer's first item ("Concierge desk") ended up behind it. The bar has
     three heights (default, ride-active, desktop) and its padding already
     includes the safe-area inset, so a literal here can only ever be right for
     one of them. The fallback is the phone default, for the frame before JS
     measures. */
  top: calc(var(--topbar-h, 102px) + 6px);
  right: calc(12px + env(safe-area-inset-right));
  width: 220px;
  max-width: calc(100vw - 24px);
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 14px;
  box-shadow: 0 24px 60px rgba(0,0,0,0.45);
  padding: 6px;
  display: flex;
  flex-direction: column;
  z-index: 100;
  opacity: 0;
  transform: translateY(-6px);
  pointer-events: none;
  transition: opacity 180ms ease, transform 180ms ease;
}
.nav-drawer.open {
  opacity: 1; transform: translateY(0); pointer-events: auto;
}
.nav-drawer a {
  display: block;
  padding: 12px 14px;
  color: var(--cream);
  text-decoration: none;
  font-size: 13px;
  letter-spacing: 0.06em;
  border-radius: 10px;
}
.nav-drawer a:hover { background: rgba(255,255,255,0.04); color: var(--accent); }
.nav-drawer a + a { border-top: 1px solid var(--border); }
@media (prefers-reduced-motion: reduce) {
  .nav-drawer { transition: none; }
}

.layout { display: grid; grid-template-columns: 400px 1fr; height: 100%; }
.layout.full { grid-template-columns: 1fr; }
.sidebar { background: var(--panel); border-right: 1px solid var(--border); overflow-y: auto; padding: 20px; }
/* isolation:isolate is load-bearing, not tidiness. Without it .main is
   position:relative with z-index:auto — no stacking context — so everything
   inside it competes against page-level chrome directly. That is fine while
   the contents are ours. It is not fine with Leaflet in there: its own
   stylesheet plants .leaflet-pane at 400, .leaflet-control at 800 and
   .leaflet-top/.leaflet-bottom at 1000, all of which beat the nav drawer's
   100. Result, reported by Patrick 2026-08-16: "I can't access the concierge
   desk menu because the map is covering it." Verified with the real
   stylesheets in a browser — elementFromPoint over both drawer links returned
   .leaflet-map-pane, so every tap went to the map.
   Isolating makes Leaflet's numbers local to .main and unable to reach page
   chrome at all. Chasing it the other way — raising the drawer above 1000 —
   only holds until the next vendor z-index. */
.main { position: relative; overflow: hidden; isolation: isolate; }

/* Member portal: mobile-first single-column stack. No full-screen map —
   the map only lives inside the active ride card. */
.member-stack {
  /* One source of truth for the column's side gutter: the padding below uses
     it, and .map-stage negates it to run a map edge-to-edge. Change it here
     and the full-bleed breakout follows. */
  /* Patrick, 2026-08-20: "scale the card sizes down so that they have more
     padding on all sides that keeps the sides off the edge." 14px -> 20px.
     One token does it: the padding below reads it and .map-stage negates it to
     run a map edge-to-edge, so the full-bleed breakout follows automatically.
     
     Costs the booking card 12px of width, which matters to exactly one thing:
     the greeting subline is sized in vw and its longest string was already
     inside the old column by 25px. Re-measured after the change rather than
     assumed -- see the note on .greet-sub's font-size. */
  --stack-gutter: 20px;
  height: 100%;
  overflow-y: auto;
  padding: 12px var(--stack-gutter) 24px;
  display: flex;
  flex-direction: column;
  gap: 12px;
  max-width: 520px;
  margin: 0 auto;
  width: 100%;
  -webkit-overflow-scrolling: touch;
}

/* Home feed — vertical snap feed for the passenger home screen.
   Matches Booked Out's Feed pattern: full-viewport cards with y-mandatory
   snap, gesture arbiter prevents momentum chaining, swipe-down hint when
   there's more below. */
.home-feed {
  height: 100%;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-y: contain;
}
.home-feed::-webkit-scrollbar { display: none; }
.home-feed-card {
  min-height: auto;
  display: flex;
  flex-direction: column;
}
.home-feed-hint {
  position: fixed;
  bottom: 20px;
  left: 50%;
  transform: translateX(-50%);
  animation: mc-hint-bounce 1.4s ease-in-out infinite;
  pointer-events: none;
  z-index: 10;
  opacity: 0;
  transition: opacity 0.3s;
}
.home-feed-hint.show { opacity: 0.6; }
@keyframes mc-hint-bounce {
  0%, 100% { transform: translateX(-50%) translateY(0); }
  50% { transform: translateX(-50%) translateY(6px); }
}

/* Time-of-day greeting above the booking card. The member-stack centers the
   greeting+card group in the viewport; this breathing room below the greeting
   lifts it up into the space between the top of the screen and the card, so it
   reads as its own upper element rather than hugging the card. */
.home-greeting {
  padding: 2px 4px 0;
  /* Kept tight (main's "tighten gap" fix) so the booking card + Send Caddie
     always land on one screen, complementing the compact estimate strip. */
  margin-bottom: clamp(16px, 4vh, 32px);
}
.home-greeting .greet-line {
  font-family: var(--font-display);
  font-size: 30px;
  font-weight: 600;
  letter-spacing: 0.01em;
  line-height: 1.08;
  color: #1a1410;
  margin: 0 0 5px;
}
/* The rotating line under the greeting — the club's own script, the hand the
   wordmark is cut from.
   
   This line has been four faces in a day and the trail is worth keeping,
   because the moves were not all the same kind of mistake:
   
     display serif    where it started
     BDScript 26px    Patrick: "match the font used in 'McDowell'" (PR #548)
     BD Sans 16px     Patrick: "BD sans should be used on the subheader of the
                      landing page below the salutation" (PR #549)
     BDScript 26px    Patrick: "It should be Beautifully Delicious Script"
   
   The middle two were me acting on the words rather than checking the
   intent -- both times the instruction was clear and both times it was not
   what he meant. Restored verbatim to the PR #548 rule; the only surviving
   change from the detour is that BD Sans stays wired but unused.
   
   26px and #4A3A28 are measured, not chosen. BDScript is a hairline with a
   very small x-height: at DPR 3 on the paper it is illegible at 15, poor at
   19, and only reads cleanly from about 24 up. The colour moves off --muted
   (5.4:1) to 9.0:1 for the same reason -- a monoline script at one stroke
   weight needs contrast a serif does not.
   
   Bigger than the 15px it replaced and still subordinate to the 30px greeting
   above it, because the script's x-height does the hierarchy work the point
   size normally would. */
.home-greeting .greet-sub {
  font-family: var(--font-script);
  font-style: normal;
  /* Patrick, 2026-08-20: "make the font of that subheader biggger so it's
     legible." 26px was my read off a screenshot; his was off a phone, and his
     wins. Two things the measurement changed:

       The BOLD cut is not a lever. Rendered side by side at 30 and 34, the
       700 face is barely heavier than the 400 -- this is a monoline script
       and its weights differ far less than a text face's do. Size is the only
       axis that moves legibility here.

       Wrapping was a non-issue, which is why this can go this far. Measured
       every one of the 24 rotating sublines at each size: the widest ("Your
       Caddie is standing by.") is 267px at 38px against 354px of column, so
       nothing wraps with 87px to spare. The face reads wide and sets narrow.

     Bumped a second time (Patrick: "Bigger still!"), and this time the wall
     was measured rather than approached. Widest subline against the column:

       320px screen   40px is the last that fits (3px spare); 42px wraps
       390px screen   50px is the last that fits (3px spare); 52px wraps
       430px screen   56px fits with 1px

     320 is the binding constraint, so the clamp is built around it.
     
     Re-fitted when --stack-gutter went 14px -> 20px: a wider gutter is 12px
     off the column this line is measured against, and at 12vw that left a 320
     screen 3px of margin -- fits today, wraps the first time a subline gains a
     character. 11.6vw restores it to 12px there and costs 390 about 1.6px,
     which is not a difference anyone can see. Now 37.1px at 320 with 12px
     spare, 45.2 at 390 with 25, and the 48px ceiling leaves a 430 with 45. */
  font-size: clamp(36px, 11.6vw, 48px);
  /* 700, the shipped Bold. Patrick: "Add some weight to the script font now."
     
     I had called this a dead lever earlier today, on a side-by-side at 30 and
     34px where the two faces looked nearly identical. That was true at those
     sizes and is not true here. Measured on identical element crops at 46.8px:
     
       400   5978 ink pixels, mean ink value 94
       700   7980 ink pixels, mean ink value 85
     
     +33% ink and denser strokes. A monoline script's weights separate as the
     stroke gets thick enough to see, so the size bump is what made the weight
     worth having. Both faces are shipped, so this is the real Bold and not a
     synthesised smear -- fontface.test.js enforces the pair. */
  font-weight: 700;
  /* Patrick, 2026-08-22: "heavier still" -- but 700 above is already the
     shipped Bold, the heaviest REAL cut BDScript has. Past this, font-weight
     has nothing left to give except a synthesised smear, which is exactly
     the artifact the "not a synthesised smear" note above was written to
     rule out, and which script-face-usage.test.js pins to {400, 700} for
     that reason. font-weight itself is not the lever left to pull.
     -webkit-text-stroke is: it thickens the actual painted strokes of the
     real Bold glyphs rather than asking the browser to fake a heavier one.
     0.4px at this element's 36-48px clamp reads as denser ink, not a halo --
     verified in Chromium: at 1px it started looking outlined rather than
     bold, at 0.4px it did not. script-face-usage.test.js constrains
     font-weight, letter-spacing, size and line-height on every script
     surface; it says nothing about this property, so it does not need
     touching for this to be a legitimate answer on this one surface. */
  -webkit-text-stroke: 0.4px currentColor;
  /* currentColor, not a hardcoded hex: this rule's own color line below can
     move (it already has once) and the stroke must track it, not fall back
     to whatever shade happened to be current the day this was written. */
  /* Stated, not inherited. The default is `none`, but this is the property
     that took the landing tagline apart, and an ancestor picking up an
     uppercase rule later must not be able to reach in here. */
  text-transform: none;
  letter-spacing: 0.01em;
  /* 1.3, and it is a CORRECTNESS floor rather than taste. BDScript's own
     metrics are 0.788em ascent + 0.412em descent = a 1.200em extent, so any
     line-height under 1.2 leaves the glyphs taller than their line box. They
     then overflow it by half the difference at each end -- and .home-greeting
     sets overflow:hidden for the collapse, which turns that overflow into a
     clip.
     
     I shipped exactly that in PR #551 by tightening this to 1.1 on the theory
     that the script "carries its own optical leading". Measured afterwards at
     all three widths: the descenders were losing 1.6-2.0px off their tails at
     320, 390 and 430 alike. It was small enough to miss in a screenshot and
     it was wrong at every size.
     
     1.3 leaves 0.1em of margin over the extent, so a taller glyph in some
     future string still has room. */
  line-height: 1.3;
  color: #4A3A28;
  margin: 0;
}



/* Subtle entrance: each top-level child of the home stack fades + lifts
   in on first paint, in stagger. Keeps the surface from feeling like a
   wall of cards landing all at once. Triggered on the .first-paint hook
   that app.js adds to the stack once the API call settles. */
@keyframes mc-rise {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: translateY(0); }
}
.member-stack.first-paint > * {
  animation: mc-rise 280ms ease-out both;
}
.member-stack.first-paint > *:nth-child(1) { animation-delay: 0ms; }
.member-stack.first-paint > *:nth-child(2) { animation-delay: 40ms; }
.member-stack.first-paint > *:nth-child(3) { animation-delay: 80ms; }
.member-stack.first-paint > *:nth-child(4) { animation-delay: 120ms; }
.member-stack.first-paint > *:nth-child(5) { animation-delay: 160ms; }
.member-stack.first-paint > *:nth-child(n+6) { animation-delay: 200ms; }
@media (prefers-reduced-motion: reduce) {
  .member-stack.first-paint > * { animation: none; }
}

/* ---- Identity strip (slim replacement for the old credit-card-style
   membership card) ---- */
.identity-strip {
  display: flex;
  align-items: baseline;
  gap: 10px;
  padding: 6px 4px 10px;
  /* Flat ribbon, not a card. Sits as quiet identity context under
     the topbar without competing with the booking hero for attention.
     Bottom rule keeps the copper signal without the heavy panel chrome. */
  border: 0;
  border-bottom: 1px solid rgba(184, 115, 51, 0.25);
  border-radius: 0;
  background: transparent;
  background-image: none;
  color: var(--ink);
  box-shadow: none;
  flex-wrap: wrap;
}
.identity-strip .id-mark {
  width: 28px;
  height: 28px;
  object-fit: contain;
  flex-shrink: 0;
}
.identity-strip .id-text {
  min-width: 0;
  flex: 1;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 10px;
  line-height: 1.2;
}
.identity-strip .id-name {
  font-family: var(--font-display);
  font-size: 14px;
  font-weight: 700;
  color: var(--ink);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  line-height: 1.1;
  overflow-wrap: anywhere;
}
.identity-strip .id-meta {
  font-size: 10px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--muted);
  margin-top: 0;
  display: inline-flex;
  flex-wrap: wrap;
  gap: 6px;
  align-items: baseline;
}
.identity-strip .id-tier { color: var(--copper-dark); font-weight: 700; }
.identity-strip .id-dot { opacity: 0.5; }

/* ---- Covers-delivered metric (Club House host + Concierge) ---- */
.covers-summary { display: flex; gap: 12px; margin: 4px 0 14px; }
.covers-kpi {
  flex: 1;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 12px 14px;
  text-align: center;
}
.covers-kpi .n {
  font-family: var(--font-ui);
  font-size: 26px; font-weight: 600; color: var(--cream);
}
.covers-kpi:last-child .n { color: var(--accent); }
.covers-kpi .l {
  font-size: 9px; letter-spacing: 0.2em; text-transform: uppercase;
  color: var(--muted); margin-top: 2px;
}
.covers-bars { display: flex; flex-direction: column; gap: 8px; }
.covers-row { display: flex; align-items: center; gap: 10px; font-size: 12px; }
.covers-row .day {
  width: 34px; flex-shrink: 0; color: var(--muted);
  text-transform: uppercase; letter-spacing: 0.1em; font-size: 10px;
}
.covers-row .track {
  flex: 1; height: 18px; background: rgba(255,255,255,0.03);
  border-radius: 6px; overflow: hidden; display: flex;
}
.covers-row .fill { height: 100%; background: rgba(184,115,51,0.45); }
.covers-row .fill.program { background: var(--accent); }
.covers-row .val { width: 28px; text-align: right; color: var(--cream); flex-shrink: 0; }
.covers-row.slow .day { color: var(--accent); font-weight: 700; }

/* ---- Demand-engine "comp outing this week" banner ---- */
.featured-banner {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 12px 16px;
  border-radius: 14px;
  text-decoration: none;
  background: linear-gradient(135deg, rgba(184,115,51,0.22), rgba(184,115,51,0.08));
  border: 1px solid var(--accent);
  box-shadow: var(--shadow-card);
}
.featured-banner .featured-tag {
  font-size: 9px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--accent);
  font-weight: 700;
  flex-shrink: 0;
}
.featured-banner .featured-text {
  flex: 1;
  min-width: 0;
  color: var(--cream);
  font-family: var(--font-ui);
  font-size: 16px;
  line-height: 1.15;
}
.featured-banner .featured-go {
  flex-shrink: 0;
  color: var(--accent);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.08em;
}

/* ---- Hero booking card ---- */
.hero-header-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 16px;
}
/* "View the menu ↗" — quiet copper link under the route card, shown only
   when the picked Club House has a menu link on file. */
.dest-menu-link {
  display: inline-block;
  margin: 2px 0 10px 34px;   /* aligns under the destination text, past the route icons */
  font-family: var(--font-ui);
  font-size: 12.5px;
  font-weight: 600;
  letter-spacing: 0.04em;
  color: var(--copper-deep, #8A551F);
  text-decoration: none;
  border-bottom: 1px solid rgba(184, 115, 51, 0.4);
  padding-bottom: 1px;
}
.dest-menu-link:hover { color: var(--copper-mid, #B87333); border-color: var(--copper-mid, #B87333); }

/* (The retired Locker button left no trace — the Locker is one swipe to the
   right of the booking card; the menu links remain the accessible path.) */
.hero-card {
  background-color: var(--panel);
  background-image: none;
  border: 1px solid rgba(184, 115, 51, 0.30);
  border-left: 4px solid var(--copper);
  border-radius: 14px;
  /* Promoted: more padding and a stronger shadow than the secondary
     sections so the booking action reads as the page's primary CTA. */
  padding: 26px 22px 24px;
  box-shadow: 0 18px 44px rgba(120, 85, 50, 0.18);
  color: var(--ink);
  position: relative;
  transition: box-shadow var(--duration) var(--ease), transform var(--duration) var(--ease);
}
.hero-card .hero-title {
  font-family: var(--font-display);
  font-size: 32px;
  font-weight: 600;
  letter-spacing: 0.005em;
  margin: 0 0 10px;
  color: var(--cream);
}

/* Direction control — "Going out" vs "Heading home". The old two-pill
   segmented control was never rendered (no #dir-out/#dir-home markup ever
   existed); this is the mount for the flip toggle that replaced it. */
.direction-row {
  display: flex;
  justify-content: flex-end;
  margin: 0 0 6px;
}
.direction-row:empty { display: none; }
/* From address — styled exactly like the destination select-lg.
   Tappable whole; opens the residence picker on tap. Sits above
   the destination dropdown so the two read as a matching pair:
   pick from, pick to. */
.from-select {
  display: flex;
  align-items: center;
  gap: 10px;
  height: 52px;
  padding: 0 16px;
  margin: 0 0 12px;
  background: var(--field-bg);
  border: 1px solid transparent;
  box-shadow: var(--field-inset);
  border-radius: 12px;
  cursor: pointer;
  transition: background var(--duration) var(--ease), box-shadow var(--duration) var(--ease);
  box-sizing: border-box;
}
.from-select:hover { background: var(--field-bg-hover); }
.from-select:focus-visible {
  outline: none;
  background: var(--field-bg-focus);
  box-shadow: var(--field-inset), 0 0 0 3px var(--field-ring);
}
.from-select .muted {
  text-transform: uppercase;
  letter-spacing: 0.16em;
  font-size: 10px;
  font-weight: 600;
  font-family: var(--font-ui);
  color: var(--muted);
  flex: 0 0 auto;
  line-height: 1;
  margin-top: 1px;
}
.from-select #from-label {
  flex: 1;
  font-size: 15px;
  font-weight: 500;
  color: var(--ink);
  font-family: var(--font-ui);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  margin-top: 1px;
}
.from-select .from-chevron {
  color: var(--muted);
  flex: 0 0 auto;
  display: block;
}
.hero-card .hero-step { margin-top: 0; }
.hero-card .select-lg {
  height: 52px;
  font-size: 15px;
  font-family: var(--font-ui);
  padding: 0 16px;
  border-radius: 12px;
}
.hero-card .btn-hero {
  width: 100%;
  height: 52px;
  font-size: 13px;
  letter-spacing: 0.25em;
  border-radius: 12px;
  margin-top: 14px;
  text-transform: uppercase;
}
.hero-card .btn-hero:disabled { opacity: 0.4; cursor: not-allowed; }
.hero-card #book-actions { display: block; margin-top: 0; }
/* Visually hidden, not removed: content stays in the accessibility tree
   (screen readers, the field's accessible name) with zero footprint on
   screen. The standard clip-based technique -- display:none takes it out of
   the tree entirely, which is the wrong tool for a label that still names a
   control. */
.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;
}

/* Riders — a flex ITEM inside .est-strip, on the same wrapped line as the
   tier chip, not a row of its own. Nesting it in #estimate-card (one report)
   still left every fact on its own line; folding it into the chip's line (a
   second report, explicit about not adding words to explain the first fix)
   removed a whole row instead. flex:0 0 auto so it neither grows to fill the
   line nor shrinks the chip beside it -- .est-strip's own
   justify-content:space-between then pushes the two to opposite ends of
   whatever line they land on. */
/* ...and a flex CONTAINER in its own right, because it holds two fields now.
   As a plain block the guests picker sat UNDER the rider count -- reported
   2026-08-29 with the two boxed and an arrow pointing sideways. The pair reads
   as one fact ("two people, no guests"), so they belong on one line; the gap
   is what keeps them legible as two controls rather than a single wide one. */
.est-riders {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 8px;
}
/* Both selectors, because shared/wheel-picker.js progressively enhances every
   <select class="select">: it hides the real <select> and inserts a sibling
   <button class="select wheel-field"> as the visible control. That button
   copies the select's className verbatim, so an element-type selector like
   ".est-riders select" would stop matching the moment the picker runs --
   this bit .rider-count-row before the row existed here, so both selectors
   are scoped from the start this time. */
.est-riders select,
.est-riders .wheel-field {
  width: auto;
  min-width: 64px;
  height: 38px;
  /* Override the base .select padding (12px 14px). With the fixed height, that
     24px of vertical padding leaves no room for the digit inside the 38px box,
     so iOS Safari clips the value out of view (the number "disappears" while the
     native chevron still shows). Tight vertical padding fits the line; the right
     padding keeps the number clear of the chevron. */
  padding: 4px 30px 4px 12px;
  font-size: 15px;
  font-weight: 600;
  line-height: 1.2;
  color: var(--ink);
  border-radius: 8px;
}

/* Estimate slides + fades into view when populated. Toggled via JS by
   removing the inline display:none; the slide animation runs on the
   transition from display:none -> display:block courtesy of the keyframe
   trigger applied when the element first becomes visible. */
@keyframes est-slide {
  from { opacity: 0; transform: translateY(-4px); max-height: 0; }
  to   { opacity: 1; transform: translateY(0);    max-height: 400px; }
}
.estimate-slide {
  margin-top: 12px;
  padding: 10px 14px;
  background: rgba(255,255,255,0.02);
  border: 1px solid var(--border);
  border-radius: 12px;
  animation: est-slide 200ms ease-out;
  overflow: hidden;
}
@media (prefers-reduced-motion: reduce) {
  .estimate-slide { animation: none; }
}
/* Compact one-row estimate: metrics left, fare right — collapses the old
   five stacked lines so the whole card + Send Caddie fit one screen. */
/* Row 1: distance·ETA on the left, the fare block on the right. Row 2 (only
   when the tier chip cannot fit beside them, which on a 390px phone is most
   of the time): the chip, at the strip's full width.
   The chip has to be able to leave the row. The fare block is fixed-width and
   must never shrink -- a truncated price is worse than any layout -- so on a
   nowrap row the chip is the only thing left to give, and it gave by running
   underneath the "To your account" caption. `wrap` gives it somewhere to go. */
.est-strip {
  display: flex; align-items: center; align-content: center;
  justify-content: space-between;
  flex-wrap: wrap; gap: 4px 12px;
}
.est-metrics {
  /* Distance·ETA, glued together (.est-de is nowrap). */
  display: flex; align-items: center; gap: 6px 8px; min-width: 0; flex: 0 1 auto;
  font-size: 13px; color: var(--muted); font-variant-numeric: tabular-nums;
}
.est-metrics .est-de { white-space: nowrap; }
.est-metrics .est-sep { opacity: 0.45; }
.est-ring {
  font-size: 9.5px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--copper-dark);
  background: rgba(184, 115, 51, 0.10);
  border: 1px solid rgba(184, 115, 51, 0.22);
  border-radius: 999px; padding: 3px 9px;
  /* Wrapping is what keeps this off the fare; shrinking is not, so the chip
     never shrinks. Ellipsis is the last-resort guard for a tenant tier name
     long enough to overrun a whole row on its own. */
  flex: 0 0 auto; min-width: 0; max-width: 100%;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.est-fare-block {
  display: flex; flex-direction: column; align-items: flex-end;
  /* Never shrink (the price must stay whole) and never grow into the metrics. */
  flex: 0 0 auto;
  /* The caption is the widest thing in the block and it is the piece the tier
     chip was colliding with; a hard floor keeps the two apart at 320px. */
  padding-left: 4px;
}
.est-fare-cap {
  font-size: 9px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.12em;
  color: var(--copper-dark);
}
.est-fare-amt {
  font-size: 19px; font-weight: 700; line-height: 1.1; color: var(--ink);
  font-variant-numeric: tabular-nums;
}
/* "Included" / "Spent" / "Complimentary" in place of a price. A word is 3-5x
   wider than "$15.00" at the same size, so it steps down to keep the fare
   block the same width the strip was laid out around. */
.est-fare-amt.is-word {
  font-size: 14px; letter-spacing: 0.01em; white-space: nowrap;
}
/* The routine allowance answer -- "5 left" or "5 of 8 left" -- as a caption
   under the fare it explains, not a sentence of its own below the strip.
   Muted like .est-fare-cap's register but not shouting like its uppercase
   treatment; this is a quiet number, not a section label. */
.est-allowance-mini {
  font-size: 11px; color: var(--muted); margin-top: 2px;
  font-variant-numeric: tabular-nums; white-space: nowrap;
}

/* Fit the ENTIRE booking card — greeting, route, riders, estimate, and the
   Send Caddie slide — on one screen, including iOS small-viewport (Safari
   toolbars visible) where the usable height drops ~130px and the slide button
   otherwise clips below the fold. Scoped to the home cell (#home-card-1) so the
   landing hero and pending-ride route keep their roomier spacing. */
/* The greeting + booking card sit near the TOP of the cell, not centred in it.
   Centring was written for the fully-populated card (route + riders + estimate
   + Send Caddie), where the group nearly fills the cell and the leftover
   margin splits evenly. On the landing state -- greeting, "Where to?", two
   route rows and nothing else -- the same rule pushed the whole group down
   past the fold's midpoint and left a screen-third of dead cream under the
   pager, which is the first thing a Member sees. Top-aligned with a small
   deliberate inset keeps the calm breathing room without the void, and the
   slack now falls BELOW the card, over the mountain ridge, where it reads as
   backdrop instead of emptiness. */
#home-card-1 {
  justify-content: flex-start;
  gap: 0;
  padding-top: clamp(6px, 1.6vh, 16px);
  padding-bottom: 8px;
}
#home-card-1 #home-greeting { margin-bottom: clamp(8px, 2vh, 18px); }
#home-card-1 .home-greeting .greet-line { line-height: 1.05; }
#home-card-1 .home-greeting .greet-sub { margin-top: 3px; }
/* Narrow-screen guard for the script subline.
   The bead + peek cost the greeting column 14px, and 320px screens are where
   that bites: at 11.6vw a 320 renders 37.1px, and the widest subline ("Your
   Caddie is standing by.") needs 260px against 258px of column — a two-pixel
   wrap. 390 and 430 keep the size Patrick asked for twice and still clear it
   by 12px and 31px, so this narrows ONLY the screens that need it rather than
   pulling the whole clamp down.
   Measured with the face actually loaded, which matters more than it sounds:
   an unloaded BDScript falls back to a serif that measures ~1.7x wider and
   reports every size as wrapping at every width. */
@media (max-width: 360px) {
  .home-greeting .greet-sub { font-size: 34px; }
}
#home-card-1 #book-panel.hero-card { padding: 18px 20px 16px; }
#home-card-1 #book-panel .hero-title { font-size: 26px; margin: 0 0 6px; }
#home-card-1 #book-panel .route-container { padding: 4px 0; }
#home-card-1 #book-panel .route-row { padding: 12px 0; }
#home-card-1 #book-panel .private-cart-opt { margin-top: 8px; }
#home-card-1 #book-panel .estimate-slide { margin-top: 8px; padding: 8px 14px; }
/* Tighten the metrics gap on the home cell. The chip's own row costs ~17px on
   a 390px phone, which used to be the reason it was forced inline (the Send
   Caddie slide clipped below the fold on short iOS viewports); the top-aligned
   card above reclaims several times that, so the row is affordable now. */
#home-card-1 #book-panel .est-metrics { gap: 2px 6px; }
#home-card-1 #book-panel .slide-action { margin-top: 6px; }

.member-stack .card { margin-bottom: 0; }

/* Hero card sets its own typography; only tighten the residence picker
   labels (which live inside the hero) to match the compact rhythm. */
.member-stack #book-panel .label { margin: 8px 0 4px; }
.member-stack #book-panel .step { margin-top: 0; }

/* Member-stack section headings: consistent small-caps copper labels.
   More vertical breathing room since the sections no longer wear card
   chrome — the heading itself has to do the visual separation. */
.member-stack .section { padding: 0; margin-top: 8px; }
.member-stack .section h3 {
  font-size: 10px;
  letter-spacing: 0.32em;
  text-transform: uppercase;
  color: var(--copper-dark);
  font-weight: 700;
  margin: 18px 0 8px;
  padding-left: 4px;
  text-shadow: none;
}
/* Compact panel surface for the Member portal's secondary cards
   (journeys, premium, sitter, inbound). Same rhythm as the hero
   card, lighter weight. */
/* Member portal secondary panels (Private Service, locker, sitter) —
   render as quiet horizontal data rows under their section heading,
   not as competing cards. Removes the panel chrome that made every
   section look identical to the booking hero. */
.member-stack .section .ch-confirm {
  padding: 0;
  border: 0;
  background: transparent;
  border-radius: 0;
}
.member-stack .section .ch-confirm-row {
  padding: 10px 4px;
  border-bottom: 1px solid rgba(184, 115, 51, 0.15);
  font-size: 13px;
}
.member-stack .section .ch-confirm-row:last-child { border-bottom: 0; }
.member-stack .section .ch-confirm-row.total {
  border-top: 0;
  margin-top: 0;
  padding-top: 10px;
}
.member-stack #history-panel .list .item { padding: 10px 12px; }
.member-stack #premium-panel .btn.full,
.member-stack #sitters-panel .btn.full { margin-top: 10px; height: 44px; }
.ride-map {
  width: 100%;
  height: 220px;
  border-radius: var(--radius-md);
  margin: 12px 0;
  background: var(--bg);
  border: 1px solid var(--border);
}
.ride-map.leaflet-container { background: var(--bg); }

/* ---- Full-screen maps (tiktokify) ----
   Anytime we show a map, the map owns the screen: it fills the panel
   viewport and the related status content floats OVER it in a frosted
   card instead of stacking below in dead space. */
/* The map owns the panel: edge-to-edge, no card chrome, filling whatever
   height is left under the deck header — the way a native map screen behaves.
   Height comes from the flex column below rather than a viewport calc, so it
   can't drift when the chrome above it changes. */
.ride-map.map-full {
  height: 100%;
  min-height: 0;
  margin: 0;
  border: 0;
  border-radius: 0;
}
/* A section holding a map becomes a flex column so .map-stage can take the
   remaining space. overflow:hidden because the map is the scroller now — the
   panel must not also scroll behind it. */
.section:has(> .map-stage) {
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.map-stage {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
  /* Break out of the stack's side gutter so the map reaches both edges. */
  margin-inline: calc(-1 * var(--stack-gutter, 0px));
  width: calc(100% + 2 * var(--stack-gutter, 0px));
}
.map-stage .map-float {
  position: absolute;
  left: 10px; right: 10px; bottom: 10px;
  z-index: 500;                      /* above the Leaflet panes, below its popups */
  background: color-mix(in srgb, var(--paper, #FBF4E6) 90%, transparent);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  border: 1px solid var(--border, rgba(184, 115, 51, 0.3));
  border-radius: 12px;
  padding: 10px 14px;
  box-shadow: 0 8px 24px rgba(40, 24, 8, 0.18);
  max-height: 40%;
  overflow-y: auto;
  overscroll-behavior: contain;
}
/* Title row inside the floating card. The panel has no heading of its own —
   the deck header names it — so this labels the list it sits with. */
.map-stage .map-float-head {
  font-size: 11px;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.22em;
  color: #14110a;
  margin: 0 0 8px;
}
/* A name pinned onto the map (the Parent portal's kid). Sits top-left, clear
   of Leaflet's zoom control on the opposite side. */
.map-stage .map-chip {
  position: absolute;
  top: 10px; left: 10px;
  z-index: 500;
  padding: 6px 13px;
  border-radius: 999px;
  background: color-mix(in srgb, var(--paper, #FBF4E6) 92%, transparent);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  border: 1px solid var(--border, rgba(184, 115, 51, 0.3));
  box-shadow: 0 4px 14px rgba(40, 24, 8, 0.16);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.06em;
  color: #14110a;
  pointer-events: none;
}

/* Hosted content sheds its own card chrome inside the float. */
.map-stage .map-float .empty-state { border: 0; background: transparent; padding: 4px 0; margin: 0; }
.map-stage .map-float .list { margin: 0; }
.map-stage .map-float .ch-confirm { border: 0; background: transparent; box-shadow: none; padding: 0; margin: 0; }

/* ===========================================================
   Host "Caddies inbound" — a list, not a map.
   Patrick, 2026-08-12: a Host at 7pm is deciding whether to hold a table,
   and a moving dot on a street they already know does not answer that.
   Four fields do: last name, party size, ETA, cart. This panel used to be
   a full-bleed Leaflet map with the list floating over it in a frosted
   card; the card's type scale is kept so the panel still reads as the
   same thing, minus the map underneath it.
   =========================================================== */
.ch-at-door {
  margin: 0 0 14px;
  padding: 10px 12px 8px;
  border: 1px solid var(--line, rgba(0,0,0,0.08));
  border-radius: 12px;
  background: var(--surface, #fff);
}
.ch-at-door[hidden] { display: none; }
.ch-at-door-label {
  font-family: var(--font-ui);
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
  margin: 0 0 8px;
}
.ch-at-door-row .btn { flex: 0 0 auto; }
.ch-blotter {
  padding: 2px 2px 6px;
}
.ch-blotter .list { margin: 0; }
.ch-blotter .empty-state { border: 0; background: transparent; padding: 4px 0; margin: 0; }
/* The timestamp column. Tabular so a vertical scan of times lines up — the
   Host reads this against the clock on the wall, and proportional digits put
   7:05 and 7:11 at different widths. Muted because the time is the index, not
   the news; the line itself is what they are reading for. */
.blotter-at {
  font-family: var(--font-ui);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  color: var(--muted);
  flex: 0 0 auto;
  padding-left: 10px;
}
/* A cancellation is the one line worth finding at a glance on a busy log. */
.blotter-row[data-kind="cancelled"] strong { color: var(--danger); }

/* Caddie/Concierge still use the sidebar+map layout; #map fills the .main
   region when that layout is active. Inside .ride-map the map sizes itself. */
.main #map { position: absolute; inset: 0; }

.card {
  background-color: var(--panel);
  background-image: none;
  border: 1px solid rgba(184, 115, 51, 0.25);
  border-left: 3px solid var(--copper);
  border-radius: 10px;
  padding: 22px;
  margin-bottom: 14px;
  color: var(--ink);
  box-shadow: 0 14px 34px rgba(120, 85, 50, 0.12);
  transition: box-shadow var(--duration) var(--ease), transform var(--duration) var(--ease);
}
.card h3 {
  margin: 0 0 14px;
  font-family: var(--font-display);
  font-size: 11px;
  font-weight: 700;
  color: var(--copper-dark);
  text-transform: uppercase;
  letter-spacing: 0.3em;
  text-shadow: none;
}
/* Section h3 sits on bare sandstone — deep near-black warm tone for
   clear contrast against the copper-tan stone at small caps. */
.section h3 {
  margin: 0 0 14px;
  font-family: var(--font-display);
  font-size: 11px;
  font-weight: 800;
  color: #14110a;
  text-transform: uppercase;
  letter-spacing: 0.3em;
  text-shadow: 0 1px 0 rgba(255, 235, 200, 0.35);
}

/* Text inside cream-card surfaces — dark coffee for body + headlines,
   copper for the small-caps eyebrows and links, muted bronze-taupe for
   secondary copy. */
.hero-card .hero-title,
.hero-card .pickup-line strong,
.identity-strip .id-name,
.ride-sheet .sheet-title h3 {
  color: var(--ink);
  text-shadow: none;
}
.hero-card,
.card,
.identity-strip,
.ride-sheet { color: var(--ink); }
.hero-card .muted,
.card .muted,
.identity-strip .muted,
.identity-strip .id-meta,
.ride-sheet .muted { color: var(--muted); }
.identity-strip .id-tier { color: var(--copper-dark); font-weight: 700; }
.hero-card .link,
.card .link,
.identity-strip .link,
.ride-sheet .link {
  color: var(--copper-dark);
  border-bottom: 1px solid rgba(138, 85, 31, 0.4);
}
.hero-card .link:hover,
.card .link:hover,
.identity-strip .link:hover,
.ride-sheet .link:hover { color: var(--copper); border-bottom-color: var(--copper); }

/* Flat content section — no card chrome. Use this when the contained
   inputs / list items already carry their own visual weight and don't
   need another panel wrapping them. Cards are reserved for things that
   genuinely ARE a card (membership card, en-route sheet). */
.section {
  padding: 0;
  margin: 0;
}
.section h3 {
  margin: 0 0 12px;
}

.label {
  display: block;
  font-size: 10px;
  color: var(--muted);
  margin: 10px 0 5px;
  text-transform: uppercase;
  letter-spacing: 0.2em;
  font-weight: 600;
}
.input, .select, .textarea {
  width: 100%;
  background: var(--field-bg);
  border: 1px solid transparent;
  box-shadow: var(--field-inset);
  color: var(--ink);
  padding: 13px 14px;
  border-radius: var(--field-radius);
  font-size: 14px;
  font-family: var(--font-ui);
  transition: background var(--duration) var(--ease), box-shadow var(--duration) var(--ease);
}
.input:hover, .select:hover, .textarea:hover { background: var(--field-bg-hover); }
.input:focus, .select:focus, .textarea:focus {
  outline: none;
  background: var(--field-bg-focus);
  box-shadow: var(--field-inset), 0 0 0 3px var(--field-ring);
}
.input::placeholder, .textarea::placeholder { color: rgba(58, 38, 20, 0.42); }

/* ============================================================
   Wheel picker — iOS-style spinning selector (shared/wheel-picker.js).
   Progressively enhances every <select class="select">: the native select is
   hidden and this trigger field opens a branded wheel sheet on tap.
   ============================================================ */
/* The trigger inherits .select sizing; these override the button chrome and
   add the value + chevron. */
button.wheel-field {
  -webkit-appearance: none;
  appearance: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  text-align: left;
  cursor: pointer;
  position: relative;
  font-weight: 500;
}
button.wheel-field .wp-value {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  flex: 1 1 auto;
}
button.wheel-field .wp-value.wp-placeholder { color: rgba(58, 38, 20, 0.42); }
button.wheel-field::after {
  content: '';
  flex: 0 0 auto;
  width: 8px; height: 8px;
  margin-left: 6px;
  border-right: 2px solid var(--copper-mid, #B87333);
  border-bottom: 2px solid var(--copper-mid, #B87333);
  transform: translateY(-2px) rotate(45deg);
}
button.wheel-field:active { border-color: var(--copper-dark); }

/* Bottom-sheet overlay */
.wp-overlay {
  position: fixed;
  inset: 0;
  z-index: 3000;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  background: rgba(30, 18, 6, 0);
  transition: background 0.2s ease;
}
.wp-overlay.open { background: rgba(30, 18, 6, 0.42); }
.wp-sheet {
  width: 100%;
  max-width: 460px;
  background: #F7EFDF;
  border-radius: 18px 18px 0 0;
  box-shadow: 0 -10px 40px rgba(40, 22, 6, 0.3);
  padding: 8px 14px calc(14px + env(safe-area-inset-bottom, 0px));
  transform: translateY(100%);
  transition: transform 0.26s cubic-bezier(0.22, 0.61, 0.36, 1);
}
.wp-overlay.open .wp-sheet { transform: translateY(0); }
.wp-sheet-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 6px 2px 10px;
}
.wp-title {
  font-family: var(--font-display);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted, #6E5A40);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.wp-cancel {
  background: none;
  border: none;
  color: var(--copper-dark, #8A551F);
  font: 600 14px var(--font-ui);
  cursor: pointer;
  padding: 6px 4px;
}
.wp-done { padding: 8px 18px; font-size: 13px; }

/* The wheel: a scroll-snap column with a 3D curve painted by JS. */
.wp-wheel-wrap {
  position: relative;
  height: 200px;                 /* ITEM_H(40) * VISIBLE(5) */
  overflow: hidden;
  perspective: 800px;
}
.wp-band {
  position: absolute;
  left: 0; right: 0;
  top: 80px; height: 40px;       /* center row */
  border-top: 1px solid rgba(184, 115, 51, 0.5);
  border-bottom: 1px solid rgba(184, 115, 51, 0.5);
  background: rgba(184, 115, 51, 0.06);
  pointer-events: none;
  z-index: 2;
}
.wp-wheel {
  height: 100%;
  overflow-y: auto;
  scroll-snap-type: y mandatory;
  padding: 80px 0;               /* PAD(2) * ITEM_H(40) so ends can center */
  transform-style: preserve-3d;
  scrollbar-width: none;
  -ms-overflow-style: none;
  -webkit-overflow-scrolling: touch;
  outline: none;
}
.wp-wheel::-webkit-scrollbar { display: none; }
.wp-item {
  height: 40px;
  /* One line ALWAYS — a wrapped label makes the row taller than 40px and
     scroll-snap can no longer center it. Smaller font + nowrap keeps long cart
     names on a single line; line-height centers them vertically. */
  display: block;
  line-height: 40px;
  scroll-snap-align: center;
  font-family: var(--font-ui);
  font-size: 16px;
  color: var(--ink, #2E2114);
  backface-visibility: hidden;
  will-change: transform, opacity;
  padding: 0 14px;
  text-align: center;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.wp-item.disabled { color: rgba(58, 38, 20, 0.35); }
@media (prefers-reduced-motion: reduce) {
  .wp-sheet, .wp-overlay { transition: none; }
}

/* ============================================================
   Sitter Desk — the babysitter's own dashboard card. A solid cream
   card (so it reads cleanly over the fixed ridge), a header, an
   availability pill, three stat tiles, and the sits list.
   ============================================================ */
.sitter-desk { position: relative; z-index: 1; }
.sitter-card {
  background: linear-gradient(160deg, #FBF4E6 0%, #F3E7D0 100%);
  border: 1px solid rgba(184, 115, 51, 0.28);
  border-radius: 16px;
  padding: 16px 15px 18px;
  box-shadow: 0 12px 34px rgba(120, 85, 50, 0.16);
  margin-bottom: 104px;                    /* clear the ridge + switcher FAB */
}
.sitter-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.sitter-head-title { display: flex; align-items: center; gap: 11px; }
.sitter-glyph {
  font-size: 22px;
  width: 42px; height: 42px;
  display: flex; align-items: center; justify-content: center;
  background: rgba(184, 115, 51, 0.12);
  border-radius: 12px;
  flex-shrink: 0;
}
.sitter-head-title h3 { margin: 0; font-size: 19px; line-height: 1.1; }
.sitter-sub { font-size: 12px; color: var(--muted, #6E5A40); }

.sitter-avail {
  display: flex; align-items: center; justify-content: space-between;
  gap: 10px;
  margin-top: 14px;
  padding-top: 13px;
  border-top: 1px solid rgba(184, 115, 51, 0.18);
}
.sitter-avail-label { font-size: 13px; color: var(--ink, #2E2114); font-weight: 600; }
.sitter-toggle {
  border-radius: 999px;
  padding: 8px 16px;
  font: 700 12px var(--font-ui);
  letter-spacing: 0.02em;
  cursor: pointer;
  border: 1px solid rgba(184, 115, 51, 0.45);
  background: #FFFAF0;
  color: var(--copper-dark, #8A551F);
  white-space: nowrap;
}
.sitter-toggle.on {
  background-image: var(--copper);
  background-size: var(--copper-size);
  background-repeat: no-repeat;
  color: #FFFFFF;
  text-shadow: 0 1px 2px rgba(40, 22, 6, 0.5);
  border-color: rgba(40, 22, 6, 0.45);
}
.sitter-toggle:active { transform: scale(0.97); }

.sitter-stats {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px;
  margin-top: 14px;
}
.sitter-stat {
  background: #FFFDF8;
  border: 1px solid rgba(184, 115, 51, 0.2);
  border-radius: 12px;
  padding: 11px 6px;
  display: flex; flex-direction: column; align-items: center; gap: 4px;
  text-align: center;
}
.ss-label {
  font: 700 9px var(--font-ui);
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--muted, #6E5A40);
}
.ss-val {
  font-family: var(--font-ui);
  font-size: 17px;
  line-height: 1.05;
  color: var(--ink, #2E2114);
}
.sitter-sits { margin-top: 18px; }
.sitter-empty {
  display: flex; align-items: center; gap: 9px;
  padding: 14px;
  background: #FFFDF8;
  border: 1px dashed rgba(184, 115, 51, 0.32);
  border-radius: 12px;
  color: var(--muted, #6E5A40);
  font-size: 13px;
  margin-top: 4px;
}
.sitter-empty-glyph { font-size: 20px; flex-shrink: 0; }

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  background-image: var(--copper);
  background-size: var(--copper-size);
  background-repeat: no-repeat;
  /* White label on the metallic copper CTA — a dark shadow keeps it legible
     against the light coppery highlights. */
  color: #FFFFFF;
  text-shadow: 0 1px 2px rgba(40, 22, 6, 0.55);
  border: 1px solid rgba(40, 22, 6, 0.55);
  padding: 10px 16px;
  border-radius: 6px;
  font-weight: 700;
  cursor: pointer;
  font-size: 13px;
  letter-spacing: 0.04em;
  line-height: 1.15;
  white-space: nowrap;         /* keep a label on one line — no "Caddie to my / home" */
  font-family: var(--font-ui);
  transition: filter var(--duration) var(--ease), transform var(--dur-1) var(--ease-spring);
  box-shadow:
    inset 0 1px 0 rgba(255, 230, 175, 0.42),
    inset 0 -1px 0 rgba(0, 0, 0, 0.28),
    0 5px 12px rgba(40, 22, 6, 0.22);
}
.btn:hover { filter: brightness(1.08); }
/* Tactile press — a small spring scale confirms the tap without a wait. */
.btn:active { transform: scale(0.965); }
@media (prefers-reduced-motion: reduce) { .btn:active { transform: none; } }
.btn:disabled { opacity: 0.45; cursor: not-allowed; filter: saturate(0.55); }
.btn.secondary {
  background: transparent;
  background-image: none;
  /* Flat copper outline — a deliberate secondary, distinct from the metallic
     primary CTAs. */
  color: #8A551F;
  text-shadow: none;
  border: 1px solid rgba(184, 115, 51, 0.5);
  box-shadow: none;
  font-weight: 600;
}
.btn.secondary:hover {
  border-color: var(--accent);
  color: var(--accent);
  background: rgba(184, 115, 51, 0.08);
  filter: none;
}
.btn.danger { background: transparent; background-image: none; border: 1px solid var(--danger); color: var(--danger); text-shadow: none; box-shadow: none; }
.btn.danger:hover { background: rgba(194,92,92,0.08); filter: none; }
.btn.full { width: 100%; }

/* Locker secondary/danger buttons use the flat outline (base .btn.secondary /
   .btn.danger) so utility actions — Back, Cancel, Invite/Revoke, Send test
   text — read as secondary instead of competing with the copper primary CTAs
   (Save gate code, Activate, Save card). */

.ride-sheet .sheet-summary-pills {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
}
.ride-sheet .sheet-pill {
  display: inline-block;
  padding: 3px 8px;
  border-radius: 12px;
  font-size: 11px;
  background: var(--bg-2, #f0e6d8);
  color: var(--ink, #2E2114);
  border: 1px solid var(--border, #d8c8b4);
}
.btn.mini { padding: 6px 12px; font-size: 11px; border-radius: 4px; }
.label.small { font-size: 9px; margin: 6px 0 3px; }

.row { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
/* Items grow to share the row, but when two nowrap buttons can't both fit they
   wrap to their own line instead of overflowing/clipping the panel. */
.row > * { flex: 1 1 auto; min-width: 0; }
.between { display: flex; justify-content: space-between; align-items: center; gap: 10px; }
/* Long text inside a flex .between row must be allowed to shrink — without
   min-width: 0 the flex item refuses to go below its intrinsic content width,
   which on a phone-sized viewport pushes the parent past 100% and forces a
   horizontal scrollbar. The page then auto-centers the wide content and clips
   both edges. min-width: 0 + overflow-wrap lets the text wrap instead. */
.between > * { min-width: 0; }
.between > strong, .between > span { overflow-wrap: anywhere; }

/* The HTML [hidden] attribute should always win, even against display:flex */
[hidden] { display: none !important; }

/* ---------- Mobile-first overrides ----------
   Phone-sized viewports: bigger tap targets (44px+), bigger fonts, simpler
   topbar. The Member portal lives here almost exclusively. */
@media (max-width: 720px) {
  .topbar {
    padding: calc(12px + var(--safe-top)) calc(14px + env(safe-area-inset-right)) 12px calc(14px + env(safe-area-inset-left));
    /* The wordmark art is free to grow into whatever the bar has left, so it
       needs a hard floor of space before the hamburger — without this it grows
       until the two touch. The art shrinks to respect this, not the reverse. */
    gap: 14px;
  }
  /* Show the wordmark next to the shield on mobile — it's the brand
     signal; previously hidden to save space but the gap between the
     shield and the hamburger is plenty wide. The staff portals' sub-label
     ("Concierge Desk", "Caddie Portal", …) still drops here to keep the
     lockup a single short row. */
  .topbar .brand { gap: 10px; }
  .topbar .brand .wordmark { display: flex; }
  .topbar .brand .wordmark .name {
    /* Was a flat 14px, which left the name small on every phone in order to
       survive a 320px one. Scaling with the viewport instead holds that
       floor on the narrowest handsets and gives a modern phone the ~17px
       the space actually allows. */
    font-size: clamp(14px, 4.1vw, 22px);
    letter-spacing: 0.10em;
  }
  .topbar .brand .wordmark .name-img {
    /* Same scaling story as .name: floored for a 320px handset, doubled from
       the old 22-30px so the brand actually reads at arm's length. The art is
       5.11:1, so height buys width fast — 14vw is the most a phone can give
       the wordmark before the crest and the hamburger run out of bar. */
    height: clamp(38px, 14vw, 60px);
  }
  .topbar .brand .wordmark .sub { display: none; }
  /* Height-driven like the desktop rule, and sized above the wordmark art
     (38-60px here) so the crest leads the lockup on a phone, where it is the
     only part of the brand read at a glance. The bar takes its height from
     this, so the band grows by the difference and nothing crops. */
  .topbar .brand .mark { height: 78px; width: auto; }

  /* ...but not in the cart. The 78px crest above is a deliberate brand moment
     on the MEMBER portal, where the header is the first thing a Member sees
     and the page below it is a booking form they scroll anyway.

     The Caddie portal is a work tool, held one-handed, and its screen is a
     fixed-height dashboard that does not scroll — so every pixel the header
     takes is a pixel off the duty toggle, the cart picker and the incoming
     request. At 78px the lockup was 102px of an 852px phone: 19% of the
     working area spent telling a Caddie on shift which club they work for.
     Reported 2026-08-21 as "a phat header at the top".

     34px still reads as the crest at arm's length, and brings the bar to the
     ~64px the layout was originally drawn around. */
  .caddie-body .topbar .brand .mark { height: 34px; }
  .caddie-body .topbar .brand .wordmark .name-img { height: clamp(22px, 7vw, 30px); }
  .caddie-body .topbar .brand .wordmark .name { font-size: clamp(13px, 3.4vw, 16px); }
  .caddie-body .topbar {
    padding-top: calc(9px + var(--safe-top));
    padding-bottom: 9px;
  }
  /* On mobile we hide the inline links entirely and surface a single
     hamburger toggle that opens the drawer. Desktop keeps the inline
     row + Sign Out chip. */
  .topbar .links { display: none; }

  /* Phones get the same 20px as everywhere else. This override is where the
     gutter actually lands for the people using the app -- it sits inside
     max-width:720px, so the token above only ever governed tablets and up,
     and raising that alone changed nothing on a phone. */
  .member-stack { --stack-gutter: 20px; padding: 12px var(--stack-gutter) 32px; gap: 12px; }

  .card { padding: 16px; }
  .card h3 { font-size: 12px; letter-spacing: 0.22em; }

  .input, .select, .textarea {
    padding: 14px 14px;
    font-size: 16px; /* prevents iOS Safari zoom on focus */
    min-height: 48px;
  }
  .btn {
    padding: 14px 18px;
    font-size: 13px;
    min-height: 48px;
    letter-spacing: 0.18em;
  }
  .label { font-size: 11px; }
  .ride-map { height: 200px; }

  /* Drop the strict credit-card aspect on phones so member name +
     number + since never clip. Card still keeps the ID-card look. */
  .member-card { aspect-ratio: auto; min-height: 200px; padding: 18px 20px; }
  .member-card .mc-mark { width: 44px; height: 44px; }
  .member-card .mc-name { font-size: 22px; }
  .member-card .mc-tier { top: 12px; }
}

/* ---------- Tablet band ----------
   The inline link row needs roughly 1000px before it stops crowding the
   wordmark. Below that the Member portal's four uppercase, wide-tracked
   links spilled onto two and three rows and squeezed the club name into a
   second line — already true at the old fixed 20px, and no font size fixes
   it, because the links alone overflow the space. So these widths get the
   same hamburger the phone gets. Only the nav swaps; the rest of the
   tablet layout keeps its desktop treatment. Every portal's drawer carries
   the full inline link set, so nothing is lost by hiding the row. */
@media (min-width: 721px) and (max-width: 1000px) {
  .topbar .links { display: none; }
  /* With the row gone the bar is just crest + wordmark + hamburger, so the
     name can take the space the links were failing to use. */
  .topbar .brand .wordmark .name { font-size: clamp(22px, 3.2vw, 26px); }
  .topbar .brand .wordmark .name-img { height: clamp(60px, 8vw, 72px); }
}

.badge {
  display: inline-flex;
  align-items: center;
  padding: 4px 12px;
  border-radius: 999px;
  font-size: 10px;
  font-weight: 600;
  background: var(--accent-soft);
  border: 1px solid var(--border-2);
  color: var(--accent);
  text-transform: uppercase;
  letter-spacing: 0.12em;
  /* Pills are single-token status chips — never let "ACTIVE" break to "ACTIV E"
     when a flex row squeezes them. */
  white-space: nowrap;
  flex: 0 0 auto;
}
/* Escape hatch for the rare badge carrying UNBOUNDED text rather than a status
   token — currently just the Clubhouse pulse history's "Claimed by <member>".
   The base rule above is right for fixed labels, but combined with
   `.between > * { min-width: 0 }` it means a long member name can neither wrap
   nor shrink, so the row grows past the viewport and gets clipped by the
   page-level horizontal-overflow rules. Here the pill may shrink and ellipsize
   instead; `display: inline-block` is required because text-overflow has no
   effect on the text of an inline-flex container. Pair with a title attribute
   so the untruncated value is still reachable. */
.badge.badge-dynamic {
  display: inline-block;
  min-width: 0;
  flex-shrink: 1;
  overflow: hidden;
  text-overflow: ellipsis;
}
.badge.ok { background: var(--accent-soft); color: var(--accent); border-color: var(--border-2); }
.badge.warn { background: rgba(216,179,106,0.12); color: var(--warn); border-color: rgba(216,179,106,0.45); }
.badge.bad { background: rgba(194,92,92,0.12); color: var(--danger); border-color: rgba(194,92,92,0.45); }
.badge.info { background: rgba(184,115,51,0.08); color: var(--cream); border-color: var(--border); }

.list { display: flex; flex-direction: column; gap: 0; }
.list-item {
  padding: 12px 4px;
  border: 0;
  border-bottom: 1px solid rgba(184, 115, 51, 0.18);
  border-radius: 0;
  background: transparent;
  cursor: pointer;
  transition: background 0.15s ease;
}
.list-item:last-child { border-bottom: 0; }
.list-item:hover { background: rgba(184, 115, 51, 0.05); }
.list-item .between strong {
  font-family: var(--font-ui);
  font-size: 16px;
  font-weight: 600;
  letter-spacing: 0.01em;
}
.list-item .meta { color: var(--muted); font-size: 11px; margin-top: 2px; letter-spacing: 0.04em; }

.muted { color: var(--muted); font-size: 12px; letter-spacing: 0.02em; }
.h-divider { height: 1px; background: var(--border); margin: 14px 0; }

/* Concierge desk roster rows (partner contacts, signed agreements). */
.list-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  background: rgba(184, 115, 51, 0.06);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  margin-bottom: 6px;
}
.list-row-main { flex: 1; min-width: 0; }
.btn-link {
  background: transparent;
  border: none;
  color: var(--accent);
  cursor: pointer;
  font-size: 13px;
  padding: 4px 6px;
}
.btn-link:hover { text-decoration: underline; }
.btn-link.danger { color: var(--danger); }
.history-more {
  display: block;
  width: 100%;
  text-align: center;
  margin-top: 4px;
  font-weight: 600;
  letter-spacing: 0.03em;
}

.fare-line { display: flex; justify-content: space-between; padding: 5px 0; font-size: 13px; }
.fare-line.total { font-weight: 700; border-top: 1px solid var(--border); padding-top: 10px; margin-top: 10px; font-size: 14px; }

.tip-row {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 8px;
  margin-top: 8px;
}
.tip-btn {
  font-family: var(--font-ui);
  font-size: 16px;
  font-weight: 600;
  letter-spacing: 0.02em;
  background: var(--panel-2);
  color: var(--cream);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: 14px 0;
  min-height: 52px;
  cursor: pointer;
  transition: all var(--duration) var(--ease);
}
.tip-btn:hover { border-color: var(--accent); color: var(--accent); }
.tip-btn.selected {
  background: var(--accent);
  color: var(--cream);
  border-color: var(--accent);
}
.tip-note {
  margin-top: 10px;
  font-family: var(--font-ui);
  font-style: italic;
  font-size: 14px;
  /* --sand (#E8DDC8) is a SURFACE token, and this note sits on --paper
     (#FBF4E6): 1.23:1, which is not dim, it is invisible. It read fine in the
     navy kit, where --sand was a light value on a dark card; the sandstone
     re-theme kept every variable NAME and moved the values, so a light-on-dark
     text colour silently became light-on-light. --muted is the documented
     "readable on cream" value and lands at 6.0:1 here. */
  color: var(--muted);
  text-align: center;
}
/* "100%" is the load-bearing word in the gratuity note, and it is a factual
   claim the payout pipeline has to keep true (packages/core/payouts.js takes
   no platform cut on tips). Upright and copper so it reads as a fact rather
   than part of the italic aside around it. */
.tip-note strong {
  font-style: normal;
  font-weight: 600;
  color: var(--accent);
}

.banner {
  padding: 14px 16px;
  border-radius: var(--radius-md);
  margin-bottom: 14px;
  font-size: 13px;
  line-height: 1.5;
}
.banner.info { background: var(--accent-soft); color: var(--cream); border: 1px solid var(--border); border-left: 3px solid var(--accent); }
.banner.warn {
  background: rgba(216, 179, 106, 0.14);
  color: #6B4317;
  border: 1px solid rgba(216, 179, 106, 0.55);
  border-left: 3px solid var(--copper);
  border-radius: 6px;
}
.banner.error { background: rgba(194,92,92,0.10); color: var(--danger); border: 1px solid rgba(194,92,92,0.35); }
.banner.success { background: var(--accent-soft); color: var(--accent); border: 1px solid var(--border-2); }

.link {
  color: var(--accent);
  text-decoration: none;
  font-size: 12px;
  letter-spacing: 0.05em;
  border-bottom: 1px solid transparent;
}
.link:hover { border-bottom-color: var(--accent); }

.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
.grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; }
.grid-4 { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; }

.stat .num { font-family: var(--font-ui); font-size: 28px; font-weight: 600; color: var(--cream); }
.stat .lbl { font-size: 10px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.25em; margin-top: 2px; }

table.data { width: 100%; border-collapse: collapse; }
/* Wide concierge tables (Access requests' 6 columns etc.) can't fit a phone;
   let them scroll inside their card instead of clipping both edges. */
@media (max-width: 720px) {
  table.data { display: block; overflow-x: auto; -webkit-overflow-scrolling: touch; }
}
table.data th, table.data td { text-align: left; padding: 10px 8px; border-bottom: 1px solid var(--border); font-size: 13px; }
table.data th { color: var(--accent); font-weight: 600; text-transform: uppercase; font-size: 10px; letter-spacing: 0.3em; }

/* ---- Members table: per-member detail drawer (Concierge desk) ----
   A full-width row that opens directly under its member, so editing details
   and managing the household happen in place instead of in a modal that hides
   the roster you're working against. */
tr.member-detail > td { background: var(--accent-soft, rgba(184,115,51,0.07)); padding: 0 8px 14px; }
.md-wrap {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  gap: 18px;
  padding: 14px 2px 4px;
}
.md-block { min-width: 0; }
.md-h {
  margin: 0 0 10px;
  font-size: 10px; letter-spacing: 0.28em; text-transform: uppercase;
  color: var(--accent); font-weight: 600;
}
.md-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; }
.md-f { display: block; min-width: 0; }
.md-f .input, .md-f .select { width: 100%; }
.md-actions { display: flex; align-items: center; gap: 10px; margin-top: 12px; flex-wrap: wrap; }
.md-msg { font-size: 12px; color: var(--muted); }
.md-hh-list { display: flex; flex-direction: column; gap: 8px; }
.md-hh-row {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 8px 10px; border: 1px solid var(--border); border-radius: 8px;
}
.md-hh-row > div { min-width: 0; }
.md-res-results { display: flex; flex-direction: column; align-items: flex-start; gap: 2px; margin-top: 6px; }
.md-res-hit { text-align: left; font-size: 12px; }

.tab-bar {
  display: flex;
  border-bottom: 1px solid var(--border);
  margin-bottom: 14px;
  /* 11 tabs don't fit a phone; scroll horizontally instead of clipping the
     tail (Verifications etc. were unreachable on mobile). */
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}
.tab-bar::-webkit-scrollbar { display: none; }
.tab-bar .tab {
  padding: 10px 16px;
  cursor: pointer;
  color: var(--muted);
  border-bottom: 1px solid transparent;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.25em;
  font-weight: 600;
  white-space: nowrap;
  flex-shrink: 0;
}
.tab-bar .tab.active { color: var(--accent); border-bottom-color: var(--accent); }

/* Concierge tab strip — the old inline #161e19 navy-green remnant rendered
   tabs dark-on-dark (var(--muted) is a light-surface taupe). Seat the strip
   on the topbar's espresso material with warm-ivory tabs + copper active. */
.adm-tabstrip {
  background: linear-gradient(180deg, rgba(36, 24, 17, 0.94), rgba(26, 16, 9, 0.96));
  border: 1px solid rgba(196, 132, 74, 0.45);
  border-bottom: 1px solid rgba(196, 132, 74, 0.45);
  border-radius: 10px;
  padding: 4px 8px;
}
.adm-tabstrip .tab { color: #A89878; }
.adm-tabstrip .tab.active { color: #E8B87A; border-bottom-color: #E8B87A; }

/* The strip floats over the fleet map rather than pushing it down. It used to
   carry an inline z-index:1000 — the same band as .modal-container — which was
   ten times the nav drawer's 100, so the Concierge's account menu opened UNDER
   the tabs: the first item was invisible and elementFromPoint at that spot
   returned .tab, i.e. the tap went to the tab bar, not the menu.
   The strip only ever needs to beat #map inside .main, so it sits in a local
   band well under the drawer (100), the edge affordances (900), and modals
   (1000).
   UPDATE 2026-08-17: .main now carries isolation:isolate, so this is belt and
   braces rather than the thing holding the line — nothing inside .main can
   reach page chrome any more, whatever number it claims. That change was
   forced by Leaflet, which plants 400/800/1000 inside .main and covered the
   drawer exactly as the tab strip once did. The convention below still stands
   for OUR elements; it just no longer depends on every one of them obeying. */
.adm-tabstrip-float {
  position: absolute;
  top: 14px;
  left: 14px;
  right: 14px;
  z-index: 40;
}

/* ---- Onboarding splash ---- */
.onboard {
  position: fixed;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  /* Cream limestone surface matching the in-app body + landing.
     Stone backdrop dropped so the entire onboarding flow (Member,
     Caddie, Clubhouse) reads as one continuous calm canvas. */
  background-color: #F2E8D4;
  background-image: none;
  z-index: 55;
  /* Tall steps (license upload + ID review, payment + Stripe mount,
     etc.) scroll cleanly; short steps still get centered via the
     auto margins on .onboard-inner below. */
  overflow-y: auto;
  box-sizing: border-box;
}
.onboard::before {
  /* Cream-lift overlay no longer needed -- the surface is already
     cream. Zeroed out instead of deleted so legacy selectors that
     reach for this layer don't break. */
  content: '';
  position: fixed;
  inset: 0;
  background: none;
  pointer-events: none;
  z-index: 0;
}
.onboard-inner {
  position: relative;
  z-index: 1;
  width: 100%;
  max-width: 640px;
  /* Auto vertical margins center the step in the column-flex .onboard
     parent. Replaces the old `align-items: center` on the parent,
     which clipped tall steps to the top and squished tall content;
     auto margins fall back to 0 when content overflows, so the user
     can scroll without losing the top edge. Horizontal 16px keeps
     the side breathing room. */
  margin: auto 16px;
  /* Extra bottom padding so the last control in a step clears the founder's
     persona-switcher FAB (fixed, bottom-right, 56px + 18px inset). Without it
     the FAB sits on top of the step's primary button — on the licence step it
     covered the right end of "Verify & continue". */
  padding: 36px 24px 96px;
  text-align: center;
}
.onboard-inner .onboard-name,
.onboard-inner .label { color: #1f160a; }

/* Onboard fields inherit the global soft inset wells (.input / .select /
   .textarea). Only .landing-input needs a skin here — its base style is the
   dark cast-bronze plate used on the landing, which would read as a black
   box on the cream onboard canvas. Same well, same focus ring; it keeps its
   own centered, tracked display typography. */
.onboard-inner .landing-input {
  background: var(--field-bg);
  border: 1px solid transparent;
  box-shadow: var(--field-inset);
  color: var(--ink);
  border-radius: var(--field-radius);
}
.onboard-inner .landing-input:hover { background: var(--field-bg-hover); }
.onboard-inner .landing-input:focus {
  background: var(--field-bg-focus);
  box-shadow: var(--field-inset), 0 0 0 3px var(--field-ring);
}
.onboard-inner .landing-input::placeholder {
  color: rgba(58, 38, 20, 0.5);
}

/* Stack the action buttons on each onboard step (primary on top, skip
   below) so neither has to wrap. */
.onb-actions {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin-top: 22px;
}

/* Native file picker — inset well body, copper tab as the action moment. */
input[type="file"] {
  width: 100%;
  display: block;
  cursor: pointer;
  background: var(--field-bg);
  border: 1px solid transparent;
  box-shadow: var(--field-inset);
  border-radius: var(--field-radius);
  padding: 10px 12px;
  font-family: var(--font-ui);
  font-size: 14px;
  font-weight: 500;
  color: var(--ink);
}
input[type="file"]::file-selector-button {
  margin-right: 14px;
  padding: 9px 14px;
  background-image: var(--copper);
  background-size: var(--copper-size);
  background-repeat: no-repeat;
  color: #FFFFFF;
  text-shadow: 0 1px 2px rgba(40, 22, 6, 0.55);
  border: 1px solid rgba(40, 22, 6, 0.55);
  border-radius: 4px;
  font-family: var(--font-ui);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  cursor: pointer;
  transition: filter var(--duration) var(--ease);
  box-shadow:
    inset 0 1px 0 rgba(255, 230, 175, 0.42),
    inset 0 -1px 0 rgba(0, 0, 0, 0.28);
}
input[type="file"]::file-selector-button:hover { filter: brightness(1.08); }

/* ---------- ID capture card (Caddie onboarding, licence step) ----------
   A licence-shaped card the Caddie taps to photograph their licence, which
   then shows the shot back before they commit it. The whole step is driven
   by one attribute on .id-capture[data-state]:
     empty | ready | uploading | pending | approved | rejected
   so the JS never toggles inline display and the states can't drift apart. */
.id-capture { position: relative; margin-top: 4px; }
/* The real input stays in the DOM and focusable — the label is the visible
   target — so keyboard and screen-reader users get the native control. */
.id-capture > input[type="file"] {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: 0;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
  box-shadow: none; background: none;
}
.id-capture-zone {
  position: relative;
  display: flex; align-items: center; justify-content: center;
  width: 100%;
  /* ~1.58:1 — a licence's own proportion, so the guide reads as the thing
     being photographed rather than a generic dropzone. */
  aspect-ratio: 1.58 / 1;
  border-radius: 10px;
  background: var(--field-bg);
  box-shadow: var(--field-inset);
  border: 1.5px dashed rgba(138, 85, 31, 0.55);
  cursor: pointer;
  overflow: hidden;
  transition: border-color var(--duration) var(--ease),
              background var(--duration) var(--ease);
}
.id-capture-zone:hover { border-color: rgba(138, 85, 31, 0.85); background: var(--field-bg-hover); }
.id-capture > input[type="file"]:focus-visible + .id-capture-zone {
  outline: 2px solid var(--copper-deep, #8A551F); outline-offset: 2px;
}
.id-capture-cue {
  display: flex; flex-direction: column; align-items: center; gap: 6px;
  padding: 0 18px; text-align: center; color: #5C3F26;
}
.id-capture-icon { width: 34px; height: 34px; opacity: 0.75; }
.id-capture-title { font-size: 15px; font-weight: 700; letter-spacing: 0.02em; color: #3A2614; }
.id-capture-hint { font-size: 12.5px; line-height: 1.4; color: #7F6A4F; }
/* The captured shot fills the card once one exists. */
.id-capture-shot {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  display: none;
}
.id-capture-badge {
  position: absolute; left: 50%; bottom: 10px; transform: translateX(-50%);
  display: none; align-items: center; gap: 6px;
  padding: 5px 12px; border-radius: 999px;
  background: rgba(46, 33, 20, 0.9); color: #FBF4E6;
  font-size: 11px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase;
  white-space: nowrap;
}
.id-capture-retake {
  display: none;
  margin: 10px auto 0;
  padding: 7px 16px;
  border: 1px solid rgba(138, 85, 31, 0.5); border-radius: 999px;
  background: none; color: #8A551F; cursor: pointer;
  font-size: 11px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase;
}
.id-capture-retake:hover { background: rgba(184, 115, 51, 0.1); }
.id-capture-status {
  font-size: 12.5px; line-height: 1.5; color: #5C3F26;
  margin-top: 10px; min-height: 18px;
}

/* --- states --- */
/* A shot exists: show it, drop the prompt, offer a retake. */
.id-capture[data-state="ready"] .id-capture-shot,
.id-capture[data-state="uploading"] .id-capture-shot,
.id-capture[data-state="pending"] .id-capture-shot,
.id-capture[data-state="approved"] .id-capture-shot { display: block; }
.id-capture[data-state="ready"] .id-capture-cue,
.id-capture[data-state="uploading"] .id-capture-cue,
.id-capture[data-state="pending"] .id-capture-cue,
.id-capture[data-state="approved"] .id-capture-cue { display: none; }
.id-capture[data-state="ready"] .id-capture-zone,
.id-capture[data-state="uploading"] .id-capture-zone,
.id-capture[data-state="pending"] .id-capture-zone,
.id-capture[data-state="approved"] .id-capture-zone { border-style: solid; border-color: rgba(138, 85, 31, 0.4); }
.id-capture[data-state="ready"] .id-capture-retake,
.id-capture[data-state="rejected"] .id-capture-retake { display: block; }
.id-capture[data-state="ready"] .id-capture-badge,
.id-capture[data-state="uploading"] .id-capture-badge,
.id-capture[data-state="pending"] .id-capture-badge,
.id-capture[data-state="approved"] .id-capture-badge { display: inline-flex; }
/* In flight or with the Concierge the card is no longer a control — dim it
   and take the pointer away so a second tap can't replace the shot under a
   request that is already running. */
.id-capture[data-state="uploading"] .id-capture-zone,
.id-capture[data-state="pending"] .id-capture-zone,
.id-capture[data-state="approved"] .id-capture-zone { pointer-events: none; }
.id-capture[data-state="uploading"] .id-capture-shot,
.id-capture[data-state="pending"] .id-capture-shot { filter: saturate(0.6) brightness(0.82); }
.id-capture[data-state="pending"] .id-capture-badge { animation: id-capture-wait 1.8s ease-in-out infinite; }
.id-capture[data-state="approved"] .id-capture-badge { background: rgba(31, 122, 61, 0.92); }
.id-capture[data-state="rejected"] .id-capture-zone { border-color: rgba(181, 54, 54, 0.7); border-style: solid; }
.id-capture[data-state="rejected"] .id-capture-status,
.id-capture[data-state="rejected"] + .id-capture-status { color: #B53636; }
@keyframes id-capture-wait { 0%, 100% { opacity: 1; } 50% { opacity: 0.55; } }
@media (prefers-reduced-motion: reduce) {
  .id-capture[data-state="pending"] .id-capture-badge { animation: none; }
}
.onboard-mark {
  width: 100%;
  max-width: 132px;
  height: auto;
  margin: 0 auto 12px;
  display: block;
  filter: drop-shadow(0 2px 5px rgba(0,0,0,0.18));
}
.onboard-name {
  font-family: var(--font-display);
  font-size: 36px;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: #000;
  margin: 0 0 14px;
}
.onboard-lede {
  /* text-frame.png was a dark wood "copper plate" behind this copy; on the
     cream onboard surface it read as a heavy slab. Drop it and let the
     typography sit directly on cream -- dark coffee body text, no frame,
     no shadow (mirrors the wordmark treatment above). */
  max-width: 540px;
  margin: 0 auto 26px;
  padding: 0 12px;
  background: none;
  border: 0;
  border-radius: 0;
  font-family: var(--font-ui);
  font-size: 19px;
  line-height: 1.6;
  font-weight: 500;
  letter-spacing: 0.01em;
  color: #3A2614;
  text-shadow: none;
}
/* Caddie Code of Conduct training list — contrasting sans-serif against
   the serif headings, tight spacing, polished hierarchy. */
.cd-training-list {
  list-style: none;
  padding: 0;
  margin: 12px auto 18px;
  max-width: 500px;
  text-align: left;
  font-family: var(--font-ui);
  font-size: 13px;
  line-height: 1.55;
  color: #3A2614;
}
.cd-training-list li {
  position: relative;
  padding: 4px 0 4px 18px;
}
.cd-training-list li::before {
  content: '';
  position: absolute;
  left: 0;
  top: 13px;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: #8A551F;
  opacity: 0.5;
}
.onb-terms {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 18px 0 22px;
  font-size: 13px;
  /* Dark coffee on the cream onboard surface -- var(--muted) was tuned
     for the old dark-stone background and disappeared on cream. */
  color: #3A2614;
  letter-spacing: 0.02em;
  cursor: pointer;
  justify-content: center;
  line-height: 1.45;
}
.onb-terms input { accent-color: #8A551F; width: 18px; height: 18px; }
.onb-terms span { max-width: 460px; text-align: left; }

/* Dues terms on the onboarding payment step. This is the disclosure for a
   recurring charge, so it reads as a quiet receipt rather than a promo card:
   hairline box, the amount carrying the only weight on the row. Same cream
   surface and coffee ink as .onb-terms above -- var(--muted) was tuned for
   the old dark background and disappears here. */
.onb-terms-box {
  max-width: 460px;
  margin: 0 auto 20px;
  padding: 14px 16px;
  border: 1px solid rgba(58, 38, 20, 0.16);
  border-radius: 10px;
  text-align: left;
}
.onb-dues-line {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  font-size: 14px;
  color: #3A2614;
  letter-spacing: 0.01em;
}
.onb-dues-line strong {
  font-weight: 700;
  color: #8A551F;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
/* Plan picker. Used in two places — onboarding step 4 and the Locker's Plan
   panel — so it carries no positioning of its own; the container places it.
   Rows read as a choice, not as buttons: quiet by default, copper only on the
   selected one, so the tier a Member is already on is the thing their eye
   lands on. */
.plan-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin: 0 0 12px;
}
.plan-opt {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  width: 100%;
  padding: 11px 13px;
  border: 1px solid rgba(58, 38, 20, 0.16);
  border-radius: 10px;
  background: rgba(251, 246, 234, 0.55);
  color: #3A2614;
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: border-color 140ms ease, background 140ms ease;
}
.plan-opt:hover:not(:disabled) { border-color: rgba(184, 115, 51, 0.5); }
.plan-opt.is-on {
  border-color: #B87333;
  background: rgba(184, 115, 51, 0.08);
}
/* Locked = a tier that exists but is not reachable from here (a downgrade, or
   one whose Stripe price is not created yet). Dimmed rather than hidden: a
   Member who cannot see their own options assumes the app is broken. */
.plan-opt.is-locked {
  opacity: 0.5;
  cursor: default;
}
.plan-opt-main {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}
.plan-opt-name {
  font-size: 13.5px;
  font-weight: 600;
  letter-spacing: 0.01em;
}
.plan-opt-inc {
  font-size: 11.5px;
  line-height: 1.35;
  color: rgba(58, 38, 20, 0.7);
}
.plan-opt-price {
  font-size: 13.5px;
  font-weight: 700;
  color: #8A551F;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.plan-opt.is-on .plan-opt-price { color: #B87333; }
.plan-foot {
  margin: 10px 0 0;
  font-size: 11.5px;
  line-height: 1.45;
}

/* What the dues buy, sitting between the price and the billing terms. Reads
   heavier than the fine print below it on purpose -- the allowance is a hard
   stop, so it is a term of the sale, not a footnote to it. */
.onb-dues-allowance {
  margin: 6px 0 0;
  font-size: 12.5px;
  line-height: 1.45;
  color: rgba(58, 38, 20, 0.9);
}
.onb-dues-note {
  margin: 6px 0 0;
  font-size: 11.5px;
  line-height: 1.45;
  color: rgba(58, 38, 20, 0.72);
}

/* Onboarding wizard step transitions + bits */
.onb-progress {
  display: flex;
  justify-content: center;
  gap: 10px;
  margin-bottom: 22px;
}
.onb-progress .dot {
  width: 8px; height: 8px;
  border-radius: 50%;
  background: var(--border);
  transition: background 0.2s ease;
}
.onb-progress .dot.active { background: var(--accent); }
.onb-progress .dot.done { background: var(--accent-dark); }
.onb-step { text-align: center; }

.onb-house-row {
  display: grid;
  grid-template-columns: 1fr 1fr auto;
  gap: 8px;
  margin-top: 12px;
}
.onb-house-row .btn { padding: 11px 16px; }

/* Chosen-address confirmation chip for the single-field onboarding
   address autocomplete. Replaces the input once a match is picked. */
.onb-addr-chosen {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-top: 10px;
  padding: 14px 16px;
  border: 1px solid rgba(184,115,51,0.4);
  border-radius: 12px;
  background: rgba(184,115,51,0.06);
  font-family: var(--font-ui);
  font-size: 16px;
  color: var(--cream);
}
.onb-addr-chosen .link { font-family: var(--font-ui); font-size: 12px; }
.onb-household-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 10px 12px;
  background: rgba(184,115,51,0.06);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  margin-bottom: 6px;
  font-size: 13px;
}
.onb-household-item .x {
  cursor: pointer;
  color: var(--muted);
  font-size: 18px;
  padding: 0 6px;
}
.onb-household-item .x:hover { color: var(--danger); }

.stripe-card-mount {
  background: var(--field-bg);
  border: 1px solid transparent;
  box-shadow: var(--field-inset);
  border-radius: var(--field-radius);
  padding: 14px 16px;
  min-height: 52px;
  transition: background var(--duration) var(--ease), box-shadow var(--duration) var(--ease);
}
.stripe-card-mount.StripeElement--focus {
  background: var(--field-bg-focus);
  box-shadow: var(--field-inset), 0 0 0 3px var(--field-ring);
}

/* Express-wallet button host (Apple Pay / Google Pay). In live mode Stripe
   mounts its own Payment Request Button here; in demo mode we render the
   look-alike .wallet-btn(s) below. */
.wallet-mount { min-height: 48px; }
.wallet-mount .wallet-btn + .wallet-btn { margin-top: 8px; }

/* Demo wallet buttons (mock mode, where Stripe's real button can't mount).
   Mirror the native pills so the demo persona sees the real flow. */
.wallet-btn {
  width: 100%;
  min-height: 48px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 0 18px;
  border: none;
  border-radius: 6px;
  font-family: var(--font-ui);
  font-size: 18px;
  font-weight: 500;
  cursor: pointer;
  transition: opacity var(--duration) var(--ease);
}
.wallet-btn:hover { opacity: 0.88; }
.wallet-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.wallet-btn svg { display: block; }

/* Apple Pay — Apple's black pill. */
.apple-pay-btn { background: #000; color: #fff; }
.apple-pay-btn svg { margin-top: -2px; }

/* Google Pay — Google's light pill with the 4-colour "G" mark. */
.gpay-btn {
  background: #fff;
  color: #3c4043;
  border: 1px solid rgba(60, 64, 67, 0.24);
  font-weight: 600;
}

/* "or pay with card" separator between the wallets and manual card entry. */
.pay-divider {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 14px 0;
  color: var(--muted);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}
.pay-divider::before,
.pay-divider::after {
  content: '';
  flex: 1;
  height: 1px;
  background: var(--border);
}

/* My Locker — current card on file rendered as "Visa •••• 4242". */
.locker-card-display {
  font-size: 16px;
  color: var(--ink);
  letter-spacing: 0.04em;
  margin-top: 2px;
}
.locker-card-display strong { font-weight: 600; margin-right: 4px; }

/* Household roster row — name + relation pill + phone + remove "x". */
.hh-row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 12px;
  background: rgba(184, 115, 51, 0.06);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  margin-bottom: 8px;
}
.hh-row-main { flex: 1; min-width: 0; }
.hh-row-name {
  font-size: 14px;
  font-weight: 500;
  color: var(--ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.hh-row-meta {
  margin-top: 2px;
  font-size: 12px;
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
}
.hh-relation {
  font-size: 10px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--copper-deep);
  font-weight: 600;
}
.hh-x {
  background: transparent;
  border: none;
  color: var(--muted);
  font-size: 20px;
  cursor: pointer;
  padding: 0 6px;
  line-height: 1;
}
.hh-x:hover { color: var(--danger); }

/* ---- Booking steps + street autocomplete ---- */
.step { margin-bottom: 12px; }
.step-hint {
  font-size: 11px;
  color: var(--accent);
  margin-top: 6px;
  letter-spacing: 0.04em;
  cursor: pointer;
}
.step-hint:hover { color: var(--cream); }

.autocomplete { position: relative; }
.autocomplete-menu {
  position: absolute;
  top: calc(100% + 2px);
  left: 0; right: 0;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  max-height: 220px;
  overflow-y: auto;
  z-index: 200;
  box-shadow: 0 8px 24px rgba(0,0,0,0.35);
}
.autocomplete-item {
  padding: 10px 12px;
  font-size: 13px;
  cursor: pointer;
  border-bottom: 1px solid var(--border);
  color: var(--text);
}
.autocomplete-item:last-child { border-bottom: 0; }
.autocomplete-item:hover, .autocomplete-item.active {
  background: rgba(184,115,51,0.1);
  color: var(--accent);
}
.autocomplete-empty {
  padding: 14px;
  text-align: center;
  color: var(--muted);
  font-size: 12px;
  font-style: italic;
}

/* ---- Favorites ---- */
.fav-row {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-bottom: 10px;
}
.fav-pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 11px;
  background: transparent;
  color: var(--cream);
  border: 1px solid var(--border);
  border-radius: 999px;
  font-size: 11px;
  letter-spacing: 0.08em;
  cursor: pointer;
  transition: all 0.15s ease;
}
.fav-pill:hover { border-color: var(--accent); color: var(--accent); }
.fav-pill.active { border-color: var(--accent); color: var(--accent); background: rgba(184,115,51,0.1); }
.fav-pill .star { color: var(--accent); font-size: 11px; }
.fav-pill .x {
  color: var(--muted); font-size: 14px; line-height: 1;
  padding: 0 2px; border-radius: var(--radius-sm);
}
.fav-pill .x:hover { color: var(--danger); background: rgba(194,92,92,0.1); }

.dest-row { display: flex; gap: 8px; align-items: stretch; }
.dest-row .select { flex: 1; }
.fav-toggle {
  background: transparent;
  border: 1px solid var(--border);
  color: var(--muted);
  border-radius: var(--radius-sm);
  width: 44px;
  font-size: 18px;
  cursor: pointer;
  transition: all 0.15s ease;
}
.fav-toggle:hover { border-color: var(--accent); color: var(--accent); }
.fav-toggle.saved { color: var(--accent); border-color: var(--accent); }

/* ---- Membership card (brandkit ISO ID-1 spec) ----
   At desktop widths the 1.586:1 ratio is kept; on mobile the ratio is
   dropped so the name / number / since lines never clip. */
.member-card {
  position: relative;
  aspect-ratio: 1.586 / 1;
  min-height: 200px;
  background: linear-gradient(155deg, #3E2D1D 0%, #2C2014 60%, #1F160D 100%);
  border: 1px solid rgba(184,115,51, 0.45);
  border-radius: 20px;
  padding: 24px;
  color: var(--cream);
  overflow: hidden;
  margin-bottom: 0; /* parent member-stack handles spacing via gap */
  box-shadow: var(--shadow-elevated);
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
}
.member-card::before {
  content: '';
  position: absolute;
  inset: 0;
  background: radial-gradient(circle at 50% 12%, rgba(184, 115, 51, 0.20), transparent 55%);
  pointer-events: none;
}
.member-card .mc-mark {
  display: block;
  width: 52px;
  height: 52px;
  margin: 0 auto 6px;
  border-radius: 8px;
}
.member-card .mc-monogram {
  font-family: var(--font-display);
  font-size: 32px;
  font-weight: 600;
  letter-spacing: 0.04em;
  color: var(--accent);
  line-height: 1;
}
.member-card .mc-club {
  font-family: var(--font-display);
  font-size: 14px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--cream);
  margin-top: 8px;
}
.member-card .mc-name {
  font-family: var(--font-display);
  font-size: 26px;
  font-weight: 500;
  letter-spacing: 0.01em;
  margin-top: auto;
  color: var(--cream);
}
.member-card .mc-row {
  display: flex;
  justify-content: space-between;
  align-items: flex-end;
  margin-top: 12px;
  font-size: 10px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  width: 100%;
  text-align: left;
}
.member-card .mc-row > div { text-align: center; flex: 1; }
.member-card .mc-row > div + div { text-align: center; }
.member-card .mc-row span { color: var(--muted); }
.member-card .mc-row strong {
  color: var(--cream);
  font-family: var(--font-ui);
  font-size: 14px;
  letter-spacing: 0.04em;
  font-weight: 500;
}
.member-card .mc-tier {
  position: absolute;
  top: 14px;
  left: 50%;
  transform: translateX(-50%);
  font-size: 10px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--accent);
  font-weight: 600;
}

.empty-state {
  text-align: center;
  padding: 22px 16px;
  color: var(--muted);
  font-style: italic;
  font-family: var(--font-ui);
  font-size: 16px;
  background: var(--panel-2);
  border: 1px solid rgba(184, 115, 51, 0.18);
  border-radius: 8px;
}

/* leaflet overrides */
.leaflet-container { background: #2A1E14; }
.leaflet-popup-content-wrapper, .leaflet-popup-tip {
  background: var(--panel-2); color: var(--text);
  border: 1px solid var(--border);
}
.leaflet-popup-content { margin: 12px 14px; font-family: var(--font-ui); }
.leaflet-bar a { background: var(--panel-2); color: var(--text); border-bottom-color: var(--border); }
.leaflet-bar a:hover { background: var(--panel); color: var(--accent); }

/* Live ride cart marker: inline SVG with transparent bounds so Leaflet
   doesn't show the checkerboard from the old PNG asset. The shell class
   keeps the hitbox tight and the SVG can rotate cleanly via cart-anim.js. */
.cart-marker-shell {
  background: transparent;
  border: 0;
}
/* The bronze cart (restored 2026-08-09). Mirrors the .cart-svg status
   treatments below so a charging or offline cart still reads at a glance —
   without them the artwork is identical whatever the cart is doing.
   transform-origin matters: cart-anim.js flips this on scaleX, and a flip
   about the wrong origin makes the cart jump sideways as it turns. */
.cart-img {
  display: block;
  width: 38px;
  height: 38px;
  object-fit: contain;
  transform-origin: 50% 50%;
  transition: transform 160ms ease;
  filter: drop-shadow(0 3px 5px rgba(46, 33, 20, 0.45));
  -webkit-user-drag: none;
  user-select: none;
}
.cart-img-in_use { filter: drop-shadow(0 3px 6px rgba(46, 33, 20, 0.52)); }
.cart-img-charging { filter: drop-shadow(0 3px 5px rgba(216, 179, 106, 0.55)) brightness(1.05); }
.cart-img-maintenance,
.cart-img-offline {
  filter: drop-shadow(0 2px 3px rgba(46, 33, 20, 0.22)) saturate(0.45) opacity(0.78);
}

.cart-svg {
  display: block;
  overflow: visible;
  transform-origin: 50% 50%;
  filter: drop-shadow(0 3px 5px rgba(46, 33, 20, 0.45));
}
.cart-svg-in_use { filter: drop-shadow(0 3px 6px rgba(46, 33, 20, 0.52)); }
.cart-svg-charging { filter: drop-shadow(0 3px 5px rgba(216, 179, 106, 0.55)) brightness(1.05); }
.cart-svg-maintenance,
.cart-svg-offline {
  filter: drop-shadow(0 2px 3px rgba(46, 33, 20, 0.22)) saturate(0.45) opacity(0.78);
}

/* Trip endpoints. Shape is the encoding — dot = origin, square = destination —
   so the pair survives a color-vision deficiency and reads without a legend.
   Both carry a cream ring so they hold up over dark tiles as well as light. */
.trip-endpoint { background: transparent; border: 0; }
.trip-endpoint .te-dot,
.trip-endpoint .te-square {
  display: block;
  width: 14px; height: 14px;
  background: var(--copper-mid, #B87333);
  border: 2.5px solid #F5E8D1;
  box-shadow: 0 2px 6px rgba(30, 18, 8, 0.45);
}
.trip-endpoint .te-dot { border-radius: 50%; }
.trip-endpoint .te-square {
  border-radius: 3px;
  background: #27392F;
}

/* Fleet map: the far end of a cart's leading route, direct-labelled with the
   cart that is driving to it. Several carts are out at once and every route is
   the same copper, so the label is what attributes a line to a cart without
   making the desk read a legend.

   The kind of place is carried by the GLYPH, not by colour — a house for a
   residence, a flag for a venue. This map is already almost entirely copper
   (cart pins, Yard pins, the routes), so a second hue would read as a third
   category of object rather than as a property of this one, and the shapes
   separate at any zoom and for any kind of colour vision. */
.fleet-stop {
  background: transparent;
  border: 0;
  display: flex;
  align-items: center;
  gap: 5px;
  /* The icon box is 22px; the label runs past it. Without this the flex row is
     clipped to the box and only the glyph survives. */
  width: max-content !important;
  pointer-events: none;
}
.fleet-stop .fs-mark {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: 22px; height: 22px;
  border-radius: 50%;
  background: #27392F;
  border: 2px solid #F5E8D1;
  box-shadow: 0 2px 6px rgba(30, 18, 8, 0.45);
  color: #F5E8D1;
  font-size: 13px;
  line-height: 1;
  pointer-events: auto;
}
.fleet-stop .fs-label {
  flex: 0 0 auto;
  padding: 2px 7px;
  border-radius: 999px;
  background: rgba(245, 232, 209, 0.94);
  border: 1px solid rgba(184, 115, 51, 0.55);
  color: var(--ink, #2E2114);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.02em;
  line-height: 1.4;
  white-space: nowrap;
  box-shadow: 0 1px 4px rgba(30, 18, 8, 0.25);
}

/* Basemap tone. CARTO's tiles are stock blue-grey cartography, which made the
   map the one surface in the app not wearing the brand — everything around it
   is cream/copper/ink. Pulling saturation down and warming slightly moves the
   tiles toward the palette and, just as importantly, stops the basemap's own
   colors competing with the copper route and the status pins. Scoped to the
   tile pane so markers, routes and controls keep their true colors. Mild
   enough to hold up on both the daytime and night tile sets. */
.leaflet-tile-pane {
  filter: saturate(0.72) sepia(0.16) contrast(1.02);
}

/* ---- En-route bottom sheet ----
   Default (desktop / wide): the ride-sheet looks like a .card. When
   `body.ride-active` is set the map fills the whole member-stack and
   the sheet becomes a collapsible overlay anchored to the bottom — the
   Uber-style ride view. */
.ride-sheet {
  background-color: var(--panel);
  background-image: none;
  border: 1px solid rgba(184, 115, 51, 0.25);
  border-left: 3px solid var(--copper);
  border-radius: 10px;
  padding: 22px;
  margin-bottom: 14px;
  color: var(--ink);
  box-shadow: 0 14px 34px rgba(120, 85, 50, 0.12);
}
.ride-sheet .sheet-handle { display: none; }
.ride-sheet .sheet-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 14px;
  margin-bottom: 14px;
}
.ride-sheet .sheet-title h3 {
  margin: 0 0 4px;
  font-family: var(--font-display);
  font-size: 11px;
  font-weight: 600;
  color: var(--accent);
  text-transform: uppercase;
  letter-spacing: 0.3em;
}
.ride-sheet .sheet-title strong {
  font-family: var(--font-ui);
  font-size: 20px;
  font-weight: 500;
  color: var(--ink);
}

body.ride-active .topbar {
  padding: calc(8px + var(--safe-top)) calc(12px + env(safe-area-inset-right)) 8px calc(12px + env(safe-area-inset-left));
}
body.ride-active .topbar .brand .wordmark .sub {
  display: none;
}
body.ride-active .topbar .links a {
  display: inline-block;
}
body.ride-active .topbar .brand .mark {
  /* Still the deliberate shrink that hands the screen to the live map, just
     height-driven like the other two so the crest keeps its own aspect. */
  height: 54px;
  width: auto;
}
body.ride-active .topbar .brand .wordmark .name {
  font-size: 15px;
  letter-spacing: 0.08em;
}
body.ride-active .topbar .brand .wordmark .name-img {
  height: 44px;
}
body.ride-active .member-stack {
  padding: 0;
  position: relative;
  overflow: hidden;
  max-width: 100%;
  gap: 0;
}
body.ride-active #membership-card,
body.ride-active #book-panel,
body.ride-active #history-panel,
body.ride-active #premium-panel,
body.ride-active #sitters-panel,
body.ride-active #sitter-inbound-panel {
  display: none !important;
}
body.ride-active #map.ride-map {
  display: block !important;
  position: absolute;
  inset: 0;
  height: auto;
  margin: 0;
  border: 0;
  border-radius: 0;
  z-index: 1;
}
body.ride-active #ride-panel.ride-sheet {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  margin: 0;
  z-index: 2;
  border-radius: 24px 24px 0 0;
  border-bottom: 0;
  padding: 0;
  max-height: 54vh;
  overflow-y: auto;
  box-shadow: 0 -16px 40px rgba(0,0,0,0.45);
  transition: max-height 0.3s var(--ease);
  -webkit-overflow-scrolling: touch;
}
body.ride-active .ride-sheet .sheet-handle {
  display: block;
  width: 100%;
  background: var(--panel);
  border: 0;
  padding: 12px 0 6px;
  cursor: pointer;
  position: sticky;
  top: 0;
  z-index: 3;
  border-radius: 24px 24px 0 0;
}
body.ride-active .ride-sheet .sheet-handle .sheet-chevron {
  display: block;
  width: 20px;
  height: 20px;
  margin: 0 auto;
  color: var(--copper-dark);
  transition: transform 0.25s var(--ease), color var(--duration) var(--ease);
}
body.ride-active .ride-sheet .sheet-handle:hover .sheet-chevron,
body.ride-active .ride-sheet .sheet-handle:active .sheet-chevron { color: var(--accent); }
body.ride-active .ride-sheet .sheet-handle .sheet-toggle-label {
  display: block;
  font-family: var(--font-ui);
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--copper-soft);
  margin-top: 3px;
  transition: color var(--duration) var(--ease);
}
body.ride-active .ride-sheet .sheet-handle:hover .sheet-toggle-label,
body.ride-active .ride-sheet .sheet-handle:active .sheet-toggle-label { color: var(--accent); }
body.ride-active .ride-sheet .sheet-header { padding: 6px 22px 12px; margin: 0; }
/* Collapsed: just the handle + one line of header, no scrollbar, no body. */
body.ride-active .ride-sheet.collapsed { max-height: 98px !important; overflow: hidden; }
body.ride-active .ride-sheet.collapsed .sheet-body { display: none; }
.sheet-header-actions {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-shrink: 0;
}
body.ride-active .ride-sheet .sheet-driver-row {
  display: flex;
  align-items: center;
  gap: 14px;
  width: 100%;
}
body.ride-active .ride-sheet .sheet-driver-row .sheet-avatar {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  object-fit: cover;
  flex-shrink: 0;
  border: 2px solid var(--copper-soft);
}
body.ride-active .ride-sheet .sheet-driver-row .sheet-title {
  flex: 1;
  min-width: 0;
}
body.ride-active .ride-sheet .sheet-driver-row .sheet-title h3 {
  font-size: 18px;
  font-weight: 700;
  font-family: var(--font-display);
  margin: 0;
  line-height: 1.2;
}
body.ride-active .ride-sheet .sheet-driver-row .badge {
  flex-shrink: 0;
}
body.ride-active .ride-sheet .sheet-body {
  padding: 0 22px calc(18px + env(safe-area-inset-bottom, 0));
}

body.ride-active .ride-sheet .sheet-summary-route {
  font-size: 13px;
  line-height: 1.45;
  color: var(--ink);
  opacity: 0.88;
}
body.ride-active .ride-sheet .sheet-summary-pills {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
body.ride-active .ride-sheet .sheet-pill {
  display: inline-flex;
  align-items: center;
  padding: 6px 10px;
  border-radius: 999px;
  background: rgba(184, 115, 51, 0.10);
  border: 1px solid rgba(184, 115, 51, 0.18);
  color: var(--accent-dark);
  font-size: 12px;
  font-weight: 600;
}
body.ride-active .ride-sheet .sheet-body > .h-divider:first-child {
  display: none;
}
body.ride-active .ride-sheet .sheet-body > .label:nth-child(2),
body.ride-active .ride-sheet .sheet-body > #ride-cart:nth-child(3),
body.ride-active .ride-sheet .sheet-body > .label:nth-child(4) {
  display: none;
}
body.ride-active .ride-sheet .person-row {
  margin-top: 2px;
  margin-bottom: 14px;
}
body.ride-active .ride-sheet .fare-line {
  font-size: 12px;
}
body.ride-active .ride-sheet.collapsed { max-height: none; overflow: hidden; }
body.ride-active .ride-sheet.collapsed .sheet-body { display: none; }
body.ride-active .ride-sheet.collapsed .sheet-header { padding-bottom: 18px; }

/* ---- Gated sub-neighborhood prompt (Member + Club House host) ---- */
.gate-prompt {
  margin-top: 14px;
  padding: 14px 16px;
  background: rgba(184,115,51,0.06);
  border: 1px solid var(--border-2);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius-md);
}
.gate-prompt .label { margin-top: 0; }
.gate-prompt .gate-text {
  font-size: 13px;
  color: var(--cream);
  margin: 4px 0 10px;
  line-height: 1.45;
}

/* On the Caddie's ride card the gate code is the most important thing
   on screen once a Caddie pulls up to a gate. */
.gate-callout {
  margin-top: 12px;
  padding: 14px 16px;
  background: var(--accent);
  color: var(--cream);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-card);
}
.gate-callout .gc-label {
  font-size: 10px; letter-spacing: 0.3em; text-transform: uppercase;
  opacity: 0.85;
}
.gate-callout .gc-pin {
  font-family: var(--font-ui);
  font-size: 28px; font-weight: 600; letter-spacing: 0.2em;
  margin-top: 2px;
}
.gate-callout .gc-meta { font-size: 11px; opacity: 0.85; margin-top: 4px; }
.gate-callout.no-pin { background: rgba(216,179,106,0.18); color: var(--warn); }

/* The .preferred-route card was removed 2026-08-07 (see renderPreferredRoute's
   removal note in frontend/driver/app.js). The route the Caddie drives is
   shown as a route now, in the framed Google directions overlay. */

/* Unlocated-cart notice in the Caddie portal. Deliberately not styled as a
   passing hint: while this shows, the geofence and overspeed telemetry the
   insurance program is priced on has nothing to score. The demo variant is
   visually distinct so a simulated shift can never be mistaken for a real
   one. See renderGpsStatus() in frontend/driver/app.js. */
.gps-status {
  margin: 10px 0 0;
  padding: 10px 12px;
  border-radius: var(--radius-md);
  border: 1px solid rgba(216, 179, 106, 0.45);
  background: rgba(216, 179, 106, 0.16);
  color: var(--ink);
  font: 600 12px/1.4 var(--font-ui);
}
.gps-status.demo {
  border-color: rgba(184, 115, 51, 0.5);
  background: rgba(184, 115, 51, 0.14);
}
.gps-status[hidden] { display: none; }

/* App-shell only: which position source is live. Recessive by design — this
   is a reassurance, not a warning, and it sits directly above .gps-status
   which IS a warning. If the two competed the one that matters would lose. */
.gps-source {
  margin: 10px 0 0;
  padding: 7px 10px;
  border-radius: var(--radius-md);
  border: 1px solid var(--divider);
  background: rgba(60, 42, 24, 0.04);
  color: var(--muted);
  font: 600 11px/1.3 var(--font-ui);
  letter-spacing: 0.01em;
}
/* The good state, and the one the whole app was built for. Warm olive, not
   a signal green — this palette has no cool hues in it. */
.gps-source.bg {
  border-color: rgba(94, 90, 58, 0.42);
  background: rgba(94, 90, 58, 0.12);
  color: var(--forest-2);
}
.gps-source.need-always {
  cursor: pointer;
  border-color: rgba(216, 179, 106, 0.45);
  background: rgba(216, 179, 106, 0.16);
  color: var(--ink);
}
.gps-source[hidden] { display: none; }

/* Caddie ride panel, decluttered 2026-08-09. The screen a Caddie reads while
   driving had, at arrived_pickup: three identical slide-to-confirms, a
   full-width button that reopened an already-open navigation screen, and the
   duty state written three times. These are the two controls that replaced
   the extra weight. */

/* The quiet route back into navigation. Deliberately not a .btn — it competes
   with the one slide that matters if it looks like a button. */
.nc-resume {
  display: block;
  width: 100%;
  padding: 9px 12px;
  background: transparent;
  border: 1px dashed rgba(184, 115, 51, 0.45);
  border-radius: var(--radius-md);
  color: var(--accent-dark);
  font: 600 12px/1.3 var(--font-ui);
  letter-spacing: 0.02em;
  text-align: center;
  cursor: pointer;
}
.nc-resume:active { background: rgba(184, 115, 51, 0.08); }

/* Cancel / no-show live behind this. Small and quiet on purpose: a Caddie
   reaches for them a few times a month and for the advancing slide on every
   ride, and the old layout gave all three the same weight. */
.trip-options-btn {
  display: block;
  margin: 10px auto 0;
  padding: 6px 10px;
  background: transparent;
  border: 0;
  color: var(--ink);
  opacity: 0.55;
  font: 600 12px/1 var(--font-ui);
  letter-spacing: 0.04em;
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
}
.trip-options-btn:active { opacity: 0.85; }
.trip-options-btn[hidden] { display: none; }

/* Above the navigation overlay (4000): reachable from inside a live trip. */
.trip-options-sheet { z-index: 4600; }

/* ---- Club House host portal ---- */
.ch-host-card {
  display: flex; align-items: baseline; justify-content: space-between;
  padding: 14px 16px;
  background: var(--panel);
  border: 1px solid var(--border-2);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius-md);
}
.ch-host-label {
  font-size: 10px; letter-spacing: 0.3em; text-transform: uppercase;
  color: var(--muted); font-weight: 600;
}
.ch-host-name {
  font-family: var(--font-display);
  font-size: 18px; letter-spacing: 0.06em; color: var(--cream);
}

/* Venue identity hero — the venue's own logo fronts the Host portal. The logo
   REPLACES the venue-name card (the serif wordmark only stands in until a
   /shared/venues/<slug>.png exists), and the hero fronts ONLY the first swipe
   panel: .hero-away (toggled by the deck's onSwipe) collapses it so every
   other section gets the full screen. */
.ch-venue-hero {
  display: flex; flex-direction: column; align-items: center; gap: 12px;
  text-align: center;
  /* The stack is a flex COLUMN with overflow-y:auto, so its children shrink
     before it ever scrolls. This hero is the one child that must not: it has
     overflow:hidden and a logo wrap with min-height:0, so a squeeze does not
     make it look cramped, it silently zeroes the wrap and the venue's brand
     disappears — leaving the card opening on a bare hairline above the
     numbers. Measured at 390x860: wrap height 0, logo 33px tall and entirely
     clipped. The 2026-08-09 fix taught the wrap to scale rather than crop
     under a squeeze; this stops the squeeze reaching it at all, and the
     column scrolls the way it was always meant to. */
  flex-shrink: 0;
  padding: 18px;
  margin-bottom: 12px;
  background: var(--panel);
  border: 1px solid var(--border-2);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius-md);
  /* This ceiling exists ONLY so .hero-away has something to animate down from;
     it was never meant to size the resting hero. At 260px it did both, and it
     cleared the real content (244px with the Thompson mark at 390px) by 15px
     -- a two-line idle sub line alone is 14 of them. Raised to 340 so the
     resting state is governed by content and the transition still has a bound.
     Belt and braces with the wrap clip below: this stops the squeeze starting,
     that stops it doing damage if it ever does. */
  max-height: 340px;
  overflow: hidden;
  transition: max-height 0.3s ease, opacity 0.3s ease,
              margin 0.3s ease, padding 0.3s ease;
}
/* The "Hosting from <name>" meta duplicated the identity right under the
   wordmark/logo — the hero shows the venue exactly once now. */
.ch-venue-hero .ch-venue-meta { display: none; }

/* The tally — the headline of the Host portal, under the venue's own logo and
   separated by a hairline so the venue's brand reads first and the numbers
   second.
   Two cells, always both present. The strip this replaced showed arrivals OR
   (when idle) the month's covers in the same space, driven by a data-state
   that swapped which of two blocks was display:none — so the two facts a Host
   compares, what is coming and what the night has done, could never be on
   screen together. */
.ch-tally {
  width: 100%;
  padding-top: 12px;
  border-top: 1px solid var(--border-2);
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 12px;
}
.ch-tally-cell {
  display: flex; flex-direction: column; align-items: center; gap: 2px;
  min-width: 0;
}
.ch-tally-rule {
  width: 1px; align-self: stretch; margin: 4px 0;
  background: var(--border-2);
}
/* The numbers carry the weight — they are the reason the screen exists. Set in
   the display face so they read as headlines rather than stat chips, and
   tabular so the two cells stay put as they tick. */
.ch-tally-n {
  font-family: var(--font-display);
  font-size: clamp(32px, 11vw, 46px);
  line-height: 1;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
}
/* Nothing inbound is a real state, not an error state: the number stays,
   quieted, because "0 carts" next to "14 arrivals today" tells a Host the
   night is running and the road is clear. A hidden zero told them nothing. */
.ch-tally[data-state="idle"] .ch-tally-cell:first-child .ch-tally-n {
  color: var(--muted);
  opacity: 0.75;
}
.ch-tally-label {
  font-family: var(--font-ui);
  font-size: 11px; font-weight: 500; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--cream);
  text-align: center;
}
/* The guest count under each number. Reserved height so the row does not jump
   as the sub appears and clears between polls. */
.ch-tally-sub {
  font-family: var(--font-ui);
  font-size: 12px; color: var(--muted); letter-spacing: 0.02em;
  min-height: 15px;
  text-align: center;
}
/* Logo mode: tighten the card around the artwork. */
.ch-venue-hero.has-logo { padding: 12px 18px 10px; }
/* Swiped off the first panel: collapse out of the way. */
.ch-venue-hero.hero-away {
  max-height: 0;
  opacity: 0;
  margin-bottom: 0;
  padding-top: 0;
  padding-bottom: 0;
  border-width: 0;
}
@media (prefers-reduced-motion: reduce) {
  .ch-venue-hero { transition: none; }
}
/* The wrap owns the clip, and the logo follows the wrap's height.
   Before 2026-08-09 neither was true, and the two failures compounded:

   .ch-venue-hero is a flex column with `max-height` + `overflow: hidden`
   (both needed by the .hero-away collapse). Whenever the hero is shorter than
   its content -- mid-collapse, or in the resting state, which cleared the old
   260px ceiling by only 15px -- the wrap is what shrinks. The logo did not
   follow it: `max-height: 152px` with no height binding kept the art at full
   size inside a box that was now smaller, so it overflowed its own wrap,
   centred, spilling equally above and below. The hero's overflow:hidden cut
   the top off, and the bottom painted straight down over the arrivals line.
   Which is what Patrick photographed: the middle band of the Thompson mark,
   clipped top and bottom, with "No one on the way right now" printed through
   it.

   `overflow: hidden` here means the art can never reach the arrivals strip
   whatever happens above. `height: 100%` + `object-fit: contain` means a
   shrinking wrap SCALES the logo instead of cropping it -- the collapse now
   shrinks the mark rather than slicing it. The percentage resolves to auto
   while the wrap's height is indefinite (the normal resting case, where
   max-height sizes the art as before) and takes effect exactly when the wrap
   becomes definite, which is the case that was broken. */
.ch-venue-logo-wrap {
  width: 100%;
  min-height: 0;
  overflow: hidden;
  display: flex; align-items: center; justify-content: center;
}
.ch-venue-logo {
  height: 100%;
  width: auto;
  max-height: 152px;
  max-width: 88%;
  object-fit: contain;
  display: block;
}
.ch-venue-wordmark {
  font-family: var(--font-display);
  font-size: clamp(28px, 8vw, 40px);
  line-height: 1.05;
  letter-spacing: 0.02em;
  color: var(--cream);
}
.ch-venue-meta {
  display: flex; flex-direction: column; align-items: center; gap: 3px;
}
/* Onboarding venue confirm card — same card vocabulary as .ch-selected /
   .ch-confirm (full hairline border, panel tint), serif identity inside. */
.ch-venue-card {
  text-align: left;
  margin: 14px 0;
  padding: 14px 18px;
  background: var(--panel);
  border: 1px solid var(--border-2);
  border-radius: var(--radius-md);
  font-family: var(--font-ui);
}
.ch-venue-card-label {
  font-family: var(--font-ui);
  font-size: 11px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 4px;
}
.ch-venue-card-name {
  font-size: 22px;
  color: var(--ink);
  font-weight: 600;
}
.ch-tabs { margin-bottom: 12px; }
.ch-pane { padding-top: 4px; }
.ch-selected {
  margin-top: 10px;
  padding: 12px 14px;
  background: var(--panel);
  border: 1px solid var(--border-2);
  border-radius: var(--radius-md);
}
.ch-selected-row {
  display: flex; justify-content: space-between; align-items: baseline;
  gap: 12px; margin-bottom: 6px;
}
.ch-confirm {
  background: var(--panel);
  border: 1px solid var(--border-2);
  border-radius: var(--radius-md);
  padding: 18px;
}
.ch-confirm-row {
  display: flex; justify-content: space-between; align-items: baseline;
  padding: 8px 0; font-size: 13px;
}
.ch-confirm-row.total {
  margin-top: 8px; padding-top: 12px;
  border-top: 1px solid var(--border); font-size: 15px; font-weight: 600;
}

/* Concierge desk Premium toggle in the Members table. */
.ps-cell { display: flex; flex-direction: column; gap: 4px; min-width: 140px; }
.ps-toggle { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; }
.ps-toggle input { accent-color: var(--accent); }
.ps-toggle span { font-size: 12px; color: var(--muted); letter-spacing: 0.06em; }
.ps-meta { display: flex; align-items: baseline; justify-content: space-between; font-size: 11px; }
.ps-meta button.link { background: none; border: 0; padding: 0; cursor: pointer; color: var(--accent); font-size: 11px; }

/* Five-star Caddie rating row on the Member post-ride sheet. */
.star-row { display: flex; gap: 8px; margin-top: 6px; }
.star-row .star {
  background: transparent; border: 0; padding: 4px 6px;
  color: var(--muted); font-size: 30px; line-height: 1; cursor: pointer;
  transition: color 0.15s ease;
}
.star-row .star:hover, .star-row .star.lit { color: var(--accent); }
.star-row .star:disabled { cursor: default; }

/* Avatar + name + neighborhood row (en-route Caddie, sitter inbound). */
.person-row { display: flex; align-items: center; gap: 12px; }
.person-avatar {
  width: 44px; height: 44px; border-radius: 50%;
  object-fit: cover; background: var(--panel-2);
  border: 1px solid var(--border);
}
.person-avatar.lg { width: 56px; height: 56px; }
.person-meta { flex: 1; min-width: 0; }
.person-meta > div { line-height: 1.25; }

/* ---------- Demo: tap-to-hop-in cart simulator ---------- */
.demo-strip {
  margin: 0 0 16px;
}
/* Demo affordance: same cream-card vocabulary as everywhere else
   (panel-2 fill, copper hairline + copper left-stripe), just sized
   as a tap target instead of a card. Drops the dashed border + the
   UPPERCASE letter-tracking that read as a debug widget. The
   italic "Demo" word in Cormorant Garamond carries the brand
   accent without shouting. */
.btn-demo {
  width: 100%;
  background: var(--panel-2);
  border: 1px solid rgba(184, 115, 51, 0.25);
  border-radius: 10px;
  font-family: var(--font-ui);
  font-size: 14px;
  font-weight: 600;
  letter-spacing: 0.02em;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  padding: 16px 20px;
  color: var(--ink);
  box-shadow: 0 6px 18px rgba(120, 85, 50, 0.10);
  transition: background 180ms, border-color 180ms, box-shadow 180ms;
}
.btn-demo em {
  font-family: var(--font-ui);
  font-style: italic;
  font-weight: 600;
  font-size: 15px;
  color: var(--copper-dark);
  letter-spacing: 0;
}
.btn-demo .demo-sep { color: rgba(58, 38, 20, 0.35); }
.btn-demo:hover {
  background: var(--panel);
  border-color: rgba(184, 115, 51, 0.45);
  box-shadow: 0 10px 24px rgba(120, 85, 50, 0.14);
}
.demo-pulse {
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--copper);
  box-shadow: 0 0 0 0 rgba(184, 115, 51, 0.4);
  animation: demoPulse 1.8s ease-out infinite;
}
@keyframes demoPulse {
  0%   { box-shadow: 0 0 0 0 rgba(184, 115, 51, 0.4); }
  70%  { box-shadow: 0 0 0 9px rgba(184, 115, 51, 0); }
  100% { box-shadow: 0 0 0 0 rgba(184, 115, 51, 0); }
}

.demo-overlay {
  position: fixed; inset: 0; z-index: 200;
  background: #2A1E14;
}
.demo-overlay-inner {
  position: absolute; inset: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.demo-overlay-head {
  position: relative;
  flex-shrink: 0;
  z-index: 1;
  display: flex; justify-content: space-between; align-items: flex-start;
  padding: 16px 16px 24px;
  background: linear-gradient(180deg, rgba(20,12,4,0.92) 60%, rgba(20,12,4,0));
}
.demo-dest-strip {
  display: flex;
  align-items: center;
  gap: 10px;
  pointer-events: auto;
}
.demo-dest-strip .select {
  font-size: 13px;
  padding: 6px 10px;
  min-width: 180px;
  background: rgba(20,12,4,0.7);
  color: #F5E8D1;
  border: 1px solid rgba(184,115,51,0.4);
  border-radius: 6px;
}
.demo-dest-strip .select option {
  background: #2E2114;
  color: #F5E8D1;
}
.demo-overlay-title {
  font-family: var(--font-display);
  font-size: 22px; font-weight: 600; color: var(--copper-bright);
  letter-spacing: 0.02em;
}
.demo-overlay-sub {
  font-family: var(--font-ui);
  font-size: 12px; color: var(--copper-soft-on-dark);
  margin-top: 4px; letter-spacing: 0.05em;
}
.demo-overlay-close {
  background: transparent; border: 0; color: var(--copper-light);
  font-size: 28px; cursor: pointer; line-height: 1;
  padding: 0 4px;
}
.demo-overlay-close:hover { color: var(--copper-bright); }
.demo-overlay-back {
  font-family: var(--font-ui);
  font-size: 12px;
  font-weight: 600;
  color: #F5E8D1;
  background: transparent;
  border: 1px solid rgba(184,115,51,0.4);
  border-radius: 6px;
  padding: 6px 12px;
  cursor: pointer;
  white-space: nowrap;
  transition: background 0.15s;
}
.demo-overlay-back:hover {
  background: rgba(184,115,51,0.2);
}
.demo-map {
  position: relative;
  flex: 1;
  min-height: 0;
  background: #2A1E14;
}
/* Bottom sheet over the full-screen map. Clean rounded top corners,
   subtle shadow, grabber handle. Collapsed state shows just the grabber +
   head row (back + locker); tapping the grabber or sheet expands it. */
.demo-sheet {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  z-index: 1000;
  background: var(--panel);
  border-radius: 20px 20px 0 0;
  box-shadow: 0 -4px 24px rgba(0,0,0,0.12);
  display: flex;
  flex-direction: column;
  color: var(--ink);
  overflow: hidden;
  transition: transform 0.25s ease, max-height 0.25s ease;
}
.demo-sheet.collapsed .demo-sheet-body {
  display: none;
}
.demo-sheet.collapsed {
  max-height: 56px;
  overflow: hidden;
}
.demo-sheet.collapsed .demo-sheet-grabber span {
  background: rgba(58,38,20,0.35);
}
.demo-sheet-grabber {
  display: flex;
  justify-content: center;
  padding: 8px 0 4px;
  flex-shrink: 0;
}
.demo-sheet-grabber span {
  width: 36px;
  height: 4px;
  border-radius: 2px;
  background: rgba(58,38,20,0.2);
}
.demo-sheet-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 16px;
  flex-shrink: 0;
}
.dsb-close {
  background: transparent;
  border: 0;
  font-size: 20px;
  color: var(--ink);
  cursor: pointer;
  padding: 4px;
  line-height: 1;
  opacity: 0.6;
}
.dsb-close:hover { opacity: 1; }
.demo-locker-link {
  font-family: var(--font-ui);
  font-size: 12px;
  font-weight: 500;
  color: var(--muted);
  text-decoration: none;
  padding: 4px;
}
.demo-locker-link:hover { color: var(--copper); }

.demo-sheet-body {
  flex: 1;
  display: flex;
  flex-direction: column;
  padding: 8px 16px 16px;
  overflow-y: auto;
  gap: 8px;
}
.dsb-dest .select {
  width: 100%;
  height: 44px;
  font-size: 15px;
  font-family: var(--font-ui);
  padding: 0 14px;
  border-radius: 10px;
  background: var(--field-bg);
  border: 1px solid transparent;
  box-shadow: var(--field-inset);
  color: var(--ink);
  cursor: pointer;
}
.dsb-dest .select:focus {
  outline: none;
  background: var(--field-bg-focus);
  box-shadow: var(--field-inset), 0 0 0 3px var(--field-ring);
}
.dsb-dest .select option {
  background: #fffaf0;
  color: var(--ink);
}

/* ─── Cover-flow destination cards ─── */
.dsb-dest-label {
  font-size: 12px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--muted);
  margin: 0 0 8px;
  padding: 0 2px;
}
.dest-cards {
  display: flex;
  gap: 10px;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-behavior: smooth;
  -webkit-overflow-scrolling: touch;
  padding: 2px 0 8px;
  scrollbar-width: none;
  user-select: none;
  -webkit-user-select: none;
  touch-action: pan-x;
}
.dest-cards::-webkit-scrollbar { display: none; }

.dest-card {
  flex: 0 0 140px;
  scroll-snap-align: start;
  border-radius: 14px;
  background: var(--panel);
  border: 1.5px solid rgba(184,115,51,0.18);
  overflow: hidden;
  cursor: pointer;
  transition: border-color 0.18s ease, box-shadow 0.18s ease, transform 0.18s ease;
  position: relative;
}
.dest-card:hover {
  border-color: rgba(184,115,51,0.45);
  box-shadow: 0 2px 12px rgba(58,38,20,0.1);
}
.dest-card.selected {
  border-color: var(--copper);
  box-shadow: 0 2px 14px rgba(139,111,71,0.25);
  transform: translateY(-1px);
}
.dest-card-logo {
  height: 80px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 28px;
  font-weight: 700;
  color: #fff;
  letter-spacing: -0.02em;
  position: relative;
  /* --mono-bg is set per card by the renderer. Keeping it in a custom property
     (rather than an inline background) is what lets .has-logo below repaint
     the plate without !important. */
  background: var(--mono-bg, linear-gradient(135deg, #8B6F47, #3a2614));
}
.dest-card-logo::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(160deg, rgba(255,255,255,0.12), transparent 60%);
}
.dest-card-mono {
  position: relative;
  z-index: 1;
  transition: opacity 0.3s ease;
}
/* Venue artwork. Cut against the same cream panel as .dest-cover-media, so the
   plate has to switch from the monogram's saturated gradient to that panel —
   a transparent logo over a deep-purple tile would read as a sticker. */
.dest-card-logo img {
  max-height: 62px;
  max-width: 84%;
  object-fit: contain;
  position: absolute;
  opacity: 0;
  transition: opacity 0.3s ease;
  z-index: 2;
}
.dest-card-logo img.loaded { opacity: 1; }
.dest-card-logo.has-logo {
  background: linear-gradient(135deg, #E8DDC8 0%, #F5E8D1 100%);
}
.dest-card-logo.has-logo .dest-card-mono { opacity: 0; }
/* The sheen exists to give a flat colour tile some dimension; over the cream
   panel it just fogs the artwork. */
.dest-card-logo.has-logo::after { background: none; }
.dest-card-info {
  padding: 8px 10px 10px;
}
.dest-card-name {
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
  line-height: 1.3;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.dest-card-meta {
  font-size: 11px;
  color: var(--muted);
  margin-top: 2px;
}
.dsb-hint {
  font-size: 12px;
  color: var(--muted);
  text-align: center;
  margin-top: 6px;
}
.dsb-cart {
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.dsb-cart-head {
  display: flex;
  align-items: center;
  gap: 8px;
}
.dsb-cart-name {
  font-family: var(--font-display);
  font-size: 16px;
  font-weight: 600;
  color: var(--ink);
}
.dsb-cart-seats {
  font-size: 12px;
  color: var(--muted);
}
.dsb-cart-close {
  margin-left: auto;
  background: transparent;
  border: 0;
  font-size: 20px;
  color: var(--muted);
  cursor: pointer;
  padding: 4px;
  line-height: 1;
}
.dsb-cart-close:hover { color: var(--ink); }
.dsb-cart-stats {
  font-size: 13px;
  color: var(--muted);
  display: flex;
  align-items: center;
  gap: 6px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.dsb-dot {
  color: rgba(58,38,20,0.3);
  font-size: 8px;
}
.dsb-riders {
  font-size: 12px;
  color: var(--muted);
}
.dsb-reason {
  font-size: 11px;
  color: var(--muted);
  font-style: italic;
}

.demo-confirm {
  position: absolute;
  left: 14px; right: 14px; bottom: 14px;
  z-index: 1000;
  max-width: 520px;
  margin: 0 auto;
  background: var(--panel);
  backdrop-filter: blur(8px);
  border: 1px solid rgba(184, 115, 51, 0.25);
  border-left: 3px solid var(--copper);
  border-radius: 12px;
  box-shadow: 0 18px 44px rgba(120, 85, 50, 0.28);
  color: var(--ink);
}

/* Leaflet popup styling: small parchment card anchored to the cart
   icon. Same cream-on-dark accessibility-friendly surface as the
   bottom sheet -- consistent contrast story across the demo. */
.leaflet-popup-content-wrapper.demo-cart-popup,
.leaflet-popup.demo-cart-popup .leaflet-popup-content-wrapper {
  background: rgba(245, 232, 209, 0.97);
  color: #3a2614;
  border: 1px solid rgba(138, 85, 31, 0.55);
  border-radius: 8px;
  box-shadow: 0 8px 22px rgba(0,0,0,0.35);
  padding: 0;
}
.leaflet-popup.demo-cart-popup .leaflet-popup-tip {
  background: rgba(245, 232, 209, 0.97);
  border: 1px solid rgba(138, 85, 31, 0.55);
}
.leaflet-popup.demo-cart-popup .leaflet-popup-content {
  margin: 10px 12px;
  font-family: var(--font-ui);
  font-size: 12px;
  line-height: 1.5;
  min-width: 180px;
}
.dcp-name {
  font-family: var(--font-display);
  font-size: 16px; font-weight: 500;
  color: #3a2614;
  margin-bottom: 6px;
}
.dcp-row {
  display: flex; justify-content: space-between; gap: 12px;
  color: #5C3F26;
}
.dcp-row span:first-child { color: #8A7560; font-size: 11px; }
.dcp-row span:last-child  { color: #3a2614; font-weight: 600; }
.demo-cart-card {
  padding: 18px 20px 20px;
}
/* New refined header: cart name in Cormorant serif, capacity chip
   right-aligned. Replaces the old .demo-cart-row hierarchy. */
.dcd-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px;
  padding-right: 28px; /* room for the close × */
  margin-bottom: 14px;
}
.dcd-name {
  font-family: var(--font-display);
  font-size: 22px;
  font-weight: 500;
  color: var(--ink);
  line-height: 1.15;
}
.dcd-seats {
  background: var(--panel-2);
  border: 1px solid rgba(184, 115, 51, 0.30);
  color: var(--copper-dark);
  padding: 5px 12px;
  border-radius: 999px;
  font-family: var(--font-ui);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.06em;
  white-space: nowrap;
}
/* 3-column stat bar with subtle dividers. Replaces the old label/value
   stacked .demo-cart-meta. Distance, ETA, Heading line up so the eye
   compares carts at a glance. */
.dcd-stats {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  background: var(--panel-2);
  border-radius: 10px;
  padding: 10px 4px;
  margin-bottom: 14px;
}
.dcd-stat {
  display: flex; flex-direction: column; align-items: center; gap: 2px;
  padding: 0 10px;
  text-align: center;
  border-right: 1px solid rgba(184, 115, 51, 0.18);
}
.dcd-stat:last-child { border-right: 0; }
.dcd-lbl {
  font-family: var(--font-ui);
  font-size: 9px; font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--muted);
}
.dcd-stat strong {
  font-family: var(--font-ui);
  font-size: 16px; font-weight: 600;
  color: var(--ink);
  line-height: 1.2;
  margin-top: 2px;
}
/* Small × in the top-right of the bottom-sheet cart inspector so the
   sheet can be dismissed without closing the whole demo overlay.
   The parent .demo-cart-card is already position: absolute via the
   combined rule above, so this works as a positioning context. */
.demo-card-close {
  position: absolute;
  top: 6px; right: 10px;
  background: transparent; border: 0;
  color: #8A7560;
  font-size: 24px; line-height: 1;
  cursor: pointer;
  padding: 4px 8px;
  z-index: 1;
}
.demo-card-close:hover { color: #3a2614; }

/* Minimize/expand toggle for the demo cart card. Positioned just left
   of the close ×. Chevron points up when expanded (showing body),
   down when collapsed (body hidden). Brand styling matches the ride
   sheet toggle — Copper text, transparent bg, uppercase label. */
.demo-card-minimize {
  position: absolute;
  top: 6px; right: 40px;
  background: transparent; border: 0;
  color: var(--copper-deep);
  font-family: var(--font-ui);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  cursor: pointer;
  display: flex; align-items: center; gap: 4px;
  padding: 4px 6px;
  z-index: 1;
  transition: color 0.15s;
}
.demo-card-minimize:hover { color: var(--copper-bright); }
.dcm-chevron {
  width: 12px; height: 12px;
  transition: transform 0.2s ease;
}

/* Collapsed state: keeps only the header row visible — body, stats,
   riders list, hop-in button, and reason text all hide. Padding
   reduces to just the header area so the card shrinks to a thin bar. */
.demo-cart-card.collapsed {
  padding: 12px 20px;
}
.demo-cart-card.collapsed .dcd-head {
  margin-bottom: 0;
  padding-right: 0;
}
.demo-cart-card.collapsed .dcd-stats,
.demo-cart-card.collapsed .demo-riders,
.demo-cart-card.collapsed #demo-hop-in,
.demo-cart-card.collapsed .demo-cart-reason,
.demo-cart-card.collapsed .demo-next-stop-card {
  display: none;
}
.demo-confirm {
  padding: 24px 22px;
  text-align: center;
}
.demo-cart-row {
  display: flex; justify-content: space-between; align-items: center;
  margin-bottom: 10px;
}
.demo-cart-row strong {
  font-family: var(--font-ui);
  font-size: 20px; color: #3a2614;
  font-weight: 500;
}
.demo-cart-pill {
  background: rgba(138, 85, 31, 0.12);
  border: 1px solid rgba(138, 85, 31, 0.5);
  color: #5C3F26;
  padding: 4px 10px; border-radius: 999px;
  font-family: var(--font-ui); font-size: 11px;
  letter-spacing: 0.1em; text-transform: uppercase; font-weight: 700;
}
.demo-cart-meta {
  font-family: var(--font-ui); font-size: 13px;
  color: #5C3F26; line-height: 1.7;
  margin-bottom: 14px;
}
.demo-cart-meta .muted { color: #8A7560; margin-right: 6px; }
.demo-cart-meta strong { font-weight: 600; color: #3a2614; }
#demo-hop-in {
  font-size: 13px;
  padding: 13px 18px;
  letter-spacing: 0.04em;
  text-transform: none;
  /* the rest comes from the .btn / .full classes — copper plate texture,
     dark espresso text, copper border. Matches every other primary
     action on the app. */
}
#demo-hop-in:disabled {
  filter: saturate(0.55);
  opacity: 0.65;
  cursor: not-allowed;
}
.demo-riders {
  background: var(--panel-2);
  border: 1px solid rgba(184, 115, 51, 0.18);
  border-radius: 10px;
  padding: 12px 14px;
  margin-bottom: 14px;
}
.demo-riders-head {
  font-family: var(--font-ui);
  font-size: 10px; font-weight: 600; letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--copper-dark);
  margin-bottom: 10px;
}
.demo-riders-head.muted { color: var(--muted); }
.demo-rider-row {
  display: grid;
  grid-template-columns: 22px 1fr auto auto;
  gap: 10px;
  align-items: center;
  font-family: var(--font-ui);
  font-size: 13px;
  color: var(--ink);
  line-height: 1.4;
  padding: 4px 0;
}
.demo-rider-row + .demo-rider-row { border-top: 1px dashed rgba(184, 115, 51, 0.15); }
.demo-next-stop-card {
  margin-bottom: 12px;
  padding: 10px 12px;
  border-radius: 10px;
  background: rgba(184, 115, 51, 0.08);
  border: 1px solid rgba(184, 115, 51, 0.22);
}
.demo-next-stop-pill {
  display: inline-flex;
  align-items: center;
  padding: 4px 8px;
  background: white;
  border: 1px solid var(--copper-dark);
  border-radius: 12px;
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  font-size: 11px;
  font-weight: 700;
  color: var(--copper-dark);
  white-space: nowrap;
}

  color: var(--ink);
}
.demo-next-stop-row strong {
  font-size: 14px;
  color: var(--copper-dark);
}
.demo-next-stop-row span {
  font-size: 11px;
  font-weight: 700;
  color: var(--copper-dark);
}
.demo-next-stop-sub {
  margin-top: 6px;
  font-size: 11px;
  line-height: 1.45;
  color: var(--muted);
}
.demo-next-stop-icon {
  background: transparent;
  border: 0;
}
.demo-next-stop-pill {
  display: inline-flex;
  align-items: center;
  padding: 4px 8px;
  background: white;
  border: 1px solid var(--copper-dark);
  border-radius: 12px;
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  font-size: 11px;
  font-weight: 700;
  color: var(--copper-dark);
  white-space: nowrap;
}

  height: 24px;
  padding: 0 10px;
  border-radius: 999px;
  background: rgba(46, 33, 20, 0.88);
  color: var(--cream);
  font-family: 'The Seasons', Cochin, 'Cormorant Garamond', Georgia, serif;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  border: 1px solid rgba(184, 115, 51, 0.45);
  box-shadow: 0 4px 12px rgba(0,0,0,0.22);
}
.demo-rider-init {
  width: 22px; height: 22px; border-radius: 50%;
  background: var(--copper-soft);
  color: var(--copper-dark);
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 10px; font-weight: 700;
  letter-spacing: 0.02em;
  border: 1px solid rgba(184, 115, 51, 0.35);
}
.demo-rider-name { color: var(--ink); font-weight: 500; }
.demo-rider-dest { color: var(--muted); font-size: 11px; padding-left: 6px; }
.demo-rider-eta {
  color: var(--copper-dark);
  font-size: 11px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}
/* Fallback hint under the Hop In button. Italic Cormorant gives it the
   country-club voice instead of debug text; the inline "Try Cart X" link
   gets a copper underline + hover tone. */
.demo-cart-reason {
  margin-top: 10px;
  font-family: var(--font-ui);
  font-size: 13px;
  font-style: italic;
  color: var(--muted);
  text-align: center;
  line-height: 1.45;
}
.demo-cart-reason .demo-waterfall {
  font-style: normal;
  color: var(--copper-dark);
  text-decoration: none;
  border-bottom: 1px solid rgba(138, 85, 31, 0.4);
  padding-bottom: 1px;
  margin-left: 4px;
}
.demo-cart-reason .demo-waterfall:hover {
  color: var(--copper);
  border-bottom-color: var(--copper);
}

/* ---------- Pulse offer suggestions (Clubhouse) ----------
   A tappable grouped list, not floating cards: one subtle surface, hairline
   rules between rows, a chevron affordance + press feedback so each row clearly
   reads as clickable — without per-item borders / left-bars / shadows. */
/* The pulse panel's lede. The panel no longer carries its own <h3> (the deck
   header prints the title), so this line is the first thing in the panel and
   sets the top rhythm: tight to the header above it, with the breathing room
   below going to the suggestion cards that are the actual decision. */
.pulse-lede {
  font-size: 12px;
  line-height: 1.5;
  margin: 0 0 14px;
}
.pulse-suggest {
  display: block;
  background: #FFFDF8;
  border: 1px solid rgba(184, 115, 51, 0.28);
  border-radius: 10px;
  overflow: hidden;
  box-shadow: 0 1px 3px rgba(120, 70, 20, 0.06);
}
.pulse-chip {
  display: block;
  width: 100%;
  position: relative;
  background: transparent;
  border: none;
  border-bottom: 1px solid rgba(184, 115, 51, 0.16);
  border-radius: 0;
  padding: 13px 34px 14px 15px;
  text-align: left;
  cursor: pointer;
  font-family: inherit;
  transition: background 120ms;
}
.pulse-chip:last-child { border-bottom: none; }
.pulse-chip:hover,
.pulse-chip:active { background: rgba(184, 115, 51, 0.09); }
/* Chosen suggestion — a clear "this is loaded into the composer" state. */
.pulse-chip.selected {
  background: rgba(184, 115, 51, 0.14);
  box-shadow: inset 3px 0 0 var(--copper, #B87333);
}
.pulse-chip.selected::after { opacity: 0.9; }
/* Chevron affordance — the "tap me" cue. */
.pulse-chip::after {
  content: '\203A';
  position: absolute;
  right: 14px;
  top: 50%;
  transform: translateY(-50%);
  font-family: var(--font-ui);
  font-size: 22px;
  line-height: 1;
  color: var(--copper-mid);
  opacity: 0.55;
}
.pulse-chip:hover::after,
.pulse-chip:active::after { opacity: 0.9; }
.pulse-chip-msg {
  font-family: var(--font-ui);
  font-size: 16px; line-height: 1.25;
  color: var(--ink);
  font-weight: 600;
  margin-bottom: 3px;
}
.pulse-chip-why {
  font-family: var(--font-ui);
  font-size: 12px; line-height: 1.45;
  color: #7a6650;
  font-style: italic;
}

/* "What Would John Taffer Do??" — espresso button (ties to the header) + a
   readable light reveal card (dark text on cream, no on-dark tokens). */
.taffer-btn {
  display: block;
  width: 100%;
  margin: 2px 0 10px;
  padding: 12px 14px;
  font-family: var(--font-ui);
  font-size: 13px; font-weight: 700; letter-spacing: 0.05em;
  color: #F1E4C9;
  background: linear-gradient(180deg, #241811 0%, #1a1009 100%);
  border: 1px solid var(--copper-mid);
  border-radius: 8px;
  cursor: pointer;
  transition: filter 120ms, box-shadow 120ms;
}
.taffer-btn:hover { filter: brightness(1.18); box-shadow: 0 2px 10px rgba(120, 70, 20, 0.18); }
.taffer-btn:disabled { opacity: 0.6; cursor: default; }
/* The Taffer "verdict": an editorial block, not a boxed card. A bold copper
   top rule marks it as the result; the type carries the rest. */
.taffer-out {
  background: transparent;
  border: none;
  border-top: 2px solid #8A551F;
  border-radius: 0;
  padding: 12px 2px 4px;
  margin: 4px 0 14px;
}
.taffer-name {
  font-family: var(--font-display);
  font-size: 18px; font-weight: 700; line-height: 1.2;
  color: var(--ink);
}
.taffer-principle {
  font-family: var(--font-ui);
  font-size: 10px; font-weight: 700; letter-spacing: 0.16em; text-transform: uppercase;
  color: #8A551F; margin: 4px 0 8px;
}
.taffer-why {
  font-family: var(--font-ui);
  font-size: 12.5px; line-height: 1.5; color: #4a3a2c;
}
.taffer-hint {
  font-family: var(--font-ui);
  font-size: 11px; font-style: italic; color: #7a6650; margin-top: 8px;
}

#demo-hop-in:disabled {
  opacity: 0.4; cursor: not-allowed;
  background: transparent; border-color: var(--copper-mid); color: var(--copper-soft-on-dark);
}
.demo-cart-reason {
  margin-top: 8px; font-size: 12px; line-height: 1.5;
  font-family: var(--font-ui);
}
.demo-waterfall {
  color: var(--copper-bright);
  text-decoration: underline;
  text-decoration-color: var(--copper-mid);
  text-underline-offset: 2px;
  font-weight: 600;
  white-space: nowrap;
  margin-left: 4px;
}
.demo-waterfall:hover {
  text-decoration-color: var(--copper-bright);
  color: var(--copper-light);
}
.demo-confirm-icon {
  width: 56px; height: 56px;
  border-radius: 50%;
  background: #8A551F;
  color: #F5E8D1;
  font-size: 30px; font-weight: 600;
  display: flex; align-items: center; justify-content: center;
  margin: 0 auto 14px;
}
.demo-confirm-title {
  font-family: var(--font-display);
  font-size: 22px; font-weight: 500;
  color: #3a2614;
}
.demo-confirm-sub {
  font-family: var(--font-ui); font-size: 13px;
  color: #5C3F26; margin-top: 6px;
  line-height: 1.5;
}

/* ---------- Pulse offer banner (Member booking panel) ---------- */
.pulse-offer-banner {
  background: linear-gradient(135deg, rgba(184,115,51,0.18), rgba(184,115,51,0.04));
  border: 1px solid var(--copper-mid);
  border-left: 3px solid var(--copper-bright);
  border-radius: 6px;
  padding: 14px 16px;
  margin: 12px 0 4px;
  position: relative;
}
.pulse-offer-pill {
  display: inline-block;
  font-family: var(--font-ui);
  font-size: 10px; font-weight: 800; letter-spacing: 0.22em;
  color: #1a120a;
  background: var(--copper-bright);
  padding: 2px 8px;
  border-radius: 3px;
  margin-bottom: 8px;
}
.pulse-offer-msg {
  font-family: var(--font-ui);
  font-size: 17px; color: #3A2614; font-weight: 600;
  line-height: 1.35;
}
.pulse-offer-meta {
  font-family: var(--font-ui);
  /* Was --copper-soft-on-dark (a light on-dark token) on this light banner —
     faint. Muted brown reads on the cream-toned card. */
  font-size: 11px; color: #7a6650;
  letter-spacing: 0.04em; margin-top: 6px;
}

/* ---------- Tier-1 Club House venue dots on the demo map ---------- */
.venue-dot-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  /* Make sure clicks land on the dot, not the surrounding leaflet
     marker box. */
  pointer-events: auto;
}
.venue-dot {
  position: relative;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: #8A551F;
  border: 2px solid #F2E8D4;
  box-shadow: 0 0 0 1px rgba(58, 38, 20, 0.4), 0 2px 4px rgba(0, 0, 0, 0.25);
  cursor: pointer;
}
/* Pulse ring expands outward from each dot to draw the eye. Two
   keyframes so the ring fades as it grows; pointer-events: none on
   the pseudo-element so it doesn't block taps on the dot. */
.venue-dot::after {
  content: '';
  position: absolute;
  inset: -4px;
  border-radius: 50%;
  border: 2px solid rgba(138, 85, 31, 0.7);
  pointer-events: none;
  animation: venuePulse 1.8s ease-out infinite;
}
@keyframes venuePulse {
  0%   { transform: scale(0.65); opacity: 0.75; }
  80%  { transform: scale(1.85); opacity: 0; }
  100% { transform: scale(1.85); opacity: 0; }
}

/* ---------- Member 'you are here' blue pulse ---------- */
.member-dot-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: auto;
}
.member-dot {
  position: relative;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: #2E7AC2;
  border: 2px solid #F2E8D4;
  box-shadow: 0 0 0 1px rgba(20, 50, 90, 0.45), 0 2px 4px rgba(0, 0, 0, 0.25);
}
.member-dot::after {
  content: '';
  position: absolute;
  inset: -4px;
  border-radius: 50%;
  border: 2px solid rgba(46, 122, 194, 0.65);
  pointer-events: none;
  animation: memberPulse 1.8s ease-out infinite;
}
@keyframes memberPulse {
  0%   { transform: scale(0.65); opacity: 0.85; }
  80%  { transform: scale(1.85); opacity: 0; }
  100% { transform: scale(1.85); opacity: 0; }
}

/* Pulse ring around demo shield icons for eligible (hop-in-able) carts.
   The .demo-pulse-ring div is rendered inline by demoCartIcon() when the
   cart passes proximity + direction checks. Animates outward like a radar
   blip so the member can see at a glance which carts are available. */
@keyframes demoPulse {
  0%   { transform: scale(0.75); opacity: 0.7; }
  70%  { transform: scale(1.35); opacity: 0.15; }
  100% { transform: scale(1.35); opacity: 0; }
}
.demo-pulse-ring {
  animation: demoPulse 1.6s ease-out infinite;
}

/* ---- Caddie turn-by-turn navigation callout ---- */
.nav-callout {
  margin: 12px 0;
  padding: 14px 16px;
  background: var(--panel, #FBF6EA);
  border: 1px solid rgba(184, 115, 51, 0.25);
  border-left: 3px solid #B87333;
  border-radius: 10px;
}
.nav-callout .nc-label {
  font-family: var(--font-ui);
  font-size: 10px; font-weight: 700;
  letter-spacing: 0.18em; text-transform: uppercase;
  color: var(--muted, #8A7560);
  margin-bottom: 10px;
}
.nav-callout .nc-cta { display: flex; align-items: center; gap: 14px; }
.nav-callout .nc-cta .btn { flex: 0 0 auto; }
.nav-callout .nc-alt { font-size: 12px; white-space: nowrap; }
.nav-callout .nc-steps { margin-top: 10px; }
.nav-callout .nc-step {
  display: flex; align-items: baseline; gap: 10px;
  padding: 6px 0;
  border-top: 1px solid rgba(184, 115, 51, 0.14);
  font-size: 13px; line-height: 1.35;
}
.nav-callout .nc-step:first-child { border-top: 0; }
.nav-callout .nc-glyph {
  flex: 0 0 22px; text-align: center;
  color: #8A551F; font-weight: 700;
}
.nav-callout .nc-text { flex: 1; min-width: 0; }
.nav-callout .nc-dist {
  flex: 0 0 auto; font-size: 11px;
  color: var(--muted, #8A7560); white-space: nowrap;
}
.nav-callout .nc-hint { font-size: 12px; color: var(--muted, #8A7560); }

/* ============================================================
   Swipe deck — TikTok-style horizontal snap navigation
   ------------------------------------------------------------
   Turns a vertical stack of panels into a full-width carousel the
   member swipes left/right through instead of thumb-scrolling down.
   Native CSS scroll-snap does the physical swipe; swipe-deck.js layers
   on the dot pager, live title, and keyboard/arrow support. Reusable on
   any screen — first adopted by My Locker.
   ============================================================ */

/* Pager row: current-panel title on the left, dot indicators centered. */
.swipe-deck-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin: 2px 0 10px;
  min-height: 22px;
}
.swipe-deck-title {
  font-family: var(--font-ui);
  font-size: 11px;
  font-weight: 800;
  letter-spacing: 0.28em;
  text-transform: uppercase;
  color: #14110a;
  text-shadow: 0 1px 0 rgba(255, 235, 200, 0.35);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* The pager row is orientation, not content: it should be legible when looked
   for and quiet when not. Everything below is tuned down from a first pass
   where a saturated copper pill and 7px dots sat on the cream as hard, dark
   punctuation — the eye went to the indicator before the panel it labels. */
.swipe-hint {
  font-size: 10px;
  letter-spacing: 0.08em;
  color: rgba(46, 33, 20, 0.42);
  white-space: nowrap;
  display: inline-flex;
  align-items: center;
  gap: 4px;
}
.swipe-hint::after {
  content: '\2194'; /* left-right arrow */
  font-size: 12px;
  /* Was full copper, which made a 13px glyph the highest-chroma thing in the
     row. The arrow is a hint about a gesture, not an accent. */
  color: rgba(138, 85, 31, 0.45);
}

.swipe-dots {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  flex: 1 1 auto;
  padding: 0 4px;
}
.swipe-dot {
  width: 5px;
  height: 5px;
  padding: 0;
  border: 0;
  border-radius: 999px;
  /* Ink-tinted rather than copper-tinted: an inactive dot is a placeholder,
     so it should read as a lighter step of the page's own text colour, not as
     a desaturated brand accent. */
  background: rgba(46, 33, 20, 0.20);
  cursor: pointer;
  transition: width var(--duration, 0.2s) var(--ease, ease),
              background var(--duration, 0.2s) var(--ease, ease);
}
.swipe-dot:hover { background: rgba(46, 33, 20, 0.36); }
.swipe-dot.active {
  /* Shorter and softer than the old 22px solid-copper pill. Position is
     carried by the width change; it does not also need full saturation. */
  width: 16px;
  background: rgba(138, 85, 31, 0.55);
}

/* The Concierge-desk dot pager is gone with its element — see admin/index.html. */

/* The deck is a fixed viewport: its overflow:hidden shows one page of the
   sliding track at a time. swipe-deck.js lays the panels side-by-side in a
   flex track and translates it with the finger — the TikTok/native-iOS paging
   model: 1:1 drag, rubber-band at the ends, spring settle. touch-action: pan-y
   keeps vertical scrolling inside tall panels native while horizontal drags
   go to the gesture handler. */
.swipe-deck {
  position: relative;
  overflow: hidden;
  touch-action: pan-y;
  /* Contain vertical overscroll so a vertical swipe (or a flick inside a
     short, non-scrolling panel) never chains to the document and rubber-bands
     the whole window — the "drift" on vertical gestures. The deck owns the
     vertical axis; the page underneath stays put. */
  overscroll-behavior: contain;
  padding: 2px 0 10px;
  /* Fill the space under the topbar + back row + pager so each panel reads
     as a full screen. dvh accounts for mobile browser chrome. */
  height: calc(100dvh - 210px);
  min-height: 340px;
}
.swipe-deck:focus { outline: none; }
.swipe-deck:focus-visible { outline: 2px solid var(--copper, #B87333); outline-offset: 3px; border-radius: 12px; }

/* The sliding track: once the deck is a live pager it's a flex row (column
   for axis:'y' — set inline by the JS) translated via transform. No
   transition — all motion is rAF-driven. Before init (and for single-panel
   decks) it stays a plain block so panels flow exactly as un-decked markup. */
.swipe-track {
  width: 100%;
  height: 100%;
}
.swipe-deck.is-swipeable .swipe-track {
  display: flex;
  will-change: transform;
}

.swipe-deck .card {
  margin: 0 !important;
  /* Tall panels (e.g. Concierge desk) scroll vertically inside their own
     slide — the horizontal drag still moves between panels. */
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  /* The card is its own scroll container, so the browser's touch-action
     intersection stops here: it must also say pan-y or it claims horizontal
     drags and pointercancels the deck's gesture. (Axis-correct value is set
     inline by swipe-deck.js; this is the backstop.) */
  touch-action: pan-y;
  /* Keep a panel's own vertical scroll from chaining to the document at its
     top/bottom edge, so the window never drifts behind the deck. */
  overscroll-behavior-y: contain;
}
/* Each slide is one full page of the track (its flex basis is set inline by
   the JS). min-width/min-height let long content shrink instead of blowing
   the track out; the deck's overflow:hidden clips the neighboring pages. */
.swipe-deck.is-swipeable .swipe-track > .card {
  height: 100%;
  min-width: 0;
  min-height: 0;
}

/* Single-panel decks: nothing to swipe, so hide the pager affordances and
   let the lone panel size to its content. */
.swipe-deck:not(.is-swipeable) { height: auto; min-height: 0; touch-action: auto; }
.swipe-deck:not(.is-swipeable) .swipe-track { display: block; height: auto; }
.swipe-deck:not(.is-swipeable) .card { height: auto; overflow-y: visible; }

/* ============================================================
   Swipe shell — native scroll-snap 2-axis deck (shared/swipe-shell.js).
   Horizontal = sections (.shell-panel), vertical = cards (.shell-card) inside
   each section's .shell-feed. The browser is the physics engine; there is NO
   custom drag/fling for touch. Namespaced (shell-*) so it never collides with
   the app's ubiquitous .card. Replaces the liquid swipe-deck / 2D board.
   ============================================================ */
.shell-deck {
  display: flex;
  height: 100dvh;
  width: 100%;
  overflow-x: auto;
  overflow-y: hidden;
  scroll-snap-type: x mandatory;
  overscroll-behavior-x: none;          /* no bounce past the first / last section */
  scrollbar-width: none;
  -ms-overflow-style: none;
}
.shell-deck::-webkit-scrollbar { display: none; }
.shell-deck:focus { outline: none; }

.shell-panel {
  flex: 0 0 100%;              /* one viewport-width per section */
  height: 100dvh;
  scroll-snap-align: start;
  scroll-snap-stop: always;
  position: relative;         /* NOT absolute — keeps scroll-snap honest */
}

/* ---- Bead + peek: the whole affordance, defined once (Patrick, 2026-08-21:
   "globally scale down the swipe cards for a bead of border, get rid of the
   indicators and make it intuitive where you can swipe to").

   The BEAD is a margin of breathing room on all four sides, so a panel reads
   as a card sitting on the page rather than as the page itself.

   The PEEK is the navigation. It narrows each panel by a further sliver so
   the next one is always partly visible at the edge. Content cut off at an
   edge is the oldest "there is more this way" signal there is, and unlike a
   dot pager or a floating chevron it costs no chrome and parks nothing on top
   of a card.

   The axis caveat is load-bearing: scroll-snap-align aligns the BORDER BOX,
   so without scroll-margin the leading bead falls outside the scrollport and
   the gap appears on the trailing side only. These decks snap on X, so the
   bead is handed back on the inline axis. (#member-board snaps on Y inside
   each column and uses scroll-margin-block for exactly the same reason.)

   Every deck's panel selector is listed rather than inherited, because the
   shell lets each portal keep its own markup — .section, .caddie-panel and
   .tab-content are all "a panel" to swipe-shell.js. A new deck adds its
   selector here. ---- */
.shell-deck {
  --deck-bead: 20px;
  --deck-peek: 14px;
}
.shell-deck > .shell-panel,
.shell-deck > .section,
.shell-deck > .caddie-panel,
.shell-deck > .tab-content {
  flex: 0 0 calc(100% - (var(--deck-bead) * 2) - var(--deck-peek));
  height: calc(100% - (var(--deck-bead) * 2));
  margin: var(--deck-bead) 0 var(--deck-bead) var(--deck-bead);
  scroll-margin-inline: var(--deck-bead);
  overflow-y: auto;
  overflow-x: hidden;
  overscroll-behavior-y: contain;
  scroll-snap-align: start;
  scroll-snap-stop: always;
  scrollbar-width: none;
  -ms-overflow-style: none;
  box-sizing: border-box;
}
.shell-deck > .shell-panel::-webkit-scrollbar,
.shell-deck > .section::-webkit-scrollbar,
.shell-deck > .caddie-panel::-webkit-scrollbar,
.shell-deck > .tab-content::-webkit-scrollbar { display: none; }

.shell-feed {
  height: 100dvh;
  overflow-y: auto;
  overflow-x: hidden;
  /* y MANDATORY again: firm card-to-card anchoring. The old nested-mandatory
     deadlock is gone because horizontal paging no longer relies on native
     cross-axis chaining — swipe-shell's touch pan-X driver owns it. */
  scroll-snap-type: y mandatory;
  overscroll-behavior-y: none;          /* fits-one-screen feeds can't drag/bounce */
  scrollbar-width: none;
  -ms-overflow-style: none;
}
.shell-feed::-webkit-scrollbar { display: none; }

/* ---- Edge chevrons (swipe affordance + tap fallback), built by swipe-shell.
   Pinned at the deck's left/right edges; each shows only when a neighbor
   panel exists. Tapping pages there. ---- */
.shell-edge-host { position: relative; }
.shell-edge {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  /* Above Leaflet's panes (400) and its controls (800) so the deck's own
     chevrons stay tappable over a full-screen map — which is why map panels
     used to ship a second, duplicate pair of arrows. Still below every
     overlay in the app (sheets and modals start at 1000). */
  z-index: 900;
  width: 34px; height: 34px;
  display: flex; align-items: center; justify-content: center;
  border: 1px solid rgba(184, 115, 51, 0.35);
  border-radius: 50%;
  background: rgba(255, 250, 240, 0.82);
  color: var(--copper-dark, #8A551F);
  font-size: 22px; font-weight: 700; line-height: 1;
  padding: 0 0 2px;
  box-shadow: 0 2px 8px rgba(120, 85, 50, 0.18);
  cursor: pointer;
  transition: opacity 0.25s ease, transform 0.25s ease;
}
.shell-edge.prev { left: 6px; }
.shell-edge.next { right: 6px; }
.shell-edge:active { transform: translateY(-50%) scale(0.92); }
.shell-edge.hide { opacity: 0; pointer-events: none; }
/* Soft breathing nudge so the affordance reads as "swipe from here". */
@keyframes shell-edge-nudge {
  0%, 100% { transform: translateY(-50%) translateX(0); }
  50%      { transform: translateY(-50%) translateX(2px); }
}
.shell-edge.next:not(.hide) { animation: shell-edge-nudge 2.4s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) { .shell-edge.next { animation: none; } }

/* Vertical variants: ∧ pinned top-center (JS offsets it below any header rows
   the host wraps), ∨ pinned bottom-center. Same chevron art, rotated. */
.shell-edge-glyph { display: block; }
.shell-edge.up, .shell-edge.down {
  left: 50%; right: auto;
  transform: translateX(-50%);
}
.shell-edge.up { top: 8px; }
.shell-edge.down { top: auto; bottom: 10px; }
.shell-edge.up .shell-edge-glyph { transform: rotate(90deg) translateX(-1px); }
.shell-edge.down .shell-edge-glyph { transform: rotate(90deg) translateX(1px); }
.shell-edge.up:active, .shell-edge.down:active { transform: translateX(-50%) scale(0.92); }
@keyframes shell-edge-nudge-v {
  0%, 100% { transform: translateX(-50%) translateY(0); }
  50%      { transform: translateX(-50%) translateY(3px); }
}
.shell-edge.down:not(.hide) { animation: shell-edge-nudge-v 2.4s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) { .shell-edge.down { animation: none; } }

/* The shell's tappable ∨ supersedes the old non-interactive bounce hint. */
.home-feed-hint { display: none !important; }

.shell-card {
  height: 100dvh;
  width: 100%;
  scroll-snap-align: start;
  scroll-snap-stop: always;
  display: flex;
  flex-direction: column;
  justify-content: center;
}
/* Inner scrollers the gesture arbiter defers to: a card whose own body scrolls. */
.shell-card .card-scroll { overflow-y: auto; overscroll-behavior-y: contain; -webkit-overflow-scrolling: touch; }

/* The ONLY touch-specific rule, gated to coarse pointers so mouse/trackpad/
   keyboard are untouched. Never `pan-y` here — that silently kills horizontal
   swipe-between-sections on real phones. */
@media (pointer: coarse), (any-pointer: coarse) {
  .shell-deck { touch-action: pan-x pan-y; }
  .shell-feed { touch-action: pan-x pan-y; }
}

/* Dot pager (reuses the copper dot look). */
.shell-dots { display: flex; align-items: center; justify-content: center; gap: 7px; }
.shell-dot {
  width: 7px; height: 7px; padding: 0; border: 0; border-radius: 999px;
  background: rgba(138, 85, 31, 0.28); cursor: pointer;
  transition: width var(--duration, 0.2s) var(--ease, ease), background var(--duration, 0.2s) var(--ease, ease);
}
.shell-dot.active { width: 22px; background: var(--copper, #B87333); }

@media (prefers-reduced-motion: reduce) {
  .shell-deck, .shell-feed { scroll-behavior: auto; }
}

/* Clubhouse host deck → native scroll-snap (Phase 3). A 1-D horizontal deck of
   full-viewport .section panels that scroll their own content vertically. Keeps
   the .section markup (so every section's visual CSS still applies); only the
   layout is native. Sits under the topbar + venue hero + pager head. */
#ch-deck.shell-deck { height: calc(100dvh - 140px); min-height: 400px; }
/* Bead, peek and panel geometry: shared, see .shell-deck above. */

/* Parent portal deck — same native 1-D section deck as the clubhouse. */
#parent-deck.shell-deck { height: calc(100dvh - 140px); min-height: 400px; }

/* Concierge desk deck — the 12 tabs become native scroll-snap panels (Phase 3b).
   Panels keep their .tab-content markup; the tab-bar overlay + dots stay pinned
   over .main (they live outside #admin-deck so they don't scroll with it). The
   deck fills the .main grid cell rather than 100dvh (the topbar sits above). */
#admin-deck.shell-deck { height: 100%; width: 100%; }
/* Fleet-map panel: leaflet fills it; kill the panel padding and give it its own
   positioning context so #map's inset:0 sizes to the panel, not to .main. */
#admin-deck > #tab-map { position: relative; padding: 0; overflow: hidden; }
/* Concierge queue card — per-rider list with the cart-speed + remaining-route
   ETA each waiting Member sees. */
.queue-riders { display: flex; flex-direction: column; gap: 4px; }
.queue-rider {
  display: flex; align-items: baseline; gap: 8px;
  padding: 6px 9px; border-radius: 8px;
  background: rgba(255, 255, 255, 0.03);
  border: 1px solid var(--border);
  font-size: 12px;
}
.queue-rider .qr-pos { flex: 0 0 auto; font-weight: 700; color: var(--accent, #4ea87b); }
.queue-rider .qr-name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.queue-rider .qr-eta { flex: 0 0 auto; font-weight: 700; font-variant-numeric: tabular-nums; }

/* Mobile-first desk: below the dots breakpoint the 400px sidebar can't fit, so
   drop it and let the tab deck own the full width. Desktop (>=900px) keeps the
   sidebar dashboard + full tab bar; phones swipe the 12-tab deck with dots. */
@media (max-width: 899px) {
  /* Single full-width column, and pin the row to the layout height so .main
     (and the deck inside it) get a DEFINITE height. On desktop the row was
     sized by the sidebar's content; with the sidebar gone the deck would
     otherwise grow to its own content and never bound the panels/map. */
  .layout { grid-template-columns: 1fr; grid-template-rows: 100%; min-height: 0; }
  .layout .sidebar { display: none; }
  .layout .main { min-height: 0; }
}

/* ============================================================
   2D clustered swipe BOARD (Option B navigation) — swipe-board.js
   ------------------------------------------------------------
   Columns are "places" laid left→right; each column stacks its
   cells top→bottom. Swipe horizontally between places, vertically
   through a place's detail. Both columns and cells are stacked
   absolutely; only the active cell (and any incoming one mid-wave)
   paints. touch-action: pan-y lets a tall cell scroll natively —
   the controller only navigates vertically from the cell's edge.
   ============================================================ */
.swipe-board {
  position: relative;
  overflow: hidden;
  /* The board owns BOTH axes so a vertical swipe registers instantly instead
     of being handed to the browser as a scroll. Cells opt into their own
     native scrolling via an inner .board-scroll / .fill region (below). */
  touch-action: none;
  overscroll-behavior: contain;
  height: calc(100dvh - 128px);
  min-height: 420px;
}
.swipe-board:focus { outline: none; }
.swipe-board:focus-visible {
  outline: 2px solid var(--copper, #B87333); outline-offset: 3px; border-radius: 12px;
}
.swipe-board.is-swipeable .board-col { position: absolute; inset: 0; }
.swipe-board.is-swipeable .board-cell {
  position: absolute;
  inset: 0;
  visibility: hidden;
  /* The cell itself does not scroll — vertical drags navigate. A tall panel
     puts its scrollable content in an inner .board-scroll / .fill region. */
  overflow: hidden;
}
/* Opt-in native scroll region inside a tall cell (long lists, Concierge desk).
   pan-y lets it scroll; the controller hands the vertical swipe back to
   navigation once the region reaches its top/bottom edge. */
.swipe-board .board-cell .board-scroll,
.swipe-board .board-cell [data-board-scroll],
.swipe-board .board-cell .fill {
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
  touch-action: pan-y;
}
.swipe-board.is-swipeable .board-cell.board-active { visibility: visible; }
.swipe-board.is-swipeable .board-cell.board-incoming {
  visibility: visible;
  z-index: 3;
  will-change: clip-path;
}
/* Single-cell board: nothing to swipe — let it size to content. */
.swipe-board:not(.is-swipeable) { height: auto; min-height: 0; touch-action: auto; }
.swipe-board:not(.is-swipeable) .board-col { position: static; }
.swipe-board:not(.is-swipeable) .board-cell {
  position: static; visibility: visible; overflow: visible;
}
/* Dual pagers: a horizontal rail of place-dots and a vertical rail of row-dots
   (shown only when the current place has >1 row). Reuses .swipe-dot styling. */
.board-row-dots {
  position: absolute; right: 10px; top: 50%; transform: translateY(-50%);
  display: flex; flex-direction: column; gap: 8px; z-index: 6; pointer-events: auto;
}

/* Board-hosted locker/club panels: same full-screen panel treatment the deck
   gave them. Inline margin-top (the old stacked layout) must not offset an
   absolutely-stacked cell. */
.swipe-board .card.deck-panel {
  margin: 0 !important;
  display: flex;
  flex-direction: column;
  gap: 18px;
  padding: 28px 22px 24px;
  box-sizing: border-box;
}
.swipe-board .card.deck-panel.center { justify-content: safe center; }

/* ---- Member board: NATIVE scroll-snap (Phase 2 pt.A) ----
   The board keeps its .swipe-board/.board-col/.board-cell markup (so every
   cell's visual CSS above still applies), but instead of swipe-board.js's
   transform drag it's now a native 2-axis scroll-snap deck driven by
   swipe-shell.js. ID-scoped rules override the .swipe-board defaults. Sits
   under topbar + back row + pager head. */
#member-board {
  display: flex !important;
  /* The MMR ridge is a position:fixed background (body::after) that reserves NO
     layout space, so the deck can run nearly the full height and simply FLOAT
     over the ridge — the booking card sits above the mountains instead of being
     boxed in above them. Reserve exactly the topbar + pager head above it.
     `--member-board-top` is written by syncBoardHeight() in app.js from the
     board's measured offset; the 96px here is only the pre-measurement
     fallback, and it is deliberately the OLD number so first paint is never
     taller than what the measurement will settle on. A fixed 96 was 52px short
     of the real 148px, which hung the bottom of every panel below the window
     with no scroller able to reach it. */
  height: calc(100dvh - var(--member-board-top, 96px));
  min-height: 420px;
  overflow-x: auto;
  overflow-y: hidden;
  scroll-snap-type: x mandatory;
  overscroll-behavior-x: none;          /* no bounce past Home / Club ends */
  scrollbar-width: none;
  -ms-overflow-style: none;
}
#member-board::-webkit-scrollbar { display: none; }
#member-board > .board-col {
  position: static;
  /* Narrower than the viewport by one peek, so the neighbouring place shows
     its edge. Snap alignment is unaffected — each column still starts at the
     scrollport start, the peek just lives past its right edge. */
  flex: 0 0 calc(100% - var(--board-peek));
  height: 100%;
  overflow-y: auto;
  overflow-x: hidden;
  /* y MANDATORY again: firm section-to-section anchoring. The old
     nested-mandatory deadlock no longer applies — horizontal paging is owned
     by swipe-shell's touch pan-X driver, not native cross-axis chaining. */
  scroll-snap-type: y mandatory;
  overscroll-behavior-y: none;          /* fits-one-screen panels can't drag/bounce */
  scrollbar-width: none;
  -ms-overflow-style: none;
  /* Horizontal snap TARGETS. Without this the board's `scroll-snap-type: x
     mandatory` has nothing to snap to on the x-axis (the .board-cell align
     below is the y-axis target inside each column), so a sideways swipe
     free-scrolls and rests mid-panel instead of paging Home↔Locker↔Club.
     snap-stop: always keeps one firm swipe = one panel. */
  scroll-snap-align: start;
  scroll-snap-stop: always;
}
#member-board > .board-col::-webkit-scrollbar { display: none; }
/* Each panel IS the snap cell, so at height:100%/margin:0 every card ran hard
   into all four edges of the deck — no frame, and on a phone the card read as
   the page rather than as a card sitting on one.
   The bead is that frame. The cell shrinks by twice the bead and takes it back
   as margin, so the snap pitch is still exactly one screen and nothing drifts.
   scroll-margin-block is the part that is easy to miss: scroll-snap-align
   aligns the BORDER box, not the margin box, so without it the top bead would
   sit above the scrollport and the gap would only ever appear at the bottom. */
/* The deck breaks out of .member-stack's side gutter, the same way .map-stage
   does, because the bead IS that gutter for these cards.
   Without this the two stack: 20px of stack padding plus 20px of bead put the
   membership card 40px in from each edge and left the Home greeting's script
   subline — a size measured and re-fitted three times against the column —
   wrapping to three lines. Reclaiming the gutter keeps the net inset at the
   20px Patrick asked for on 2026-08-20 and pays for the peek out of what was
   double-counted. */
#member-deck-wrap {
  margin-inline: calc(-1 * var(--stack-gutter, 0px));
  width: calc(100% + 2 * var(--stack-gutter, 0px));
  /* Declared on the WRAP, not the board, so the deck head can line its title
     up with the card edge using the same number the cards are inset by. */
  /* 12px read as a hairline rather than a frame on a phone — the card still
     looked like the page. 20px is enough that the cream ground reads as a mat
     the card is sitting on. It costs 40px of card height, which the panels
     absorb: they were already inner-scrolling when their content ran long. */
  --board-bead: 20px;
  /* The sliver of the NEXT card left showing past the bead, on both axes.
     This is what replaced the floating ‹ › ∨ chevrons: content cut off at an
     edge is the oldest "there is more this way" signal there is, and unlike a
     button it never sits on top of the membership card.
     Kept small on purpose — enough to read as a card edge, not so much that it
     looks like a layout mistake. */
  --board-peek: 14px;
}

/* The wrap pulls itself out of the stack gutter so the DECK can run edge to
   edge. The title above it must not follow it there — with the dot pager and
   the "Swipe" label gone it is the only thing in that row, and it was landing
   flush against the left edge of the phone while the card it names sat 20px
   in. Put it back on the card's own line: gutter + bead. */
#member-deck-wrap > .swipe-deck-head {
  padding-inline: calc(var(--stack-gutter, 0px) + var(--board-bead));
}
#member-board .board-cell {
  position: static !important;
  visibility: visible !important;
  height: calc(100% - (var(--board-bead) * 2) - var(--board-peek));
  width: calc(100% - (var(--board-bead) * 2));
  margin: var(--board-bead) !important;
  scroll-margin-block: var(--board-bead);
  scroll-snap-align: start;
  scroll-snap-stop: always;
}
/* The one touch rule — pan-x pan-y so horizontal paging survives on phones. */
@media (pointer: coarse), (any-pointer: coarse) {
  #member-board, #member-board > .board-col { touch-action: pan-x pan-y; }
}

/* Fixed, non-scrolling Standings cell (tiktokify): the leaderboard is capped
   to 7 rows in JS and compacted here so it ALWAYS fits the cell — a vertical
   swipe on it navigates instead of smooshing an inner scroller around. */
.swipe-board #standings-panel { padding: 18px 16px 14px; gap: 10px; overflow: hidden; }
.swipe-board #standings-panel .masters-head { padding: 12px 14px 8px; }
.swipe-board #standings-panel .masters-head::before { margin-bottom: 5px; font-size: 10px; }
.swipe-board #standings-panel .masters-title { font-size: 20px; }
.swipe-board #standings-panel .masters-crest { height: 26px; }
.swipe-board #standings-panel .masters-cols { padding: 8px 12px 5px; }
.swipe-board #standings-panel .masters-row { padding: 7px 4px; }
.swipe-board #standings-panel .masters-row .mc-col-name { font-size: 14.5px; }
.swipe-board #standings-panel .masters-row .mc-col-par {
  min-width: 46px; padding: 4px 6px; font-size: 15px;
}
.swipe-board #standings-panel .nb-emblem { width: 20px; height: 20px; }
.swipe-board #standings-panel .masters-tagline { font-size: 11px; margin: 8px 4px 0; }
/* The "..." row standing in for the ranks between the top and your line. */
.masters-gap {
  text-align: center;
  color: rgba(246, 241, 228, 0.5);
  font: 700 12px/1 var(--font-ui);
  letter-spacing: 0.3em;
  padding: 3px 0 1px;
}

/* ============================================================
   Locker swipe panels — full-screen content redesign
   ------------------------------------------------------------
   Each swipe panel is now a full viewport, so its content is laid
   out to fill and breathe: a subdued back control, a larger panel
   header, and per-panel treatments (a dark-ink membership card, a
   copper card-on-file, prominent pricing, elegant empty states).
   ============================================================ */

/* Subdued, on-theme back control — quiet copper text, not a button slab. */
.locker-back-row { margin: 2px 0 6px; }
.deck-back {
  appearance: none;
  border: 0;
  background: transparent;
  color: var(--copper-deep, #8A551F);
  font: 700 11px/1 var(--font-ui);
  letter-spacing: 0.2em;
  text-transform: uppercase;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 8px 6px 8px 2px;
  cursor: pointer;
  opacity: 0.72;
  transition: opacity var(--duration, 0.2s) var(--ease, ease),
              color var(--duration, 0.2s) var(--ease, ease);
}
.deck-back:hover { opacity: 1; color: var(--copper-mid, #B87333); }
.deck-back-chev { font-size: 16px; line-height: 0; margin-top: -1px; }

/* Panel scaffold: fill the height, comfortable rhythm. */
.swipe-deck .card.deck-panel {
  display: flex;
  flex-direction: column;
  gap: 18px;
  padding: 28px 22px 24px;
  box-sizing: border-box;
}
.swipe-deck .card.deck-panel.center { justify-content: safe center; }

/* Larger panel header than the tiny .label eyebrow, with an optional subline. */
.panel-head { display: flex; flex-direction: column; gap: 5px; }
.panel-eyebrow {
  font: 800 12px/1.15 var(--font-ui);
  letter-spacing: 0.26em;
  text-transform: uppercase;
  color: var(--copper-deep, #8A551F);
}
.panel-sub { font-size: 13px; line-height: 1.4; color: var(--muted, #6E5A40); }

/* ---- Deck-panel action buttons ----
   In a full-screen swipe panel the flat hollow secondaries ("Add family
   member", "Send test approval text", "Invite", "Revoke", "Deny") read as
   weak, empty boxes and the wide primary tracking looks awkward at this size.
   Give deck secondaries a solid, refined soft-copper *fill* (not hollow) so
   they read as real, tappable controls while staying clearly below the
   metallic primary CTAs; give the minis tighter tracking, a comfortable 40px
   tap height, and consistent sizing so action rows line up cleanly. */
.deck-panel .btn.secondary {
  background: rgba(184, 115, 51, 0.13);
  border-color: rgba(184, 115, 51, 0.3);
  color: #7A4A18;
  font-weight: 700;
  box-shadow: inset 0 1px 0 rgba(255, 245, 232, 0.5);
}
.deck-panel .btn.secondary:hover {
  background: rgba(184, 115, 51, 0.22);
  border-color: rgba(184, 115, 51, 0.55);
  color: #653B12;
  filter: none;
}
.deck-panel .btn.secondary:active { background: rgba(184, 115, 51, 0.3); }
.deck-panel .btn.mini {
  padding: 9px 16px;
  min-height: 40px;
  font-size: 12px;
  letter-spacing: 0.01em;
  border-radius: 9px;
  line-height: 1.05;
}
/* "Send test approval text" sits on its own — let it size to content as a
   solid chip rather than reading as a big hollow slab. */
#concierge-test-sms-row .btn.mini { flex: 0 0 auto; }
/* The invite input row: keep the button the same height as the inputs it
   sits beside so the row reads as one aligned control cluster. */
#concierge-plan-section .row-inline { align-items: stretch; }
#concierge-plan-section .row-inline .btn.mini { min-height: 0; }

/* Household: the "Add family member" action reads better as a full-width
   control under the header than floating top-right against the multi-line
   description. */
#locker-household-add { width: 100%; margin-top: 4px; }

/* Concierge desk: give the stacked review sections a touch more vertical
   breathing room so the screen doesn't crowd at the top. */
#concierge-pending { display: flex; flex-direction: column; gap: 20px; }
#concierge-pending > div { margin-top: 0; }

/* The desk's scroll region. The panel head stays pinned; everything below it
   scrolls, because the four review sections grow with the queue and the cell
   is capped at the board height. `.fill` supplies flex:1/min-height:0/
   overflow-y:auto; this adds back the column gap the wrapper swallowed (it was
   the panel's own flex gap) and a bottom pad so the last row clears the edge
   rather than sitting flush against it. */
#concierge-desk-card { min-height: 0; }
#concierge-desk-card > .fill.concierge-scroll {
  display: flex;
  flex-direction: column;
  gap: 18px;
  /* A hairline under the pinned head. The head does not move and the queue
     does, so without it a half-scrolled Approve/Deny row appears to grow out
     of the sub-line; the rule is also the only cue that this region scrolls
     at all. */
  border-top: 1px solid var(--border);
  padding-top: 16px;
  padding-bottom: 28px;
  /* The board's own y-paging owns the gesture once this reaches an edge (the
     gesture arbiter decides), so it must not swallow the overscroll itself. */
  overscroll-behavior-y: auto;
}
#concierge-desk-card > .fill.concierge-scroll > #concierge-test-sms-row { margin-top: 0 !important; }
#concierge-desk-card > .fill.concierge-scroll > #concierge-pending { margin-top: 0 !important; }

/* Lists inside a panel fill the remaining height and scroll internally. */
.deck-panel .fill { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
.deck-panel .empty-state {
  margin: auto 0;
  text-align: center;
  color: var(--muted, #6E5A40);
  font-size: 14px;
  padding: 28px 12px;
}

/* ---- Membership card: the identity object, inverted to deep ink ----
   2026-07-22 design lift: the card used to stretch its five pieces of
   content (brand row, name, handicap chip, info grid, ride pass) across the
   full swipe-deck panel height with `justify-content: space-between` --
   since the deck forces every panel to `calc(100dvh - 128px)` regardless of
   content, this read as large, seemingly-accidental gaps between every
   section instead of "purposeful whitespace". Content now clusters at the
   top with a tight, deliberate rhythm (flex-start + gap); the one real
   variable -- extra vertical space on a tall phone -- is absorbed by a
   single `margin-top: auto` on the new .mc-hero-foot strip at the very
   bottom, so there's exactly one, intentional void instead of five small
   arbitrary ones. Two subtle layered background touches (a soft top-left
   highlight like light catching foil, and a large low-opacity crest
   watermark bleeding off the bottom-right corner) give that lower field a
   reason to exist instead of reading as empty. */
.card.mc-hero {
  background: linear-gradient(158deg, #3C2917 0%, #2E2114 55%, #221810 100%),
              radial-gradient(120% 90% at 8% 0%, rgba(251, 246, 234, 0.10) 0%, rgba(251, 246, 234, 0) 42%) !important;
  border: 1px solid rgba(212, 152, 107, 0.35) !important;
  border-left: 3px solid var(--copper-mid, #B87333) !important;
  color: #FBF6EA !important;
  box-shadow: 0 18px 44px rgba(40, 28, 15, 0.4), inset 0 1px 0 rgba(255, 235, 214, 0.08) !important;
  position: relative;
  z-index: 0;
  overflow: hidden;
  justify-content: flex-start;
  gap: 20px;
  min-height: 288px;
}
/* Crest watermark, bled off the bottom-right corner -- a security-print
   touch (echoes a passport/premium-card backing pattern), not a UI element:
   z-index -1 keeps it under the text/QR but above the card's own gradient. */
.card.mc-hero::after {
  content: '';
  position: absolute;
  right: -30px;
  bottom: -30px;
  width: 260px;
  height: 260px;
  /* Reversed crest — the card is deep ink, so the white-line mark is the
     native fit. Replaces the bronze crest plus the grayscale/brightness
     filter that was faking this same reversal out of it. */
  background: url('/shared/shield-white.png') center / contain no-repeat;
  opacity: 0.12;
  pointer-events: none;
  z-index: -1;
}
.mc-hero-brand { display: flex; align-items: center; gap: 12px; }
.mc-hero-crest { height: 38px; width: auto; filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.4)); }
.mc-hero-club {
  font: 700 10px/1.2 var(--font-display);
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: #D4986B;
}
.mc-hero-name {
  font-family: var(--font-display);
  font-size: clamp(26px, 8vw, 34px);
  line-height: 1.02;
  font-weight: 600;
  color: #FBF6EA;
  margin: 2px 0;
}
.mc-hero-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px 20px; }
.mc-cell.wide { grid-column: 1 / -1; }
.mc-k {
  display: block;
  font: 600 9px/1 var(--font-ui);
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: rgba(251, 246, 234, 0.5);
  margin-bottom: 6px;
}
.mc-v {
  font-family: var(--font-ui);
  font-size: 14px;
  letter-spacing: 0.05em;
  color: #E4A87E;
}
/* Member No. reads as the card's "embossed" primary identifier -- a touch
   larger and more tracked-out than the secondary Tier/Since values. */
.mc-cell.wide .mc-v { font-size: 16px; letter-spacing: 0.12em; }
/* Footer strip: absorbs whatever vertical space is left over on a tall
   phone instead of it splitting awkwardly between every section above. */
.mc-hero-foot {
  margin-top: auto;
  padding-top: 4px;
  /* line-height was 1. Combined with 0.24em tracking on a long string it wrapped
     to two lines with zero leading, which reads as broken alignment rather than
     as two lines. Shortening the text fixes today's card; nowrap + leading make
     it structural, so no future width or wording can put it back. */
  font: 600 9px/1.4 var(--font-ui);
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: rgba(251, 246, 234, 0.32);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ---- MCC Handicap: the chip on the membership card ---- */
.mc-handicap {
  /* Label stacked ABOVE the value — collision-proof (a horizontal label + big
     value could overlap on some devices' font metrics). */
  display: inline-flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 3px;
  align-self: flex-start;
  /* 2026-07-22: Patrick — brand row + name should stay pinned at the top,
     but the rest read as "squished" once justify-content: space-between was
     removed (see .card.mc-hero comment). One flexible gap here, shared with
     .mc-hero-foot's own margin-top: auto below, splits the card's leftover
     vertical space into two deliberate pockets instead of either one big
     void or five arbitrary ones -- the identity/account-details/pass group
     stays visually clustered while still using more of a tall phone's
     height. */
  margin-top: auto;
  padding: 8px 14px;
  border-radius: 12px;
  border: 1px solid rgba(212, 152, 107, 0.4);
  background: rgba(212, 152, 107, 0.12);
  color: inherit;
  cursor: pointer;
  transition: filter var(--dur-1, .13s) var(--ease-spring, ease),
              transform var(--dur-1, .13s) var(--ease-spring, ease);
}
.mc-handicap:hover { filter: brightness(1.08); }
.mc-handicap:active { transform: scale(0.97); }
/* Standings parked for now (LOCKER_SIMPLE_MODE in app.js) -- the chip still
   shows the handicap value, just isn't a tap-through to a hidden panel. */
.mc-handicap:disabled { cursor: default; pointer-events: none; }
.mc-handicap:disabled:hover { filter: none; }
.mc-handicap-k {
  font: 600 9px/1 var(--font-ui);
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: rgba(251, 246, 234, 0.55);
}
.mc-handicap-v {
  font-family: var(--font-ui);
  font-size: 24px;
  line-height: 1;
  font-weight: 700;
  letter-spacing: 0.02em;
  font-variant-numeric: tabular-nums;
  color: #E4A87E;
}
/* "To par" coloring, broadcast-style: red under par (the flex), gold at even.
   The gold is the club's warm F2C879, not Masters yellow. */
.par-under { color: #FF6D55 !important; }
.par-even  { color: #F2C879 !important; }
.par-over  { color: #C9B79A; }

/* ---- MCC Handicap: the club standings board ---- */
/* A broadcast-style board in the club's own copper/espresso (deliberately NOT
   a Masters green-and-yellow clone): MCC shield crest in the header, a
   neighborhood emblem per member where a broadcast puts the country flag, and
   the stylized McDowell range as the backdrop behind the board.
   The panel scrolls vertically inside its slide (the shared .swipe-deck .card
   rule sets overflow-y:auto); this keeps the last rows off the deck's bottom
   edge and stops a scroll from chaining into the page. */
.masters-panel {
  gap: 12px;
  padding-bottom: 28px;
  overscroll-behavior-y: contain;
  position: relative;
}
/* Deep Augusta-green board with faint vertical "slat" lines and a gold inset
   rule — the manual Masters scoreboard. */
.masters-board {
  position: relative;
  border-radius: 14px;
  overflow: hidden;
  padding: 0 0 4px;
  color: #F6F1E4;
  background:
    repeating-linear-gradient(90deg, rgba(0, 0, 0, 0.10) 0 1px, transparent 1px 46px),
    linear-gradient(178deg, #065f39 0%, #054e2f 60%, #053f26 100%);
  background-color: #065f39;
  border: 1px solid #063a24;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08),
              inset 0 0 0 3px rgba(230, 194, 90, 0.10),
              0 14px 34px rgba(6, 40, 26, 0.4);
}
/* Header: club shield + title on a darkened band with a gold double rule. */
.masters-head {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  text-align: center;
  padding: 16px 16px 12px;
  background: linear-gradient(180deg, rgba(0, 0, 0, 0.20), transparent);
  border-bottom: 2px solid rgba(230, 194, 90, 0.35);
}
.masters-head::after {
  content: "";
  position: absolute;
  left: 22px; right: 22px; bottom: -5px; height: 1px;
  background: rgba(230, 194, 90, 0.35);
}
/* The shield is dark artwork; tint it warm gold to sit on the green board. */
.masters-crest {
  height: 34px;
  width: auto;
  filter: brightness(0) saturate(100%) invert(78%) sepia(38%) saturate(560%) hue-rotate(357deg) brightness(96%);
}
.masters-head-text { display: flex; flex-direction: column; text-align: left; }
.masters-title {
  display: block;
  font: 700 22px/1 var(--font-display);
  letter-spacing: 0.10em;
  text-transform: uppercase;
  color: #EBC65C;
  text-shadow: 0 1px 0 rgba(0, 0, 0, 0.35);
}
.masters-sub {
  display: block;
  margin-top: 4px;
  font: 600 9.5px/1 var(--font-ui);
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: rgba(246, 241, 228, 0.6);
}
.masters-cols,
.masters-row {
  display: grid;
  grid-template-columns: 38px 30px 1fr 72px;
  align-items: center;
  gap: 8px;
}
.masters-cols {
  padding: 12px 14px 8px;
  font: 700 9px/1 var(--font-ui);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: rgba(246, 241, 228, 0.5);
}
.masters-cols .mc-col-pos,
.masters-cols .mc-col-par { text-align: center; }
.masters-rows { padding: 0 8px 6px; }
.masters-row {
  padding: 10px 6px;
  border-top: 1px solid rgba(230, 194, 90, 0.14);
}
.masters-rows .masters-row:first-child { border-top: 0; }
.masters-row .mc-col-pos {
  font: 700 15px/1 var(--font-ui);
  font-variant-numeric: tabular-nums;
  color: #EBC65C;
  text-align: center;
}
/* Neighborhood emblem (per-member community mark). */
.mc-col-crest { display: flex; align-items: center; justify-content: center; }
.nb-emblem { width: 24px; height: 24px; display: block; }
.masters-row .mc-col-name {
  display: flex;
  align-items: baseline;
  gap: 7px;
  min-width: 0;
  font: 600 15px/1.1 var(--font-ui);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: #F6F1E4;
}
/* The name ellipsizes; the "(you)" marker is a non-shrinking sibling so it is
   never clipped, even on a long name. */
.masters-row .mc-col-name .mc-name-txt {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.masters-row .mc-col-name em {
  flex: none;
  font-style: normal;
  color: rgba(235, 198, 92, 0.9);
  font-size: 0.72em;
  letter-spacing: 0.12em;
}
/* The signature Masters number plate — ivory tile, red under-par / green over. */
.masters-row .mc-col-par {
  justify-self: center;
  min-width: 54px;
  text-align: center;
  padding: 6px 7px;
  border-radius: 4px;
  background: linear-gradient(180deg, #FBF7EC, #EFE7D2);
  box-shadow: inset 0 -2px 0 rgba(0, 0, 0, 0.10),
              inset 0 1px 0 rgba(255, 255, 255, 0.7),
              0 1px 2px rgba(0, 0, 0, 0.3);
  font: 800 16px/1 var(--font-ui);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.02em;
  color: #2E2114;
}
/* Plate digit color by par — scoped + !important to beat the global .par-* text
   colors (which are tuned for the cream card, not an ivory plate). */
.masters-row .mc-col-par.par-under { color: #C8102E !important; }
.masters-row .mc-col-par.par-over  { color: #1E7A44 !important; }
.masters-row .mc-col-par.par-even  { color: #2E2114 !important; }
.masters-row.is-me {
  background: linear-gradient(90deg, rgba(230, 194, 90, 0.16), rgba(230, 194, 90, 0.03));
  border-radius: 8px;
  box-shadow: inset 3px 0 0 #EBC65C;
}
.masters-empty {
  padding: 22px 4px;
  text-align: center;
  color: rgba(246, 241, 228, 0.6);
  font: 500 13px/1.3 var(--font-ui);
}
.masters-tagline {
  margin: 12px 6px 0;
  font: italic 500 12px/1.5 var(--font-display);
  color: var(--muted, #7a6650);
  text-align: center;
}

/* Scoring calculation — "how your handicap is scored" (count × weight = pts). */
.masters-scoring { display: block; }
.score-head {
  font: 700 10px/1 var(--font-ui);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--muted, #7a6650);
  margin-bottom: 8px;
}
.score-table {
  border: 1px solid var(--border-2, rgba(184, 115, 51, 0.22));
  border-radius: 10px;
  overflow: hidden;
}
.score-row {
  display: grid;
  grid-template-columns: 1fr auto 64px;
  align-items: center;
  gap: 10px;
  padding: 9px 12px;
  border-top: 1px solid var(--border-2, rgba(184, 115, 51, 0.16));
}
.score-row:first-child { border-top: 0; }
.score-row.is-zero { opacity: 0.5; }
.score-lbl { font: 500 13px/1.2 var(--font-ui); color: var(--ink, #2E2114); }
.score-calc {
  font: 500 12px/1 var(--font-ui);
  color: var(--muted, #7a6650);
  font-variant-numeric: tabular-nums;
}
.score-sub {
  font: 700 14px/1 var(--font-ui);
  text-align: right;
  white-space: nowrap;
  color: var(--ink, #2E2114);
  font-variant-numeric: tabular-nums;
}
.score-row.score-total {
  background: var(--accent-soft, rgba(184, 115, 51, 0.1));
  border-top-width: 2px;
}
.score-row.score-result .score-lbl { font-weight: 700; }
.score-row.score-result .score-sub { font-size: 18px; }
.score-note {
  margin: 10px 2px 0;
  font: 500 11px/1.5 var(--font-ui);
  color: var(--muted, #7a6650);
}
.masters-optout {
  display: flex;
  align-items: center;
  gap: 8px;
  font: 500 12px/1 var(--font-ui);
  color: var(--muted, #7a6650);
  cursor: pointer;
}
.masters-optout input { width: 16px; height: 16px; accent-color: var(--accent, #B87333); }

/* ---- Payment: a card-on-file visual ---- */
.pay-card {
  position: relative;
  border-radius: 14px;
  padding: 20px;
  background: linear-gradient(135deg, #B87333 0%, #8A551F 62%, #6E421A 100%);
  color: #FBF6EA;
  box-shadow: 0 14px 32px rgba(138, 85, 31, 0.32);
  display: flex;
  flex-direction: column;
  gap: 22px;
  min-height: 152px;
  overflow: hidden;
}
.pay-card::after {
  content: '';
  position: absolute;
  right: -44px;
  top: -44px;
  width: 150px;
  height: 150px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.08);
}
.pay-card-chip {
  width: 40px;
  height: 30px;
  border-radius: 6px;
  background: linear-gradient(135deg, #F2D9A6, #CBA46A);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.3);
}
.pay-card-num {
  font-family: var(--font-ui);
  font-size: 17px;
  letter-spacing: 0.1em;
  color: #FBF6EA;
  position: relative;
}
.pay-card-num strong { font-weight: 700; letter-spacing: 0.03em; margin-right: 8px; }
.pay-card-num .muted { color: rgba(251, 246, 234, 0.7); }
.pay-card-foot {
  display: flex;
  justify-content: space-between;
  align-items: flex-end;
  font: 600 10px/1 var(--font-ui);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: rgba(251, 246, 234, 0.75);
  position: relative;
}
.pay-card-brandmark {
  font-family: var(--font-display);
  font-size: 16px;
  letter-spacing: 0.14em;
  color: #FBF6EA;
  text-transform: none;
}

/* ---- Private Service: make the price read as the hero figure ---- */
#premium-panel .ch-confirm-row.total { border-top: 1px solid rgba(184, 115, 51, 0.2); padding-top: 12px; }
#premium-panel .ch-confirm-row.total strong {
  font-family: var(--font-ui);
  font-size: 24px;
  color: var(--copper-deep, #8A551F);
}

/* ---- Gate access: give the code field presence ---- */
#locker-gate-card .gate-text { font-size: 14px; line-height: 1.45; color: var(--ink, #2E2114); }
#locker-gate-card #gate-pin {
  font-family: var(--font-ui);
  letter-spacing: 0.16em;
  font-size: 16px;
  text-align: center;
}

/* ============================================================
   Caddie portal swipe deck — the Caddie swipes left/right through
   their operational cards (On duty / Current Member / Cart damage)
   instead of scrolling the sidebar. Reuses the shared .swipe-deck
   engine; this only sizes it to the sidebar + stacks the map below
   on phones so the deck reads full-screen.
   ============================================================ */
/* Only reserve the full deck height once it's live (>1 panel). Before init —
   e.g. during onboarding when the operational panels are hidden — it collapses
   via the shared .swipe-deck:not(.is-swipeable) rule, leaving no empty gap. */
#caddie-deck.is-swipeable { height: calc(100dvh - 188px); min-height: 300px; }
#caddie-deck-head { margin-top: 2px; }

/* Native scroll-snap deck (matches clubhouse/parent/admin). Two full-width
   panels; each keeps the .caddie-panel card skin and scrolls its own body. */
#caddie-deck.shell-deck { height: calc(100dvh - 188px); min-height: 300px; }
/* Bead, peek and panel geometry: shared, see .shell-deck above. The lift is
   this deck's own — the Caddie panel is a cream card, and a card sitting in a
   bead of background wants a shadow to sit in. */
#caddie-deck.shell-deck > .caddie-panel {
  box-shadow: 0 12px 36px rgba(120, 85, 50, 0.18);
}
/* Each deck panel wears the cream card skin and scrolls its own overflow, so
   polling re-renders inside a panel never change the page's height (the "jolt"
   this restructure kills). */
.caddie-panel {
  background: linear-gradient(160deg, #F5E8D0 0%, #EFE0C8 100%);
  border: 1px solid rgba(184,115,51,0.30);
  border-left: 4px solid var(--copper, #B87333);
  border-radius: 14px;
  padding: 16px 14px;
}
.swipe-deck.is-swipeable .swipe-track > .card.caddie-panel {
  box-shadow: 0 12px 36px rgba(120,85,50,0.18);
}
/* Panel 2 earnings — bigger now that they have room to breathe */
.caddie-earnings-big {
  display: flex;
  gap: 18px;
  flex-wrap: wrap;
}
.caddie-earnings-big > div { display: flex; flex-direction: column; }
.caddie-earnings-big .e-val {
  font-size: 22px;
  font-weight: 800;
  color: var(--copper, #B87333);
  line-height: 1.1;
}
.caddie-earnings-big .e-lbl {
  font-size: 10px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--muted, #6E5A40);
  margin-top: 2px;
}

/* ---- Caddie dashboard feed: the deck owns its height, the feed does not
   scroll (no page-level jolt on poll). ---- */
.caddie-feed {
  /* 100% of the row .app-shell already gave it, NOT calc(100dvh - 64px).
     .app-shell is `grid-template-rows: auto 1fr; height: 100dvh` — the second
     row is by definition exactly what the header leaves, at any header height
     and any safe-area inset. Subtracting a guess instead made the feed taller
     than its own row by however far the guess was off, and the difference
     became page scroll: on an iPhone the header measured 102px against the
     assumed 64, so the Caddie could drag the whole app up by 38px to reveal
     nothing. Reported 2026-08-21: "scroll up and down when I shouldn't be able
     to because there's nothing below."

     min-height:0 because a grid item's default `min-height:auto` refuses to
     shrink below its content, which would put the overflow straight back. */
  height: 100%;
  min-height: 0;
  overflow: hidden;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  -ms-overflow-style: none;
}
.caddie-feed::-webkit-scrollbar { display: none; }

/* ============================================================
   Slide-to-confirm — iPhone "slide to unlock" for actions that
   start or end something (accept a request, advance a ride, submit
   a report). Built by /shared/swipe-action.js. A copper knob rides
   a track; drag it across to fire. Reusable across portals.
   ============================================================ */
.slide-action {
  /* Every colour the track wears is a token with a LIGHT default, because the
     component was born on cream and most of its homes still are. The nav
     screen is #14100b, and there the cream treatment vanished: the track sat
     at 12% brown on near-black, the "well" highlight was a cream inset lit
     for a cream ground, and the label was #2E2114 ink — dark on dark, so the
     copper knob floated alone with no visible track and no readable label.
     Patrick, 2026-08-09: "Can't see slider on dark screen."

     Tokens rather than a `.nav-foot .slide-action` override so the next dark
     surface re-skins it by setting seven variables instead of rediscovering
     this. See the .on-dark block below. */
  --slide-track: rgba(120, 85, 50, 0.12);
  --slide-border: rgba(184, 115, 51, 0.35);
  --slide-label: var(--ink, #2E2114);
  --slide-well-lo: rgba(58, 38, 20, 0.16);
  --slide-well-hi: rgba(255, 250, 240, 0.30);
  --slide-fill-a: rgba(184, 115, 51, 0.30);
  --slide-fill-b: rgba(184, 115, 51, 0.16);
  --slide-success: #27392F;
  position: relative;
  /* Sized to its label rather than stretched edge to edge. A full-bleed bar
     reads as "this screen is a form"; a pill reads as one control, which is
     what travels across licensee verticals where the slider sits in a card,
     a row, or beside other actions rather than owning the width.
     min-width is the gesture's floor, not a look: travel is
     trackWidth - knob - 8 and it commits at 85%, so letting a short label
     shrink the track would quietly turn a deliberate drag into a flick. */
  display: flex;
  align-items: center;
  width: fit-content;
  min-width: 250px;
  max-width: 100%;
  margin-inline: auto;
  height: 52px;
  margin-top: 10px;
  border-radius: 999px;
  background: var(--slide-track);
  border: 1px solid var(--slide-border);
  /* Inset "well" so the track reads as a physical channel the knob rides in —
     grounds the slider and makes the knob feel like it sits above the surface. */
  box-shadow: inset 0 2px 5px var(--slide-well-lo),
              inset 0 -1px 0 var(--slide-well-hi);
  overflow: hidden;
  touch-action: pan-y;
  user-select: none;
  -webkit-user-select: none;
  cursor: grab;
}
.slide-action-fill {
  position: absolute;
  top: 0; left: 0; bottom: 0;
  width: 48px;
  border-radius: 999px;
  background: linear-gradient(90deg, var(--slide-fill-a), var(--slide-fill-b));
}
.slide-action-label {
  /* In flow, not absolute: the label is what gives the track its width now,
     so it has to occupy layout. Left padding clears the resting knob (46px
     at left:3), right padding balances the pill. */
  position: relative;
  flex: 1 1 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  padding-left: 60px;
  padding-right: 26px;
  font-family: var(--font-ui);
  font-size: 14px;
  font-weight: 700;
  letter-spacing: 0.02em;
  color: var(--slide-label);
  pointer-events: none;
  white-space: nowrap;
}
.slide-action-knob {
  position: absolute;
  top: 3px; left: 3px;
  width: 46px; height: 46px;
  border-radius: 50%;
  background: var(--copper, #B87333);
  box-shadow: 0 3px 10px rgba(120, 85, 50, 0.4);
  display: flex;
  align-items: center;
  justify-content: center;
  color: #FFF5E6;
  font-size: 24px;
  font-weight: 800;
  line-height: 1;
  touch-action: none;
  cursor: grab;
  z-index: 2;
}
.slide-action-knob:active { cursor: grabbing; }
.slide-action-icon { width: 20px; height: 20px; display: block; }

/* Status glyph — spinner / check / X. Sits centred in the track and takes over
   from the knob once the action is in flight (see .has-status below). */
.slide-action-status {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--copper, #B87333);
  opacity: 0;
  transform: scale(0.5);
  transition: opacity .2s ease, transform .24s cubic-bezier(0.34, 1.56, 0.64, 1);
  pointer-events: none;
  z-index: 3;                       /* over the knob, which is fading out beneath it */
}
.slide-action.has-status .slide-action-status { opacity: 1; transform: scale(1); }
/* The knob hands off to the status glyph rather than riding along with it. */
.slide-action.has-status .slide-action-knob {
  opacity: 0;
  transition: opacity .18s ease;
  pointer-events: none;
}
.slide-action.has-status { cursor: default; }
.slide-action.is-loading .slide-action-status .slide-action-icon { animation: slide-action-spin .9s linear infinite; }
.slide-action.is-success .slide-action-status { color: var(--slide-success); }
.slide-action.is-error .slide-action-status { color: #A6432E; }
@keyframes slide-action-spin { to { transform: rotate(360deg); } }

/* ── Dark surfaces ─────────────────────────────────────────────────────────
   The nav screen paints #14100b edge to edge, and the cream treatment above
   disappears on it entirely. Applied by container so the nav screen gets it
   without every call site remembering, and by class so anything else on a
   dark ground can opt in with one word.

   Not a simple inversion. The track lifts to a cream tint at 10% so it reads
   as a channel cut INTO the dark rather than a bar laid on top; the well
   flips (shadow above, light lip below) because a dark surface is lit from
   the same direction but has further to fall; and the copper fill runs
   stronger, since at 30% over black it was indistinguishable from the track
   and the drag gave no feedback. The label goes to the banner's own #F6ECDA
   rather than pure white — white would out-shout "100 ft" two rows up. */
.nav-screen .slide-action,
.slide-action.on-dark {
  --slide-track: rgba(246, 236, 218, 0.10);
  --slide-border: rgba(226, 168, 106, 0.55);
  --slide-label: #F6ECDA;
  --slide-well-lo: rgba(0, 0, 0, 0.45);
  --slide-well-hi: rgba(246, 236, 218, 0.14);
  --slide-fill-a: rgba(184, 115, 51, 0.62);
  --slide-fill-b: rgba(184, 115, 51, 0.30);
  /* #27392F is a deep green that reads as a hole on near-black. */
  --slide-success: #7FCB9B;
}

/* Collapsed pill — once the track is only carrying status, it shrinks to a
   badge instead of holding a full-width bar of empty colour. Width itself is
   animated inline (JS freezes the pixel endpoints); this centres it. */
.slide-action.collapsed { margin-left: auto; margin-right: auto; }
.slide-action.collapsed .slide-action-label { opacity: 0; }

/* ── Terminal seal ─────────────────────────────────────────────────────────
   The end of a process, lifted off the page at the spot the slider latched.
   Solid and flat where the track was an inset well — the well invited a drag,
   this is closed. Fixed, not absolute: it has to outlive the card that spawned
   it, which the caller is usually tearing down in the same tick. */
.slide-action-seal {
  position: fixed;
  z-index: 9000;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  border-radius: 999px;
  background: #27392F;
  color: #F2EADC;
  box-shadow: 0 10px 28px rgba(39, 57, 47, 0.34);
  font-family: var(--font-ui);
  font-size: 14px;
  font-weight: 700;
  letter-spacing: 0.02em;
  white-space: nowrap;
  pointer-events: none;
  opacity: 0;
  transform: scale(0.97);
  transition: opacity .26s ease, transform .26s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.slide-action-seal.in { opacity: 1; transform: scale(1); }
.slide-action-seal.out {
  opacity: 0;
  transform: scale(0.99) translateY(-6px);
  transition: opacity .3s ease, transform .3s ease;
}
.slide-action-seal .slide-action-icon { width: 19px; height: 19px; flex: none; }
.slide-action-seal-label { overflow: hidden; text-overflow: ellipsis; }
/* An ending isn't always a win. A no-show is charged, a cancellation is a
   withdrawal — neither should wear the green of a delivered ride. */
.slide-action-seal.danger { background: #A6432E; box-shadow: 0 10px 28px rgba(166, 67, 46, 0.34); }
.slide-action-seal.secondary { background: #6E5A40; box-shadow: 0 10px 28px rgba(110, 90, 64, 0.30); }
@media (prefers-reduced-motion: reduce) {
  .slide-action-seal { transition: none; opacity: 1; transform: none; }
  .slide-action-seal.out { transition: none; opacity: 0; transform: none; }
}

/* While the action is still in flight the track stays copper — going green at
   latch would promise success before the request has actually settled. */
.slide-action.confirmed.is-loading {
  background: rgba(120, 85, 50, 0.12);
  border-color: rgba(184, 115, 51, 0.35);
}
.slide-action.confirmed.is-loading .slide-action-fill {
  background: linear-gradient(90deg, rgba(184,115,51,0.30), rgba(184,115,51,0.16));
}

/* A failed action recolours the latched track. Carries .confirmed too so it
   outranks the green "latched" rules below on specificity, not source order. */
.slide-action.confirmed.is-error {
  background: rgba(166, 67, 46, 0.14);
  border-color: rgba(166, 67, 46, 0.42);
}
.slide-action.confirmed.is-error .slide-action-fill {
  background: linear-gradient(90deg, rgba(166,67,46,0.32), rgba(166,67,46,0.18));
}

.slide-action.confirmed {
  background: rgba(39, 57, 47, 0.16);
  border-color: rgba(39, 57, 47, 0.4);
}
.slide-action.confirmed .slide-action-fill {
  background: linear-gradient(90deg, rgba(39,57,47,0.35), rgba(39,57,47,0.22));
}
.slide-action.confirmed .slide-action-knob { background: #27392F; }
/* Danger variant (no-show, cancel) — copper → clay red */
.slide-action.danger .slide-action-knob { background: #A6432E; color: #FFF0EA; }
.slide-action.danger .slide-action-fill {
  background: linear-gradient(90deg, rgba(166,67,46,0.30), rgba(166,67,46,0.15));
}
.slide-action.danger { border-color: rgba(166, 67, 46, 0.4); }
/* Secondary variant (cancel) — muted, smaller weight */
.slide-action.secondary { height: 44px; background: rgba(120, 85, 50, 0.08); }
.slide-action.secondary .slide-action-knob {
  width: 38px; height: 38px; background: #8A7560; font-size: 20px;
}
.slide-action.secondary .slide-action-icon { width: 17px; height: 17px; }
.slide-action.secondary .slide-action-label {
  font-weight: 600; font-size: 13px; color: var(--muted, #6E5A40);
  padding-left: 52px;             /* smaller knob (38px) sits closer in */
}
/* A secondary action is allowed to be the stubbiest of the three — it's the
   cancel, not the commitment — but not so short the drag stops meaning it. */
.slide-action.secondary { min-width: 220px; }
/* Disabled — greyed, no drag (mirrors a gated button like Send Caddie). */
.slide-action.disabled { opacity: 0.42; cursor: not-allowed; filter: saturate(0.55); }
.slide-action.disabled .slide-action-knob { cursor: not-allowed; }
/* Hero variant — the primary booking CTA: taller, with a light, subtle label
   and a gentle radiance that invites the slide. */
/* The primary CTA carries the longest travel of the three — it's the booking. */
.slide-action.hero { height: 58px; position: relative; min-width: 280px; }
.slide-action.hero .slide-action-label {
  font-size: 14px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  font-weight: 400;                 /* light + subtle, per request */
  color: rgba(58, 38, 20, 0.60);
  padding-left: 64px;               /* larger knob (50px) */
}
.slide-action.hero .slide-action-knob {
  width: 50px; height: 50px;
  animation: slide-knob-glow 2.2s ease-in-out infinite;   /* breathing halo */
}
.slide-action.hero .slide-action-icon { width: 22px; height: 22px; }
@keyframes slide-knob-glow {
  0%, 100% { box-shadow: 0 3px 10px rgba(120, 85, 50, 0.40), 0 0 0 0 rgba(184, 115, 51, 0); }
  50%      { box-shadow: 0 3px 12px rgba(120, 85, 50, 0.45), 0 0 16px 4px rgba(184, 115, 51, 0.55); }
}
/* A slow light sweep across the track, hinting the direction to slide. */
.slide-action.hero::after {
  content: '';
  position: absolute; inset: 0;
  border-radius: inherit;
  pointer-events: none;
  z-index: 1;                        /* over fill + label, under the knob (z:2) */
  background: linear-gradient(100deg, transparent 22%, rgba(255, 244, 218, 0.55) 50%, transparent 78%);
  transform: translateX(-120%);
  animation: slide-shimmer 3s ease-in-out infinite;
}
@keyframes slide-shimmer {
  0%       { transform: translateX(-120%); }
  55%, 100% { transform: translateX(120%); }
}
/* Stop the invitation once it's latched or gated. */
.slide-action.hero.confirmed .slide-action-knob,
.slide-action.hero.disabled .slide-action-knob { animation: none; }
.slide-action.hero.confirmed::after,
.slide-action.hero.disabled::after { display: none; }
@media (prefers-reduced-motion: reduce) {
  .slide-action.hero .slide-action-knob,
  .slide-action.hero::after { animation: none; }
}
@media (prefers-reduced-motion: reduce) {
  .slide-action-knob, .slide-action-fill, .slide-action-label { transition: none !important; }
  /* The status glyph appears without the pop, but the spinner keeps turning —
     it's the only signal that the action is still in flight. */
  .slide-action-status { transition: none !important; transform: none !important; }
  .slide-action.has-status .slide-action-status { transform: none; }
}

.caddie-card {
  min-height: auto;
  padding: 12px 14px 20px;
  max-width: 520px;
  margin: 0 auto;
  width: 100%;
}
.caddie-card-inner {
  background: linear-gradient(160deg, #F5E8D0 0%, #EFE0C8 100%);
  border: 1px solid rgba(184,115,51,0.30);
  border-left: 4px solid var(--copper, #B87333);
  border-radius: 14px;
  padding: 16px 14px;
  box-shadow: 0 12px 36px rgba(120,85,50,0.18);
}
/* Compact header: avatar + name + duty toggle in one row */
.caddie-venue-logo {
  text-align: center;
  margin-bottom: 10px;
}
.caddie-venue-logo img {
  max-height: 48px;
  max-width: 70%;
  object-fit: contain;
}
.caddie-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 10px;
}
.caddie-header-left {
  display: flex;
  align-items: center;
  gap: 8px;
}
/* Car + earnings in one row */
.caddie-meta-row {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 10px;
}
.caddie-earnings-inline {
  display: flex;
  align-items: center;
  gap: 2px;
  font-size: 12px;
  font-weight: 600;
  color: var(--copper, #B87333);
  white-space: nowrap;
}
.caddie-earnings-inline .muted { font-weight: 400; font-size: 10px; }
/* Section labels (compact) */
.caddie-section-label {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted, #6E5A40);
  margin-bottom: 6px;
}
/* Active ride info grid — tight 2x2 */
.caddie-info-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 4px 12px;
  font-size: 13px;
  line-height: 1.4;
}
.caddie-info-grid .muted { font-size: 11px; margin-right: 4px; }
.pending-ride-card {
  padding: 12px;
  border: 1px solid rgba(184,115,51,0.30);
  border-radius: 12px;
  margin-bottom: 8px;
  background: linear-gradient(135deg, #FBF4E6 0%, #F5E8D0 100%);
  box-shadow: 0 4px 12px rgba(120,85,50,0.10);
}
.pending-ride-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 4px;
}
.pending-ride-name {
  font-weight: 700;
  font-size: 15px;
  color: var(--ink, #2E2114);
}
.pending-ride-phone {
  font-size: 12px;
  color: var(--copper, #B87333);
  font-weight: 600;
  margin-bottom: 6px;
}
.pending-ride-route {
  font-size: 12px;
  line-height: 1.5;
  margin-bottom: 6px;
}
.pending-ride-route .route-row {
  display: flex;
  align-items: center;
  gap: 5px;
}
.pending-ride-route .route-dot {
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--copper, #B87333); flex-shrink: 0;
}
.pending-ride-route .route-square {
  width: 7px; height: 7px; border-radius: 2px;
  background: var(--bronze, #7F6A4F); flex-shrink: 0;
}
.pending-ride-meta-row {
  display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 8px;
}
.pending-ride-meta {
  font-size: 11px; color: var(--copper, #B87333); font-weight: 600;
  background: rgba(184,115,51,0.08); padding: 2px 6px; border-radius: 5px;
}
/* .pending-gps-btn was removed 2026-08-07 — it was a second "Get Directions"
   hand-off to the native Maps app, on the card BEFORE the ride is accepted.
   Navigation frames itself in-app on accept instead; see nav-frame below. */
.pending-ride-card .accept-btn {
  background: var(--copper, #B87333);
  color: var(--on-accent, #F8EFDE);
  border: none;
  font-size: 14px;
}
.pending-venue-logo-wrap {
  text-align: center;
  margin-bottom: 6px;
}
.pending-venue-logo {
  max-height: 36px;
  max-width: 60%;
  object-fit: contain;
}

/* Phones + small tablets: single full-width feed (no map stage); the caddie
   keeps the on-demand fullscreen nav overlay. */
@media (max-width: 899px) {
  .caddie-layout { grid-template-columns: 1fr; height: auto; }
  .caddie-stage { display: none; }
}

/* ============================================================
   Caddie iPad flagship (Phase 4): split the portal into a left trip rail and
   a persistent right map stage. Parked → the fleet "who's out driving" map;
   an active ride docks the turn-by-turn nav (#nav-screen) over the right
   column. Kicks in at tablet-landscape width and up.
   ============================================================ */
@media (min-width: 900px) {
  .caddie-layout { grid-template-columns: 380px 1fr; height: 100%; min-height: 0; }
  .caddie-feed { border-right: 1px solid var(--border); }

  .caddie-stage {
    display: flex;
    flex-direction: column;
    position: relative;
    height: 100%;
    min-height: 0;
    min-width: 0;
    background: var(--panel);
  }
  .caddie-fleet-head {
    display: flex; align-items: baseline; gap: 12px;
    padding: 14px 20px;
    border-bottom: 1px solid var(--border);
    flex: 0 0 auto;
  }
  .caddie-fleet-title { font-size: 15px; letter-spacing: 0.02em; }
  .caddie-fleet-sub { font-size: 12px; }
  .caddie-fleet-map { flex: 1 1 auto; min-height: 0; }

  /* "Who's out driving" roster — a horizontal chip strip above the map. */
  .caddie-fleet-list {
    display: flex; gap: 8px; flex: 0 0 auto;
    padding: 10px 20px; overflow-x: auto;
    border-bottom: 1px solid var(--border);
    scrollbar-width: none;
  }
  .caddie-fleet-list::-webkit-scrollbar { display: none; }
  .caddie-fleet-list .fleet-chip {
    flex: 0 0 auto; display: flex; flex-direction: column; gap: 2px;
    padding: 7px 12px; border-radius: 10px; min-width: 118px;
    background: var(--bg); border: 1px solid var(--border);
  }
  .caddie-fleet-list .fleet-chip.me {
    border-color: var(--copper, #B87333);
    background: rgba(184, 115, 51, 0.08);
  }
  .caddie-fleet-list .fc-cart { font: 700 12px/1 var(--font-ui); letter-spacing: 0.04em; }
  .caddie-fleet-list .fc-who { font-size: 11px; color: var(--muted); }
  .caddie-fleet-list .fleet-empty { font-size: 12px; padding: 4px 0; }

  /* Dock the turn-by-turn nav into the right column (below the topbar) instead
     of covering the whole viewport. z-index keeps it above the fleet map. */
  .nav-screen { inset: 64px 0 0 380px; }
}

/* ---- Sitter directory: ratings, preferred toggle, review picker ---- */
.sitter-rating { display: inline-flex; align-items: center; }
.sitter-star { color: rgba(138, 85, 31, 0.28); font-size: 14px; line-height: 1; letter-spacing: 1px; }
.sitter-star.on { color: var(--copper-mid, #B87333); }
.sitter-fav {
  background: none; border: none; cursor: pointer; padding: 2px 4px;
  font-size: 20px; line-height: 1; color: rgba(138, 85, 31, 0.3);
  transition: color 0.15s, transform 0.12s;
}
.sitter-fav:hover { transform: scale(1.12); }
.sitter-fav.on { color: var(--copper-mid, #B87333); }
.sitter-star-picker .pick {
  cursor: pointer; font-size: 24px; padding: 0 3px; color: rgba(138, 85, 31, 0.28);
  transition: color 0.1s, transform 0.1s;
}
.sitter-star-picker .pick:hover { transform: scale(1.1); }
.sitter-star-picker .pick.on { color: var(--copper-mid, #B87333); }
.sitter-review-form { border-top: 1px solid var(--hairline, rgba(138,85,31,0.15)); padding-top: 10px; }

/* ---- Member-stack swipe decks (Clubhouse host + Parent portals) ----
   Same engine as the Locker/Caddie decks, but the panels are .section
   elements. Slid like the .card panels: one full page each in the track. */
.swipe-deck .section {
  margin: 0 !important;
  padding: 2px 2px 10px;
  box-sizing: border-box;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  touch-action: pan-y; /* see .swipe-deck .card — the scroll-container backstop */
}
.swipe-deck.is-swipeable .swipe-track > .section {
  height: 100%;
  min-width: 0;
  min-height: 0;
}
.member-deck.is-swipeable { height: calc(100dvh - 172px); min-height: 320px; }
.member-deck:not(.is-swipeable) { height: auto; min-height: 0; }
.member-deck:not(.is-swipeable) .swipe-track { height: auto; }
.member-deck:not(.is-swipeable) .section { height: auto; overflow-y: visible; }

/* Adaptive member-stack decks (Clubhouse/Parent): the deck height follows the
   ACTIVE panel's content (set inline by swipe-deck.js) instead of a fixed full
   screen, so a short section reads as a tidy card, not a blank full-viewport
   box. Panels keep height:auto so their content height stays measurable, and
   the track top-aligns them so a taller neighbor keeps its natural height
   instead of stretching to fit. */
/* No height TRANSITION: animating height relayouts every frame, which made the
   deck visibly stutter as it collapsed/expanded between a tall panel (Pulse)
   and a short one (Send a Caddie). The slide itself already carries the
   motion; the container just snaps to the new panel's height. */
.member-deck.adaptive { height: auto; min-height: 0; overflow: hidden; }
/* ...but once it's an actual pager, keep a comfortable swipe target. A short
   panel (e.g. "Send a Caddie" before a member is picked) otherwise collapses the
   deck to ~95px, leaving the big area below it OUTSIDE the deck — so a swipe
   there does nothing and you get stuck. This floor keeps the whole natural swipe
   zone live while tall panels still grow past it. */
.member-deck.adaptive.is-swipeable { min-height: 420px; }
.member-deck.adaptive .swipe-track { height: 100%; align-items: flex-start; }
.member-deck.adaptive.is-swipeable .swipe-track > .section {
  height: auto;
  overflow-y: visible;
  padding: 2px 2px 4px;
}

/* Portals whose main content IS a swipe deck (Clubhouse/Parent) top-align it —
   the shared .member-stack centers its content (right for the Member home's
   greeting+booking card, wrong for a deck, which otherwise floats mid-screen
   with blank above and below). */
.member-stack.deck-stack { justify-content: flex-start; }

/* Locker open: the home greeting (with its big lift-up margin) and the
   stack's vertical centering pushed the locker deck way down — a dead gap at
   the top and the panel card tailing off the bottom of the screen. The locker
   is its own full screen: top-align and drop the greeting entirely. */
.member-stack.locker-open { justify-content: flex-start; }
.member-stack.locker-open .home-greeting { display: none; }

/* ---- View Transitions: native continuity between discrete state changes ----
   Used for the Member locker open/close and the Concierge tab switch (see
   motion.js). Progressive: unsupported browsers just swap instantly. Kept a
   quiet cross-fade — motion should confirm the change, not perform. */
::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: var(--dur-3, 340ms);
  animation-timing-function: var(--ease, ease);
}
@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) { animation: none !important; }
}

/* ============================================================
   Caddie in-app navigation screen
   ------------------------------------------------------------
   Full-screen overlay framing GOOGLE'S directions INSIDE the
   portal (deliberately no bounce to the native Maps app — the
   geofence/overspeed banner has to stay on screen while driving,
   see frontend/driver/app.js). An espresso header with the leg and
   our own live distance-to-target, the Google iframe, then a footer
   carrying the leg's advancing slide-action and an End button.
   ============================================================ */
.nav-screen {
  position: fixed;
  inset: 0;
  z-index: 4000;
  display: none;
  flex-direction: column;
  background: #14100b;
}
.nav-screen.open { display: flex; }
.nav-banner {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: calc(12px + env(safe-area-inset-top, 0px)) 16px 12px;
  background: linear-gradient(100deg, #241811 0%, #2c1d10 45%, #1a1009 100%);
  border-bottom: 1px solid rgba(184, 115, 51, 0.55);
  color: #F6ECDA;
}
.nav-banner .nb-glyph {
  font-size: 36px;
  line-height: 1;
  width: 52px;
  text-align: center;
  color: #E4A87E;
  flex: 0 0 auto;
}
.nav-banner .nb-main { min-width: 0; }
.nav-banner .nb-dist {
  font: 800 22px/1.1 var(--font-ui);
  color: #F2C879;
  font-variant-numeric: tabular-nums;
  min-height: 12px;
}
.nav-banner .nb-text {
  font: 700 17px/1.25 var(--font-ui);
  color: #F6ECDA;
  overflow-wrap: anywhere;
}
/* The follower's map. The Google Embed iframe that briefly stood here could
   not follow the cart -- no recentering, no maneuver advance -- so the map,
   the route and the maneuvers are ours again. The wrapper keeps its name
   because the fallback message and the layout are unchanged. */
.nav-frame-wrap {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
  background: #1d150d;
}
#nav-map { position: absolute; inset: 0; }
.nav-frame-fallback {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 28px 24px;
  text-align: center;
  color: rgba(246, 236, 218, 0.86);
  font: 600 14px/1.5 var(--font-ui);
  background: #1d150d;
  z-index: 401;              /* over Leaflet's own panes */
}
.nav-frame-fallback[hidden] { display: none; }

/* "Then ..." preview of the maneuver after the current one. */
.nav-next {
  padding: 7px 16px;
  background: #1d150d;
  border-bottom: 1px solid rgba(184, 115, 51, 0.3);
  color: rgba(246, 236, 218, 0.75);
  font: 600 12px/1.2 var(--font-ui);
  letter-spacing: 0.02em;
}
.nav-next[hidden] { display: none; }

/* The route uses a road carts may cross but not travel along (ARS 28-966(B)).
   Deliberately the loudest thing on the nav screen: it sits directly under
   the maneuver banner, in the one colour nothing else on this screen uses,
   and it does not auto-dismiss. It is not an error the app can recover from
   -- it is the Caddie being told to use their judgement about a route the
   router should not have produced. */
.nav-illegal {
  padding: 9px 16px;
  background: #7a1f12;
  border-bottom: 1px solid rgba(255, 190, 170, 0.35);
  color: #ffe9e3;
  font: 700 12px/1.35 var(--font-ui);
  letter-spacing: 0.02em;
}
.nav-illegal[hidden] { display: none; }
.nav-illegal b { color: #fff; }

/* Spoken-guidance toggle. A glove-sized target next to End navigation, and
   deliberately quiet when on: a Caddie who wants the voice should not have a
   lit control reminding them of it every mile. */
.nav-voice {
  flex: 0 0 auto;
  min-width: 46px;
  height: 40px;
  padding: 0 10px;
  border-radius: 10px;
  border: 1px solid rgba(184, 115, 51, 0.45);
  background: transparent;
  color: rgba(246, 236, 218, 0.85);
  font-size: 17px;
  line-height: 1;
  cursor: pointer;
}
.nav-voice[aria-pressed="false"] {
  border-color: rgba(246, 236, 218, 0.2);
  color: rgba(246, 236, 218, 0.4);
}
.nav-voice[hidden] { display: none; }

/* Appears once the Caddie pans away from the cart. */
.nav-recenter {
  position: absolute;
  right: 14px;
  bottom: 16px;
  z-index: 402;
  padding: 9px 14px;
  border-radius: 999px;
  border: 1px solid rgba(184, 115, 51, 0.55);
  background: #241811;
  color: #F2C879;
  font: 700 12px/1 var(--font-ui);
  letter-spacing: 0.06em;
  cursor: pointer;
  box-shadow: 0 6px 18px rgba(20, 10, 2, 0.45);
}
.nav-recenter[hidden] { display: none; }

.nav-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 12px;
  padding: 12px 16px calc(12px + env(safe-area-inset-bottom, 0px));
  background: linear-gradient(100deg, #241811 0%, #2c1d10 45%, #1a1009 100%);
  border-top: 1px solid rgba(184, 115, 51, 0.55);
}
.nav-remaining {
  font: 800 16px/1.1 var(--font-ui);
  color: #F6ECDA;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
/* Holds the leg's advancing slide-action, mirrored out of the ride panel so
   the auto-opened overlay is never a trap. Full width — it is a slide, not
   a link. The old .nav-ext "Open in Maps" hatch was removed 2026-08-07:
   handing the Caddie to another app takes the geofence banner off screen. */
.nav-foot-actions { flex: 1 1 100%; order: 3; }
.nav-foot-actions:empty { display: none; }
/* The footer's End button sits on espresso — use the desk's solid secondary. */
.nav-foot .btn.secondary {
  background: rgba(228, 168, 126, 0.14);
  border-color: rgba(228, 168, 126, 0.45);
  color: #F2C879;
  box-shadow: none;
}

/* ── Duty toggle switch (driver dashboard) ──────────────────────────────
   iOS-style toggle with spring knob, progressive track fill, and
   press-scale. Stone track when off, copper track when on. */
.duty-toggle-wrap {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}
.duty-toggle-label {
  font-size: 14px;
  font-weight: 600;
  letter-spacing: 0.04em;
  color: var(--text);
  transition: color 0.4s ease;
}
.duty-toggle-label.on { color: var(--copper-mid); }

.duty-toggle {
  position: relative;
  width: 56px;
  height: 32px;
  border: none;
  border-radius: 16px;
  background: var(--stone);
  cursor: pointer;
  padding: 0;
  flex-shrink: 0;
  -webkit-tap-highlight-color: transparent;
  outline: none;
  overflow: hidden;
  transition: background 0.5s ease;
}
.duty-toggle:focus-visible { box-shadow: 0 0 0 3px rgba(184,115,51,0.25); }

/* Progressive color fill: a gradient overlay sweeps from left to right
   when the toggle is ON, matching the video's progressive blue fill. */
.duty-toggle-fill {
  position: absolute;
  inset: 0;
  border-radius: 16px;
  background: linear-gradient(90deg, var(--copper-mid) 0%, var(--copper-deep) 100%);
  transform: translateX(-100%);
  transition: transform 0.5s cubic-bezier(0.22, 1, 0.36, 1);
  pointer-events: none;
}
.duty-toggle[aria-checked="true"] .duty-toggle-fill {
  transform: translateX(0);
}

.duty-toggle-knob {
  position: absolute;
  top: 3px;
  left: 3px;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 2px 6px rgba(0,0,0,0.3);
  /* Spring with slight overshoot — matches iOS toggle feel from the video */
  transition: transform 0.5s cubic-bezier(0.34, 1.3, 0.64, 1),
              scale 0.15s ease;
  z-index: 1;
  will-change: transform;
}
.duty-toggle[aria-checked="true"] .duty-toggle-knob {
  transform: translateX(24px);
}
/* Press scale: knob grows when touched, matching the video */
.duty-toggle:active .duty-toggle-knob {
  scale: 1.15;
  transition: scale 0.08s ease;
}
.duty-toggle:active[aria-checked="true"] .duty-toggle-knob {
  transform: translateX(24px) scale(1.15);
}
@media (prefers-reduced-motion: reduce) {
  .duty-toggle, .duty-toggle-knob, .duty-toggle-label, .duty-toggle-fill {
    transition: none;
  }
}

/* ── Landing preview: "Where to?" + live cart map (pre-auth) ────────────
   Shows the booking widget and a live cart map on the landing page so
   visitors can see the club's destinations and carts before signing in. */
.landing-preview {
  width: 100%;
  max-width: 420px;
  margin: 4px auto 0;
  padding: 0 16px;
  display: flex;
  flex-direction: column;
  gap: 14px;
  flex-shrink: 0;
}
.landing-hero {
  padding: 20px 18px 18px;
  border-radius: 12px;
}
.landing-hero .hero-title {
  font-size: 24px;
  margin-bottom: 8px;
}
.landing-hero .route-row {
  cursor: pointer;
}
.landing-hero .hero-btn {
  width: 100%;
  margin-top: 14px;
  height: 44px;
  border-radius: 8px;
  font-weight: 600;
  font-size: 14px;
}
.landing-map-preview {
  width: 100%;
  height: 240px;
  border-radius: 12px;
  overflow: hidden;
  border: 1px solid rgba(184, 115, 51, 0.25);
  box-shadow: 0 8px 24px rgba(120, 85, 50, 0.14);
  background: #E7D6B8;
  z-index: 0;
}
@media (max-width: 480px) {
  .landing-preview { padding: 0 12px; }
  .landing-map-preview { height: 200px; }
}

/* ============================================================
   Landing → vertical TikTok-style snap deck (pre-auth member page).
   Hero → "Where to?" → live map, each a full-viewport snap card that
   swipes/scrolls vertically. Reuses the .shell-dot pager look.
   ============================================================ */
.landing.landing-deck {
  scroll-snap-type: y mandatory;
  overscroll-behavior-y: contain;
  scroll-behavior: smooth;
  scrollbar-width: none;
  -ms-overflow-style: none;
}
.landing.landing-deck::-webkit-scrollbar { display: none; }
/* The deck stacks full-height cards; drop the vertical-centering spacers. */
.landing.landing-deck::before,
.landing.landing-deck::after { content: none; display: none; }

.landing-card {
  position: relative;
  flex: 0 0 auto;
  width: 100%;
  min-height: 100dvh;
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 24px 16px;
  scroll-snap-align: start;
  scroll-snap-stop: always;
}
.landing-card[data-landing-card="whereto"] .landing-preview,
.landing-card[data-landing-card="map"] .landing-map-preview { width: 100%; max-width: 440px; }
.landing-card[data-landing-card="map"] .landing-map-preview { height: min(62vh, 540px); }

/* Hero card: keep the whole lockup (crest → wordmark → tagline → phone → CTA)
   a single comfortably-centered bundle so the CTA never sits under the browser
   chrome. Smaller crest + tighter rhythm than the other portal landings, and
   scoped to this card so those are untouched. */
.landing-card[data-landing-card="hero"] {
  justify-content: center;
  padding-left: 6px; padding-right: 6px;
  padding-bottom: max(30px, env(safe-area-inset-bottom, 0px));
}
/* Widen the login column so the phone field + swipe CTA get a roomier target,
   and give the stack an even, generous rhythm (gap replaces the ad-hoc margins
   below) so the crest / wordmark / tagline / field breathe instead of crowding. */
.landing-card[data-landing-card="hero"] .landing-inner { padding: 0 8px; max-width: 460px; gap: 16px; }
/* Fine print at the bottom of the (login-only) card, IN FLOW.
   It used to be absolutely pinned there, which meant it reserved no height
   and simply painted over whatever the login column put underneath it.
   Measured in Chromium with the CTA mounted, before this change:

     viewport         CTA bottom   fine-print top   overlap
     320x568 (SE)     532          520              12px  <- collided
     1280x900         859          860              0px   <- touching
     390x844          739          792              none

   So the hero had no slack left at the small end even before the About link
   was added below the CTA. In flow, .landing-inner (flex: 1 1 auto) simply
   gives the block its height and the login column compresses by that much --
   the same arrangement the non-hero landing has always used, and the reason
   the base rule above is position: static. */
.landing-card[data-landing-card="hero"] .landing-copyright {
  position: static;
  flex-shrink: 0;
  width: 100%;
  margin-bottom: 0;
  padding: 0 18px max(12px, env(safe-area-inset-bottom, 0px));
  text-align: center;
}
/* The lockup art carries crest + wordmark in one roughly square image, so it
   takes the combined footprint the crest and text wordmark used to share —
   no scale transform needed, and nothing below it gets nudged. */
.landing-card[data-landing-card="hero"] .landing-wordmark-art {
  width: 90%; max-width: 468px;   /* 0.9 with the crest — see the note there */
  /* Stands alone: a break under the art roughly 3x the rhythm below it, so
     the lockup reads as the mark and the tagline/field/CTA read as one
     supporting group rather than a continuous column. Paired with the
     tighter .landing-inner gap below — the separation comes from contrast
     between the two, not from height alone, which keeps the cost to the
     stack (and to the field's headroom under the keyboard) down. */
  margin-bottom: 28px;
}
/* The typewriter placeholder ("login with your phone #") is longer than a phone
   number — ease the tracking so it fits the field on one line. */
.landing-card[data-landing-card="hero"] #phone::placeholder {
  letter-spacing: 0.06em; font-size: 12px;
}
.landing-card[data-landing-card="hero"] .landing-wordmark { font-size: clamp(24px, 5.6vw, 34px); }
.landing-card[data-landing-card="hero"] .slide-action { margin-top: 4px; }
/* Keep the CTA copy clear of the knob, with tighter tracking so a full phrase
   fits the swipe zone. Asymmetric padding, not `0 52px`: the knob only sits on
   the left, and now that the label sizes the track, padding is width — a
   symmetric 52 would pad the right side for a knob that isn't there. */
.landing-card[data-landing-card="hero"] .slide-action.hero .slide-action-label {
  padding-left: 60px;
  padding-right: 24px;
  font-size: 12px;
  letter-spacing: 0.05em;
}
/* Progressive login: ONE flip CTA carries the whole sequence — "Danny, fetch
   my ball" → "One moment…" → back to "Sign me in" → "Welcome to the Club".
   The slide-to-sign-in that used to mount here for the code step is gone; the
   slide gesture belongs to Send Caddie alone. */
.auth-flow .auth-send { width: 100%; margin-top: 12px; }
.auth-flow .auth-send[hidden] { display: none !important; }
.auth-flow .auth-flip-mount {
  display: flex;
  justify-content: center;
  width: 100%;
  margin-top: 12px;
}
/* The flip CTA wears the mockup's black pill: BOTH faces espresso, so the
   turn swaps the words, not the colour — the motion itself is the state
   change. Title case, not the tracked-out uppercase the staff toggles use.
   (Set in the UI face, not The Seasons, as an earlier version of this comment
   said: it is a button, and buttons are DM Sans.) */
.flip-toggle.flip-cta .flip-toggle-face {
  background: #2E2114;
  border: 1px solid #2E2114;
  color: #FBF4E6;
  text-shadow: none;
  border-radius: 999px;
  padding: 12px 30px;
  min-height: 46px;
  font-family: var(--font-ui);
  font-size: 16px;
  font-weight: 700;
  letter-spacing: 0.03em;
  text-transform: none;
  box-shadow: 0 2px 6px rgba(40, 22, 6, 0.22);
}

/* Full-width flip CTA -- the Caddie's ride-advancing tap actions (At
   <location>, Member on board) share this pill's colours and one-shot
   flip-to-confirm motion, but sit in a full-width slot (the main ride
   panel + the nav-footer mirror) where the auth CTA's inline pill would
   sit small and off-center. Same target size a Caddie's thumb already
   knows to reach for from the slide action these replace.
   Patrick, 2026-08-22: "At location and member on board should just be
   taps, not swipes. Use the flip button." */
.flip-toggle.flip-cta.flip-advance { display: block; width: 100%; }
.flip-toggle.flip-cta.flip-advance .flip-toggle-inner { width: 100%; }
.flip-toggle.flip-cta.flip-advance .flip-toggle-face { width: 100%; }

/* Phone login is intentionally static. The previous focus/visualViewport
   versions jumped between full and compact compositions as iOS opened,
   dismissed, and reopened the keyboard. Keep the entire phone layout compact
   from first paint instead: the field is already above the keyboard before a
   tap, so no JS state or browser-specific keyboard detection is required. */
@media (max-width: 720px) {
  .member-landing .landing-card[data-landing-card="hero"] {
    justify-content: flex-start;
    padding-top: max(42px, env(safe-area-inset-top, 0px));
    /* 6+8 of card+inner padding cost the art 28px of width on every phone,
       and the art is the one element that wants every pixel — a 360px
       handset had only 15px of headroom left. Halved on both so the lockup
       has somewhere to grow; the text below it never came close to the
       edge anyway. */
    padding-left: 0;
    padding-right: 0;
  }
  .member-landing .landing-card[data-landing-card="hero"] .landing-inner {
    /* Tight rhythm on purpose: the tagline, field, and CTA group up as
       supporting text so the break under the art (below) is unmistakably
       the biggest interval on the page. */
    gap: 12px;
    padding: 0 7px;
    /* The 380px cap never binds on a phone, so the column ran to ~96% of the
       screen and the name sat almost edge to edge. The mock has everything
       clear of the edges; 72vw puts the wordmark at ~72% and gives the whole
       composition the margin it was missing. The px ceiling keeps a big
       handset from letting the column outgrow the art. */
    max-width: min(380px, 72vw);
  }
  .member-landing .landing-card[data-landing-card="hero"] .landing-wordmark-art {
    /* The name takes the full column. It is the widest thing on the page and
       now the piece the whole hero is centred on, so the column edge is the
       only cap that matters.

       The art gained width for free when lockup-ink.png was trimmed to its ink
       bounding box (it carried 97px of transparent margin per side, so the ink
       was only 88.8% of the image). wordmark-ink.png is cut from that trimmed
       source, so it inherits the same tight box.

       No margin: the two flex halves above and below own the spacing now, and
       a margin here would push the centre off true.

       90% rather than the full column: the same 0.9 the crest above takes, so
       the two marks shrink together and crest:wordmark holds. Applied HERE
       rather than by narrowing .landing-inner's max-width, because that column
       also sets the tagline, the number field and the CTA — shrinking it would
       scale the whole hero, not the lockup Patrick asked about. */
    width: 90%;
    max-width: 100%;
    margin: 0;
  }
  .member-landing .landing-card[data-landing-card="hero"] .landing-crest-link {
    /* Width-driven against the VIEWPORT, deliberately breaking out of the
       column. The mock's defining proportion is crest:wordmark = 0.71 — the
       crest is the dominant mark, not a small companion above the name. It was
       0.43. Sizing it by height inside the newly-narrowed column would have
       shrunk it along with the column and moved the ratio the wrong way; 50vw
       against the screen holds the ratio wherever the column lands.
       The portrait aspect (~0.72:1) turns that into ~31vh of height. */
    width: min(43.2vw, 23.4vh);   /* 0.9 of min(48vw, 26vh) */
    height: auto;
    max-width: none;
    margin: 0;
  }
  .member-landing .landing-card[data-landing-card="hero"] .landing-wordmark {
    font-size: 26px;
    line-height: 1;
    margin: 0 auto;
    padding: 8px 10px 6px;
  }
  .member-landing .landing-card[data-landing-card="hero"] .landing-field {
    min-height: 58px;
    margin-bottom: 0;
  }
  .member-landing .landing-card[data-landing-card="hero"] .auth-send,
  .member-landing .landing-card[data-landing-card="hero"] .auth-flip-mount {
    margin-top: 8px;
  }
}

/* Portrait phones. Pinned to the top, the lockup left the whole lower half
   of the screen dead — it read as crammed under the status bar, so the hero
   centres instead.

   It used to centre inside a reserved keyboard footprint, to keep the field
   above the keyboard without JS. That stopped being achievable once the
   lockup grew: the stack is now taller than the space above a ~300px keypad,
   so no reserve can fit it, and a half-sized one only bought a worse
   composition at rest. The reserve is gone; the field is allowed to sit
   under the keyboard and the browser scrolls it into view (see the :has()
   rule near .landing, which gives that scroll somewhere to go).

   Gate lowered from 800px to 620px: Mobile Safari's chrome shrinks the
   visual viewport on tall phones (844px devices report ~740-790px), which
   left exactly the phones this was built for stuck in the top-pinned
   branch. The compact override below (max-height: 700px) still scales the
   lockup down in the overlap band, so SE-height screens keep a layout that
   fits. */
@media (max-width: 720px) and (min-height: 620px) {
  .member-landing .landing-card[data-landing-card="hero"] {
    /* Was justify-content: center, which put ~140px of nothing above the
       overline on a 852px phone and pinned the art to a size the leftover
       space never got to use. The stack now sits high and the slack collects
       at the BOTTOM, where it reads as breathing room rather than as the
       lockup floating in the middle of the screen.

       Still centred rather than pinned to the top: flex-start dumps every
       spare pixel into one void beneath the field, which looks like a layout
       bug rather than a composition. Centring inside a box with a much taller
       bottom padding lifts the stack by half the difference — about 70px here
       — while the remaining slack still distributes the way it always did. */
    justify-content: center;
    padding-top: max(20px, env(safe-area-inset-top, 0px));
    /* Clears the absolutely-positioned fine print at the bottom of the card,
       and carries the upward bias described above. */
    padding-bottom: max(56px, calc(env(safe-area-inset-bottom, 0px) + 44px));
  }
}

/* The SE-height portrait and phone landscape need the same stable layout at a
   tighter scale. Nothing changes on focus; only the available screen decides. */
@media (max-width: 720px) and (max-height: 700px) {
  .member-landing .landing-card[data-landing-card="hero"] {
    padding-top: max(20px, env(safe-area-inset-top, 0px));
  }
  .member-landing .landing-card[data-landing-card="hero"] .landing-inner { gap: 8px; }
  .member-landing .landing-card[data-landing-card="hero"] .landing-wordmark-art {
    /* min(), not a bare px width. On a short AND narrow screen (320x568) the
       column is the binding constraint, so a flat 261px just clamped back to
       100% of it and the wordmark was the one mark that did NOT take the 0.9
       — crest:wordmark fell from 0.68 to 0.61. min() lets whichever limit is
       smaller win while keeping the factor applied to both. */
    width: min(261px, 90%);       /* 0.9 of 290px, 0.9 of the column */
    max-width: 100%;
    margin: 0;
  }
  .member-landing .landing-card[data-landing-card="hero"] .landing-wordmark {
    font-size: 23px;
    padding-top: 5px;
    padding-bottom: 4px;
  }
  /* SE-height screens have no slack: the standing phone prompt and the bigger
     overline together cost ~25px, which is exactly what tipped this branch
     into scrolling. Both scale down here rather than the layout giving up and
     scrolling — the whole point of this branch is that everything fits. */
  .member-landing .landing-card[data-landing-card="hero"] .landing-overline {
    /* 19px sat exactly on the wrap point once the column narrowed to 72vw
       (19 x 14.1 = 268px against a 270px column). One line is the whole joke. */
    font-size: 17px;
    margin-bottom: 0;
  }
  .member-landing .landing-card[data-landing-card="hero"] .landing-field-hint {
    font-size: 11px;
    letter-spacing: 0.1em;
    margin-top: 6px;
    margin-bottom: 0;
  }
}
.auth-flow #verify-mount { margin-top: 12px; }
.auth-flow #verify-mount:empty { display: none; }
/* Wider login column: the shared .landing-field caps at 320px and
   .landing-inner centers (doesn't stretch) its children, so the swipe read as
   narrow. Give the hero auth flow a definite, roomy width so the field + swipe
   run nearly the full card. */
.landing-card[data-landing-card="hero"] .auth-flow { width: 100%; max-width: 420px; }
.landing-card[data-landing-card="hero"] .auth-flow .landing-field {
  max-width: none; width: 100%; margin-left: 0; margin-right: 0;
}
.landing-card[data-landing-card="hero"] .auth-flow #verify-mount,
.landing-card[data-landing-card="hero"] .auth-flow .slide-action { width: 100%; }

/* Vertical dot rail (right edge). */
.landing-rail {
  position: fixed;
  right: 16px; top: 50%;
  transform: translateY(-50%);
  flex-direction: column;
  gap: 9px;
  z-index: 60;
}
.landing-rail .shell-dot { background: rgba(138, 85, 31, 0.30); }
.landing-rail .shell-dot.active { width: 7px; height: 22px; background: var(--copper, #B87333); }

/* "Where to" scroll cue nudging the visitor down from the hero. */
.landing-scroll-cue {
  position: absolute;
  bottom: 22px; left: 50%;
  transform: translateX(-50%);
  display: inline-flex; flex-direction: column; align-items: center; gap: 3px;
  background: none; border: 0; padding: 6px 10px; cursor: pointer;
  color: #5C3F26;
  font: 600 10px/1 var(--font-ui);
  letter-spacing: 0.24em; text-transform: uppercase;
  animation: landing-cue-bob 1.9s ease-in-out infinite;
}
.landing-scroll-cue svg { width: 20px; height: 20px; }
@keyframes landing-cue-bob {
  0%, 100% { transform: translateX(-50%) translateY(0); }
  50% { transform: translateX(-50%) translateY(5px); }
}
@media (pointer: coarse), (any-pointer: coarse) {
  .landing.landing-deck { touch-action: pan-y; }
}
@media (prefers-reduced-motion: reduce) {
  .landing.landing-deck { scroll-behavior: auto; }
  .landing-scroll-cue { animation: none; }
}

/* ── Ride Pass QR (member side) ───────────────────────────────────────── */
.ride-pass-wrap {
  display: flex;
  align-items: center;
  gap: 18px;
  padding: 8px 4px;
}
.ride-pass-qr {
  flex-shrink: 0;
  width: 160px;
  height: 160px;
  background: #fff;
  border-radius: 12px;
  padding: 10px;
  box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.ride-pass-qr canvas, .ride-pass-qr img {
  width: 100% !important;
  height: 100% !important;
}
.ride-pass-info {
  flex: 1;
  min-width: 0;
}
.ride-pass-name {
  font-family: var(--font-display);
  font-size: 20px;
  font-weight: 600;
  color: var(--cream, #2E2114);
  margin-bottom: 4px;
}
.ride-pass-num {
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.06em;
  color: var(--copper-mid, #B87333);
  margin-bottom: 8px;
}
.ride-pass-hint {
  font-size: 12px;
  color: var(--muted, #7F6A4F);
  line-height: 1.4;
}
@media (max-width: 480px) {
  .ride-pass-qr { width: 130px; height: 130px; }
  .ride-pass-name { font-size: 17px; }
}

/* ── QR Scanner overlay (caddie side) ─────────────────────────────────── */
.qr-scan-overlay {
  position: fixed;
  inset: 0;
  z-index: 9999;
  background: #1a1a1a;
  display: flex;
  flex-direction: column;
}
.qr-scan-inner {
  display: flex;
  flex-direction: column;
  height: 100%;
  max-width: 500px;
  margin: 0 auto;
  width: 100%;
}
.qr-scan-header {
  display: flex;
  align-items: center;
  gap: 12px;
  /* The overlay is inset:0 fixed, so it covers the status bar and the Dynamic
     Island too. Without the safe-area allowance Cancel sits underneath the
     island and cannot be tapped, which reads as the button being "off". */
  padding: calc(12px + var(--safe-top)) calc(16px + env(safe-area-inset-right, 0px)) 12px calc(16px + env(safe-area-inset-left, 0px));
  background: #2E2114;
  color: #FBF6EA;
  flex: 0 0 auto;
}
.qr-scan-title {
  font-weight: 600;
  font-size: 15px;
}
.qr-scan-video-wrap {
  flex: 1;
  position: relative;
  overflow: hidden;
  background: #000;
  display: flex;
  align-items: center;
  justify-content: center;
}
.qr-scan-video-wrap video {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.qr-scan-reticle {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  /* Square, and bounded on BOTH axes: 280px of a 393px screen left almost no
     camera visible around it. The vh term keeps it inside the frame when the
     phone is held sideways, where the wrap is short and wide. */
  width: min(58%, 220px, 38vh);
  aspect-ratio: 1;
  border: 3px solid rgba(228, 168, 126, 0.8);
  border-radius: 12px;
  box-shadow: 0 0 0 1000px rgba(0,0,0,0.4);
}
.qr-scan-hint {
  position: absolute;
  bottom: 20px;
  left: 50%;
  transform: translateX(-50%);
  color: #FBF6EA;
  font-size: 14px;
  font-weight: 500;
  text-align: center;
  background: rgba(46, 33, 20, 0.8);
  padding: 8px 16px;
  border-radius: 8px;
}
.qr-scan-fallback {
  padding: 16px 16px calc(16px + env(safe-area-inset-bottom, 0px));
  background: #F2E8D4;
  flex: 0 0 auto;
}
.qr-scan-fallback .input {
  flex: 1;
}

/* ---------- Private Cart opt-in (inside #estimate-card) ---------- */
/* A sentence, not a control that fits beside anything else -- a direct child
   of the card now rather than a flex sibling inside a rider row, so it is a
   normal block box: full card width, its own line, margin-left/flex-basis
   tricks for squeezing it into a row are gone with the row. display:flex
   here is purely internal (checkbox beside its label text), not for
   placement in the card. */
.private-cart-opt {
  display: flex; align-items: center; gap: 8px;
  margin-top: 10px;
  font-size: 12.5px; line-height: 1.35; cursor: pointer;
}
.private-cart-opt input[type="checkbox"] {
  accent-color: var(--copper, #B87333);
  width: 17px; height: 17px; flex: 0 0 auto; margin: 0;
}
.private-cart-opt strong { color: var(--copper-dark, #8A551F); font-weight: 700; }

/* ---------- Fare split: booker QR modal + joiner sheet ---------- */
.split-overlay {
  position: fixed; inset: 0; z-index: 4000;
  display: flex; align-items: center; justify-content: center;
  background: rgba(46, 33, 20, 0.55); backdrop-filter: blur(2px);
  padding: 20px;
}
.split-card {
  position: relative; width: 100%; max-width: 340px;
  background: var(--paper, #FBF6EA); border-radius: 14px;
  padding: 22px 20px; text-align: center;
  box-shadow: 0 18px 50px rgba(30, 24, 18, 0.35);
}
.split-card .split-sub { font-size: 12px; line-height: 1.5; margin: 0 0 12px; }
.split-close {
  position: absolute; top: 8px; right: 12px;
  background: none; border: none; font-size: 22px; line-height: 1;
  color: var(--copper-dark, #8A551F); cursor: pointer; padding: 4px;
}
.split-qr-wrap {
  display: flex; justify-content: center; padding: 10px;
  background: #fff; border-radius: 10px; margin: 0 auto 12px; width: fit-content;
  border: 1px solid rgba(46, 33, 20, 0.12);
}
.split-count { font-weight: 700; font-size: 14px; }
.split-names { font-size: 12px; margin-top: 4px; min-height: 15px; }
.split-card .link { display: inline-block; margin-top: 12px; font-size: 12px; }
.join-share { margin-top: 10px; display: flex; align-items: baseline; justify-content: center; gap: 8px; }
.join-amt {
  font-size: 34px; font-weight: 800; letter-spacing: -0.02em;
  color: var(--copper-dark, #8A551F); font-variant-numeric: tabular-nums;
}

/* ---------- Ride Pass consolidated onto the membership card ---------- */
.mc-pass {
  display: flex; align-items: center; gap: 16px;
  padding-top: 18px;
  border-top: 1px solid rgba(212, 152, 107, 0.22);
}
.mc-pass .ride-pass-qr {
  width: 118px; height: 118px; padding: 8px; border-radius: 10px;
  border: 1px solid rgba(212, 152, 107, 0.4);
  box-shadow: 0 8px 20px rgba(20, 12, 4, 0.5);
}
.mc-pass-cap { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.mc-pass-hint { font-size: 12px; line-height: 1.45; color: #C9B18F; }

/* ---------- Fleet safety: driver alarm banner + inspection rows ---------- */
.safety-alarm {
  position: fixed; top: 0; left: 0; right: 0; z-index: 5000;
  background: #B3261E; color: #fff;
  font: 700 14px/1.3 var(--font-ui);
  letter-spacing: 0.04em; text-align: center;
  padding: 12px 16px; box-shadow: 0 6px 18px rgba(0,0,0,0.35);
}
.insp-row {
  display: flex; align-items: flex-start; gap: 10px;
  padding: 8px 0; border-bottom: 1px solid rgba(46, 33, 20, 0.1);
  font-size: 13px; line-height: 1.4; cursor: pointer;
}
.insp-row:last-child { border-bottom: none; }
.insp-row input[type="checkbox"] {
  accent-color: var(--copper, #B87333);
  width: 18px; height: 18px; flex: 0 0 auto; margin-top: 1px;
}

/* ── Flip toggle ───────────────────────────────────────────────────────────
   A two-state control that turns over to show its other side. Real depth:
   perspective on the shell, preserve-3d on the turning element, and both faces
   backface-hidden — a bare rotateX with no perspective reads as a vertical
   mirror, not a flip.

   The two faces share one grid cell, so the control is as wide as the LONGER
   label and holds that width through the turn instead of resizing mid-flip. */
.flip-toggle {
  display: inline-block;
  perspective: 700px;
  border: 0;
  padding: 0;
  background: none;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: transform 0.18s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.flip-toggle:not(.disabled):hover { transform: scale(1.04); }
.flip-toggle:not(.disabled):active { transform: scale(0.95); }
.flip-toggle-inner {
  display: grid;
  transform-style: preserve-3d;
  transition: transform 0.6s cubic-bezier(0.22, 1.10, 0.36, 1);
}
.flip-toggle.on .flip-toggle-inner { transform: rotateX(180deg); }
.flip-toggle-face {
  grid-area: 1 / 1;               /* both faces stack in the same cell */
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  padding: 10px 20px;
  border-radius: 999px;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
  font-family: var(--font-ui);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  white-space: nowrap;
  /* Off face: the quiet, stone-toned side. */
  background: rgba(184, 115, 51, 0.10);
  border: 1px solid rgba(184, 115, 51, 0.28);
  color: var(--copper-dark, #8A5A28);
}
.flip-toggle-face.back {
  transform: rotateX(180deg);
  /* On face: copper, the app's "this is live" colour. */
  background: var(--copper, #B87333);
  border-color: var(--copper, #B87333);
  color: #FBF4E6;
}
.flip-toggle-hint { font-size: 13px; opacity: 0.55; letter-spacing: 0; }
.flip-toggle.disabled { cursor: not-allowed; opacity: 0.45; filter: saturate(0.5); }
.flip-toggle.busy { cursor: progress; }
.flip-toggle:focus-visible { outline: none; }
.flip-toggle:focus-visible .flip-toggle-face { box-shadow: 0 0 0 3px rgba(184, 115, 51, 0.30); }

/* Inert variant — a label that turns, driven by a wrapping control that owns
   the click (the ride sheet handle). No hover/press scaling of its own.
   
   IT DOES NOT ROTATE IN 3D, and that is the fix for what Patrick photographed
   on 2026-08-20 ("also this font is distorted"). Reproduced in Chromium at
   DPR 4: the settled states are crisp and the MID-FLIP frame is jagged and
   foreshortened. A compositor rasterises the layer once and scales that
   bitmap through the rotation, and sub-pixel antialiasing is dropped inside a
   3D transform -- so 10px uppercase text with 0.12em tracking spends 600ms
   visibly degrading.
   
   The button variants keep the rotation: they have a pill, a border and a
   colour change, so the turn reads as an object with two sides. This variant
   has none of that -- no background, no border, `color: inherit` -- so the
   3D bought nothing at all here except the artifact. A vertical slide reads
   as the same gesture (one label leaving, the other arriving) and stays
   text, so it renders at full quality the whole way. */
.flip-toggle.inline {
  perspective: none;
  cursor: inherit;
  overflow: hidden;          /* clips the face waiting its turn below */
}
.flip-toggle.inline .flip-toggle-inner {
  transform-style: flat;     /* no 3D context, so nothing gets rasterised */
  transition: transform 0.34s cubic-bezier(0.22, 1.10, 0.36, 1);
}
.flip-toggle.inline .flip-toggle-face.back { transform: translateY(100%); }
.flip-toggle.inline.on .flip-toggle-inner { transform: translateY(-100%); }
.flip-toggle.inline:hover, .flip-toggle.inline:active { transform: none; }
.flip-toggle.inline .flip-toggle-face {
  padding: 0;
  border: 0;
  background: none;
  border-radius: 0;
  font-size: inherit;
  font-weight: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  color: inherit;
}

/* Duty variant — reads at a glance from a moving cart, so it runs larger and
   states its own status rather than relying on a neighbouring label. */
.flip-toggle.duty .flip-toggle-face { padding: 11px 22px; font-size: 12px; }

@media (prefers-reduced-motion: reduce) {
  .flip-toggle, .flip-toggle-inner { transition: none; }
  .flip-toggle:hover, .flip-toggle:active { transform: none; }
}

/* Ride music — Member journey screen (Club picks, then search) */
.ride-music {
  margin-top: 12px;
  padding: 4px 0 12px;
}
.ride-music-msg {
  font-size: 13px;
  margin: 4px 0 10px;
}
.ride-music-hero {
  display: grid;
  grid-template-columns: 72px 1fr;
  grid-template-areas:
    "art copy"
    "progress progress"
    "times times"
    "controls controls";
  gap: 8px 12px;
  margin-bottom: 16px;
}
.ride-music-art {
  grid-area: art;
  width: 72px;
  height: 72px;
  object-fit: cover;
  border-radius: 6px;
  background: var(--accent-soft, rgba(184, 115, 51, 0.14));
}
.ride-music-copy { grid-area: copy; min-width: 0; align-self: center; }
.ride-music-title {
  font-size: 16px;
  font-weight: 500;
  color: var(--ink, #2E2114);
  line-height: 1.25;
}
.ride-music-artists {
  font-size: 13px;
  margin-top: 2px;
}
.ride-music-progress {
  grid-area: progress;
  height: 4px;
  border-radius: 2px;
  background: var(--border, rgba(127, 106, 79, 0.34));
  overflow: hidden;
}
.ride-music-bar {
  height: 100%;
  width: 0;
  background: var(--accent, #B87333);
  border-radius: 2px;
}
.ride-music-times {
  grid-area: times;
  display: flex;
  justify-content: space-between;
  font-size: 11px;
}
.ride-music-controls {
  grid-area: controls;
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 20px;
  padding: 4px 0;
}
.ride-music-btn {
  background: none;
  border: none;
  color: var(--accent, #B87333);
  cursor: pointer;
  padding: 8px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
}
.ride-music-btn:active {
  background-color: var(--accent-soft, rgba(184, 115, 51, 0.14));
}
.ride-music-btn:disabled {
  opacity: 0.35;
  cursor: not-allowed;
}
.ride-music.is-locked .ride-music-picks,
.ride-music.is-locked .ride-music-search,
.ride-music.is-locked .ride-music-results {
  opacity: 0.45;
  pointer-events: none;
}
.ride-music-picks {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: 8px 0 16px;
}
.ride-music-chip {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
  border: 1px solid var(--border, rgba(127, 106, 79, 0.34));
  background: transparent;
  color: inherit;
  border-radius: 999px;
  padding: 8px 14px;
  cursor: pointer;
  text-align: left;
  max-width: 100%;
}
.ride-music-chip:active { background: var(--accent-soft, rgba(184, 115, 51, 0.14)); }
.ride-music-chip-name {
  font-size: 13px;
  font-weight: 500;
  color: var(--ink, #2E2114);
}
.ride-music-chip-artist {
  font-size: 11px;
  color: var(--muted, #6E5A40);
}
.ride-music-search {
  width: 100%;
  box-sizing: border-box;
  padding: 10px 12px;
  border: 1px solid var(--border, rgba(127, 106, 79, 0.34));
  border-radius: 8px;
  background: transparent;
  color: inherit;
  font-size: 16px;
}
.ride-music-results { margin-top: 8px; }
.ride-music-result {
  display: flex;
  width: 100%;
  gap: 10px;
  align-items: center;
  padding: 8px 0;
  border: none;
  border-bottom: 1px solid var(--border, rgba(127, 106, 79, 0.18));
  background: none;
  color: inherit;
  text-align: left;
  cursor: pointer;
}
.ride-music-result-art {
  width: 40px;
  height: 40px;
  border-radius: 4px;
  object-fit: cover;
  background: var(--accent-soft, rgba(184, 115, 51, 0.14));
  flex-shrink: 0;
}

/* Caddie portal cart-speaker panel */
.caddie-music-panel,
.caddie-aux-panel {
  border-top: 1px solid var(--border, rgba(127, 106, 79, 0.34));
  padding-top: 8px;
}
.caddie-aux-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 4px 0 8px;
}
.caddie-aux-track-name {
  font-size: 13px;
  font-weight: 500;
  color: var(--ink, #2E2114);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 160px;
}
.caddie-aux-artist {
  font-size: 11px;
  color: var(--muted, #6E5A40);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 120px;
}

/* Allowance line on the estimate card. Reads as a receipt line, not a promo:
   the Member needs to know what the journey spends before they commit, and —
   because there are no overage fees and no legs for sale — that running out is
   a real wall rather than a surcharge. Quiet until it is the last leg, then
   copper; .is-out carries the upgrade, which is the only way past it. */
.est-allowance {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  margin-top: 10px;
  font-size: 12.5px;
  /* Same surface-token-as-text bug as .tip-note above: 1.23:1 on the cream
     card. It hid specifically the RESTING state -- .is-last and .is-out below
     both repaint to --accent -- so the allowance counter was unreadable right
     up until the leg where it turns copper to warn you. --muted is 6.0:1. */
  color: var(--muted);
  letter-spacing: 0.01em;
}
.est-allowance.is-last { color: var(--accent); }
.est-allowance.is-out  { color: var(--accent); font-weight: 600; }
.est-allowance .btn { flex: 0 0 auto; padding: 7px 14px; font-size: 12.5px; min-height: 0; }
/* Let the button drop to its own line rather than squeezing the sentence.
   The upgrade label carries a price, so it is ~200px wide and immovable
   (flex: 0 0 auto above); on a 393px phone that left the text about 160px and
   wrapped "No journeys left this month — unlimited is the way to keep riding"
   into five ragged lines beside it. The message is the part a Member has to
   read, so it gets the width and the button follows underneath. */
.est-allowance { flex-wrap: wrap; }
.est-allowance > span { flex: 1 1 auto; min-width: 0; }
.est-allowance.is-out > span,
.est-allowance.is-last > span { flex: 1 1 100%; }
.est-allowance.is-out .btn,
.est-allowance.is-last .btn { margin-left: auto; }

/* ---- The Concierge desk at desk width -----------------------------------
   The Club House wall below turns its deck into a grid of every panel at once.
   That is right for five panels a Host glances across; it is wrong for the
   desk's twelve, one of which is a full-bleed Leaflet canvas and several of
   which are heavy tables. A console keeps its navigation and shows ONE thing.

   So the deck simply stops being a deck: the tab bar was always the real
   navigation on a desktop, the active panel fills the room and scrolls itself,
   and swipe-shell is never attached up here (see initAdminDeck).

   That last part is the bug this fixes. swipe-shell's wheel-tracker steals
   vertical wheel events to page sideways, declining only when the active panel
   can still scroll itself — and at this width the panels grew to fit their
   content and the PAGE became the scroller, so the check read false and every
   scroll was eaten. The Insurance section at the bottom of Licensing could not
   be reached with a mouse at all. -------------------------------------- */
@media (min-width: 1024px) {
  /* min-height:0 all the way down, or the panel does not scroll — the PAGE
     does. A grid item defaults to min-height:auto and refuses to shrink below
     its content, so .main grew to fit twelve panels, the 1fr row grew with it,
     and .app-shell overflowed its own 100dvh. It still scrolled, but the wrong
     thing did: the tab bar rode up and off with the content, which is not what
     a console does. Same defect, same fix, as .caddie-feed. */
  .desk-body .layout,
  .desk-body .main { min-height: 0; }

  .desk-body .shell-deck {
    display: block;
    overflow: hidden;              /* the PANEL scrolls, not the page */
    scroll-snap-type: none;
    height: 100%;
    min-height: 0;
  }
  /* Every panel keeps its markup; only the active one is in the room. */
  .desk-body .shell-deck > .tab-content { display: none; }
  .desk-body .shell-deck > .tab-content.is-active {
    display: block;
    flex: initial;
    width: auto;
    height: 100%;
    min-height: 0;
    margin: 0;
    /* The bead is a DECK idea — breathing room around a card you swipe
       between. There is nothing to swipe between here. */
    scroll-snap-align: none;
    scroll-snap-stop: normal;
    overflow-y: auto;
    overscroll-behavior-y: contain;
  }
  /* The pager is orientation for a gesture that does not exist up here. */
  .desk-body .swipe-dots,
  .desk-body .shell-dot { display: none; }
}

/* ============================================================
   DESK — the portals at desktop width (Phase 5)
   ============================================================
   Patrick, 2026-08-20: "we also need a desktop version for when a Venue,
   Concierge, Admin, or Insurance Broker wants to log in and see activity."

   The operative words are SEE ACTIVITY. Desktop is not the phone job done at
   a desk; it is the job a phone cannot do — breadth at a glance. So the rule
   here is not "stretch the column", it is "stop hiding things".

   Every portal is built on .member-stack, which caps itself at 520px and
   centres. On a 27" screen that is a phone-shaped ribbon down the middle with
   two feet of empty sandstone either side, and on the Host and Parent portals
   four of the five panels are behind a SWIPE — a gesture a mouse does not
   have. The shell exposes arrow keys and a wheel-tracker, so the panels are
   reachable, but "reachable by discovering a keyboard shortcut" is not the
   same as visible.

   1024px, not the 900px the Caddie iPad flagship uses. 900 is tablet
   landscape, where the two-column split is right and this wall would be
   cramped; 1024 is a laptop. Below this breakpoint NOTHING here applies and
   every portal is byte-for-byte what it was.
   ============================================================ */
@media (min-width: 1024px) {
  /* The ribbon becomes the room -- but a room, not a field. The 520px cap goes;
     a 1600px one replaces it, because "use the whole monitor" stops being a
     virtue somewhere around 1700px. Measured on a 1920 screen with no cap: the
     blotter ran 1134px wide and threw each timestamp so far from the line it
     belonged to that pairing them became work. Capped, the centre column lands
     at 814px on a 1920 screen and 708 on a 1440 -- close enough that the panel
     reads the same on both. */
  .member-stack.deck-stack.ch-desk {
    max-width: 1600px;
    margin-inline: auto;
    height: 100%;
    min-height: 0;
    padding: 16px 22px 18px;
    gap: 16px;
    overflow: hidden;              /* the PANELS scroll, not the page */
  }

  /* The pager row is orientation for a gesture that no longer exists here.
     Title, dots and the "Swipe" hint all go — each panel prints its own name
     below. (JS clears the inline display:none on this element, so a plain
     rule wins; no !important needed.) */
  .deck-stack.ch-desk .swipe-deck-head { display: none; }

  /* The deck stops being a deck. display:contents dissolves the scroll-snap
     container so its .section children land directly in the stack's grid --
     no markup change, and the mobile deck is untouched below 1024px. */
  .deck-stack.ch-desk .shell-deck {
    display: contents;
  }
  .deck-stack.ch-desk .shell-deck > .section {
    flex: initial;
    height: auto;
    min-height: 0;
    /* The bead is a DECK idea: breathing room around a card you swipe between.
       These are grid cells now, spaced by the stack's own gap, so the shared
       --deck-bead margin has to come all the way off — leaving just the left
       one would indent every panel by 20px against a gap that already exists. */
    margin: 0;
    scroll-snap-align: none;
    scroll-snap-stop: normal;
    overflow-y: auto;
    overscroll-behavior-y: contain;
  }

  /* Panels were bare strips stacked in a column; side by side they need to
     read as separate objects. Card vocabulary borrowed from .ch-venue-hero so
     the wall is one material. */
  .deck-stack.ch-desk .shell-deck > .section {
    background: var(--panel);
    border: 1px solid var(--border-2);
    border-radius: var(--radius-md);
    padding: 14px 18px 16px;
  }

  /* The deck head printed the active panel's name. With it gone, each panel
     prints its own -- from the SAME data-deck-title attribute, so the two can
     never drift apart the way a duplicated <h3> would. */
  .deck-stack.ch-desk .shell-deck > .section[data-deck-title]::before {
    content: attr(data-deck-title);
    display: block;
    font-family: var(--font-ui);
    font-size: 11px;
    font-weight: 800;
    letter-spacing: 0.22em;
    text-transform: uppercase;
    color: #14110a;
    margin: 0 0 10px;
  }

  /* Panel scrollbars: visible on desktop, because a mouse user needs to know
     a column has more in it. Thin and sandstone rather than the OS default,
     which paints a bright grey slab against the paper. */
  .deck-stack.ch-desk .shell-deck > .section { scrollbar-width: thin; scrollbar-color: var(--border-2) transparent; }
  .deck-stack.ch-desk .shell-deck > .section::-webkit-scrollbar { display: block; width: 8px; }
  .deck-stack.ch-desk .shell-deck > .section::-webkit-scrollbar-thumb {
    background: var(--border-2); border-radius: 999px;
    border: 2px solid transparent; background-clip: content-box;
  }
  .deck-stack.ch-desk .shell-deck > .section::-webkit-scrollbar-track { background: transparent; }

  /* The shell's edge chevrons page a deck that is no longer paged. */
  .deck-stack.ch-desk .shell-edge { display: none; }
}

/* ---- Venue (Host) desk -----------------------------------------------------
   Patrick's ask, in his order: "a clean logo at the top, how many carts are
   inbound / running total of the day and then below that the blotter." On a
   phone that is a vertical reading order. On a wall it becomes a left-to-right
   one, because the three things a Host does are not equally urgent:

     LEFT   who we are + the two numbers, then the 30-day picture.
            Identity and state. Read once, glanced at after.
     CENTER the blotter, floor to ceiling. It is the only panel that CHANGES
            while you watch it, so it gets the tallest column and the eye.
     RIGHT  the two things a Host DOES -- send a Caddie, push an offer.
            A rail, always loaded, never behind a swipe.

   Nothing is hidden. That is the whole point of the format.
   --------------------------------------------------------------------------- */
@media (min-width: 1024px) {
  #app .member-stack.deck-stack.ch-desk {
    display: grid;
    /* Percentage rails with hard stops at both ends, NOT minmax(px, px).
       minmax lets a rail take its max whenever there is room, which at 1024
       gave the two rails 710px between them and left the blotter -- the panel
       someone opened a laptop to watch -- as the NARROWEST column on screen,
       wrapping every line to two. Rails now yield first and the centre keeps
       the surplus: 428px at 1024, 687 at 1440, 1134 at 1920. */
    grid-template-columns:
      clamp(264px, 22%, 330px)
      minmax(0, 1fr)
      clamp(280px, 25%, 380px);
    /* Three rows, not two, so the two rails can divide their height
       DIFFERENTLY. Row 1 is auto and belongs to the hero: it is the one panel
       that clips (overflow:hidden, for the mobile collapse), so it must be
       sized by its content and never by a share of the viewport -- a 1fr row
       four pixels short would eat the venue's logo exactly the way the flex
       stack did. Rows 2 and 3 are equal fractions everything else divides.
       Send a Caddie takes rows 1-2 and Pulse takes row 3, in that proportion,
       because the first is the commonest action of a shift and the second is a
       lever pulled once or twice a night. A two-row grid forced both rails to
       split at the same line and handed the booking form 190px. */
    grid-template-rows: auto minmax(160px, 1fr) minmax(160px, 1fr);
    align-content: stretch;
  }

  /* The hero keeps its own card treatment and never scrolls -- it is identity,
     and identity that scrolls away is just a header. */
  #app .deck-stack.ch-desk #ch-venue-hero {
    grid-column: 1; grid-row: 1;
    margin-bottom: 0;
    max-height: none;              /* the mobile collapse ceiling is moot here */
  }
  /* .hero-away is the swipe-deck's doing: the hero slid out so panels 2-5 got
     the full phone screen. There is no swipe here and the hero has its own
     column, so the class must never take effect -- the deck is not initialised
     at this width, but a resize across the breakpoint can leave it behind. */
  #app .deck-stack.ch-desk #ch-venue-hero.hero-away {
    max-height: none; opacity: 1; border-width: 1px;
    padding: 18px; margin-bottom: 0;
  }

  #app .deck-stack.ch-desk #numbers-section  { grid-column: 1; grid-row: 2 / span 2; }
  #app .deck-stack.ch-desk #inbound-section  { grid-column: 2; grid-row: 1 / span 3; }
  /* Book and confirm share one cell: the JS shows exactly one of them, so they
     occupy the same space rather than the confirm panel shoving the rail. */
  #app .deck-stack.ch-desk #book-panel,
  #app .deck-stack.ch-desk #confirm-panel    { grid-column: 3; grid-row: 1 / span 2; }
  #app .deck-stack.ch-desk #pulse-section    { grid-column: 3; grid-row: 3; }

  /* The blotter is the reason someone opened this on a big screen. Give the
     rows room to breathe now that there is height to spend. */
  #app .deck-stack.ch-desk #inbound-section .list-item { padding-top: 11px; padding-bottom: 11px; }

  /* The venue wordmark sizes off the VIEWPORT on mobile (8vw), which is right
     when the column IS the viewport and wrong the moment it is a 264px rail:
     at 1024 the vw term pins it to its 40px ceiling and "Hearth & Rye" breaks
     across two lines in a column a third that wide. Track the rail instead. */
  #app .deck-stack.ch-desk .ch-venue-wordmark { font-size: clamp(21px, 2.1vw, 32px); }
}

/* ---- Concierge / Admin desk ------------------------------------------------
   This portal already had a desktop layout -- a 400px sidebar beside the tab
   deck -- so it did not need a wall. It needed its NAVIGATION back.

   Fourteen sections live in a horizontal .tab-bar. That bar sets
   scrollbar-width:none, which is right on a phone (you swipe, and the dot
   pager underneath tells you how many there are) and silently wrong with a
   mouse: the dot pager is display:none from 900px up, so at 1440 the strip
   shows SEVEN tabs, cuts "Verifications" off mid-word at the right edge, and
   offers no scrollbar, no chevron, and no dots. Partners, Brand Studio,
   Licensing, Journeys, Phases & carriage and Safety are not merely hard to
   reach on the widest screens we serve -- there is nothing on the page that
   says they exist.

   Vertically, fourteen items are a short list. So above 1024 the strip stops
   floating over the map and becomes a rail down the left of .main: every
   section named, none clipped, the active one marked by a filled block rather
   than an underline (underlines read as tabs, and this is no longer a tab bar).
   --------------------------------------------------------------------------- */
@media (min-width: 1024px) {
  .layout .main {
    display: grid;
    grid-template-columns: 196px minmax(0, 1fr);
    grid-template-rows: 100%;
  }

  /* Was position:absolute over the fleet map. It is a column now, so it takes
     a real grid cell and stops overlapping what it navigates. */
  .adm-tabstrip-float {
    position: static;
    grid-column: 1; grid-row: 1;
    display: flex;                /* so the strip can fill the rail's height */
    padding: 12px 10px 12px 12px;
    min-height: 0;
  }
  .adm-tabstrip-float .tab-bar {
    flex: 1 1 auto;               /* a rail that stops halfway down reads as a
                                     floating card someone forgot to finish */
    flex-direction: column;
    align-items: stretch;
    overflow-x: visible;
    overflow-y: auto;
    border-bottom: 0;
    margin-bottom: 0;
    gap: 2px;
    padding: 6px;
    scrollbar-width: thin;
    scrollbar-color: rgba(196, 132, 74, 0.45) transparent;
  }
  .adm-tabstrip-float .tab-bar .tab {
    text-align: left;
    padding: 8px 11px;
    border-bottom: 0;
    border-radius: 7px;
    letter-spacing: 0.14em;   /* 0.25em is a strip track; it wraps a 196px rail */
    white-space: normal;      /* "Phases & carriage" may take two lines */
    line-height: 1.35;
  }
  /* An underline under a full-width row reads as a divider, not a selection.
     Fill the row instead. */
  .adm-tabstrip-float .tab-bar .tab.active {
    background: rgba(196, 132, 74, 0.22);
    box-shadow: inset 2px 0 0 #E8B87A;
    border-bottom-color: transparent;
  }
  .adm-tabstrip-float .tab-bar .tab:hover { background: rgba(196, 132, 74, 0.08); }
  .adm-tabstrip-float .tab-bar .tab.active:hover { background: rgba(196, 132, 74, 0.2); }

  #admin-deck { grid-column: 2; grid-row: 1; }

  /* The sidebar and the new rail together take 596px. At 1024 that leaves the
     panel a 428px slot, which is narrower than the dashboard beside it -- so
     the sidebar yields until there is width to spend. */
  .layout { grid-template-columns: clamp(280px, 28%, 400px) 1fr; }
}

/* ============================================================
   Member booking: greeting collapse + origin picker
   ============================================================ */

/* Patrick, 2026-08-20: "Once a venue is selected, there's no need for the
   salutation line."
   The greeting and the booking panel compete for the same phone screen.
   "Good morning, Patrick." earns its space while the Member is deciding;
   the moment they have named a destination they are executing, and two
   lines of pleasantry push the rider count and the slide-to-send toward
   the fold. Collapsed rather than removed, so clearing the destination
   brings it back as a movement instead of a jump. */
.home-greeting {
  overflow: hidden;
  max-height: 200px;
  opacity: 1;
  transition: max-height 0.28s var(--ease, ease), opacity 0.18s var(--ease, ease),
              margin-bottom 0.28s var(--ease, ease);
}
.home-greeting.greeting-collapsed {
  max-height: 0;
  opacity: 0;
  margin-bottom: 0;
  /* .home-greeting carries `padding: 2px 4px 0`, which survives max-height:0
     and leaves a 2px ghost row. Measured, not guessed. */
  padding-top: 0;
  padding-bottom: 0;
}
@media (prefers-reduced-motion: reduce) {
  .home-greeting { transition: none; }
}

/* ---- Origin picker --------------------------------------------------------
   Reuses .modal-container / .modal-content from the destination modal so the
   two pickers are the same object to a Member -- only the contents differ. */

/* The best guess sits above the lists and is styled as the primary action,
   because it is right most of the time and every other row is a correction. */
.origin-here {
  display: flex;
  flex-direction: column;
  gap: 3px;
  width: 100%;
  text-align: left;
  padding: 13px 15px;
  margin-bottom: 16px;
  border: 1px solid var(--accent);
  border-radius: var(--radius-md);
  background: var(--accent-soft);
  cursor: pointer;
}
.origin-here[disabled] {
  cursor: default;
  opacity: 0.55;
  border-color: var(--border-2);
  background: transparent;
}
.origin-here-eyebrow {
  font-family: var(--font-ui);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--accent);
}
.origin-here[disabled] .origin-here-eyebrow { color: var(--muted); }
.origin-here-label {
  font-family: var(--font-ui);
  font-size: 15px;
  line-height: 1.3;
  color: var(--cream);
}

.origin-group + .origin-group { margin-top: 18px; }
.origin-group-title {
  font-family: var(--font-ui);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--muted);
  margin: 0 0 8px;
}
.origin-list { display: flex; flex-direction: column; }
.origin-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  width: 100%;
  text-align: left;
  padding: 11px 2px;
  border: 0;
  border-bottom: 1px solid var(--border-2);
  background: transparent;
  cursor: pointer;
}
.origin-row:last-child { border-bottom: 0; }
.origin-row-label {
  font-family: var(--font-ui);
  font-size: 14px;
  color: var(--cream);
  /* Takes the row. min-width:0 is what actually lets a flex child shrink
     below its content, without which the ellipsis below never triggers. */
  flex: 1 1 auto;
  min-width: 0;
}
/* The annotation is the index, not the news -- muted, and never allowed to
   wrap the address it annotates.
   That was always the intent and the old rule protected the wrong element:
   `flex: 0 0 auto` made the SUB rigid, so a long value starved the label and
   the ADDRESS wrapped one word per line instead. Seen for real when a county
   parcel description ("PARCEL T UNIT 1 AT MCDOWELL MOUNTAIN RANCH AMD")
   reached this slot. The sub now yields and truncates; the address wins. */
.origin-row-sub {
  font-family: var(--font-ui);
  font-size: 12px;
  color: var(--muted);
  white-space: nowrap;
  flex: 0 1 auto;
  min-width: 0;
  max-width: 42%;
  overflow: hidden;
  text-overflow: ellipsis;
}
.origin-empty {
  font-family: var(--font-ui);
  font-size: 13px;
  color: var(--muted);
  padding: 10px 2px;
}
#origin-search { margin-bottom: 6px; }

/* The second kind of destination. A Club House gets the cover flow above it --
   it has artwork, a menu and a name worth showing large. A residence is a line
   of text, so it opens the plain place picker instead of being forced into a
   cover with nothing on it. Quiet, because picking a Club House is still the
   common case and this must not compete with it. */
.dest-elsewhere {
  display: block;
  width: 100%;
  margin-top: 14px;
  padding: 11px 12px;
  border: 1px solid var(--border-2);
  border-radius: var(--radius-md);
  background: transparent;
  font-family: var(--font-ui);
  font-size: 13px;
  letter-spacing: 0.02em;
  color: var(--muted);
  cursor: pointer;
}
.dest-elsewhere:hover { color: var(--cream); border-color: var(--accent); }

/* ---- Journey sheet --------------------------------------------------------
   The Locker's history rows open this. Reuses .modal-container /
   .modal-content and the .tip-btn set from the ride sheet, so a gratuity
   added a day later is the same object as one added at the kerb. */
.jr-when {
  font-family: var(--font-ui);
  font-size: 12px;
  letter-spacing: 0.04em;
  color: var(--muted);
  margin-bottom: 10px;
}
.jr-caddie {
  font-family: var(--font-ui);
  font-size: 14px;
  color: var(--cream);
  padding-bottom: 12px;
  margin-bottom: 4px;
  border-bottom: 1px solid var(--border-2);
}
.jr-caddie span {
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--muted);
  display: block;
  margin-bottom: 3px;
}
.jr-loading,
.jr-note {
  font-family: var(--font-ui);
  font-size: 13px;
  line-height: 1.5;
  color: var(--muted);
  padding: 12px 0 2px;
}
/* The close-out warning is the one note that costs money to ignore, so it
   reads as a caution rather than an aside. */
#journey-closeout-note {
  color: var(--cream);
  background: var(--accent-soft);
  border: 1px solid var(--accent);
  border-radius: var(--radius-md);
  padding: 10px 12px;
  margin-top: 8px;
}
#journey-tip .label { margin-top: 14px; }
/* .modal-container centres its content and neither element scrolls, so a
   sheet taller than the viewport is clipped at BOTH ends with no way to
   reach either. The journey sheet is the first one that gets tall enough to
   hit it: measured at 320x568 (iPhone SE), the close-out state ran 591px and
   sat at top:-12 with its charge button 8px off the bottom edge. Scoped to
   this sheet -- the two pickers are short and their centring is correct. */
#journey-modal .modal-content {
  max-height: calc(100dvh - 32px);
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}
