/* ============================================================
   ActivGate — Detail System (ds-)
   Foundation stylesheet for the six service-detail page families:
   activities, tours, stays, cars, transfers, flights.

   Visual source of truth: the approved mockup stylesheet at
   docs/superpowers/mockups/2026-07-28-services-detail/assets/gloss.css
   Every primitive below mirrors that file's look 1:1, but reads its
   colors from the production tokens in static/css/design-tokens.css
   via the --ds-* bridge just below, so no brand hex is hardcoded
   past this comment block (a token-sourced sheet survives future
   palette changes; the mockup's own hex values never appear here).

   Plain CSS only — loaded by an ordinary <link> tag, not a bundler:
   no @import of other local files, no preprocessor syntax.

   Comment discipline: never close a comment early. A previous sheet
   had a comment that mentioned a Bootstrap icon class name ending in
   the two characters that close a CSS comment, which truncated the
   file and silently dropped roughly 85% of its rules in Chrome. Read
   every comment you add here back before committing, character by
   character, for that exact two-character sequence (star, slash).
   ============================================================ */

/* ---------- token bridge (--ds-* reading --ag-* design tokens) ---------- */
:root {
  /* brand + text ink (sourced from --ag-color-*; see design-tokens.css) */
  --ds-navy: var(--ag-color-brand-secondary);
  --ds-blue: var(--ag-color-brand-primary);
  --ds-sand: var(--ag-color-brand-accent);
  --ds-cta1: var(--ag-color-brand-primary-hover);
  --ds-ink: var(--ag-color-text-body);
  --ds-ink-2: var(--ag-color-text-muted);
  --ds-ink-3: var(--ag-color-text-subtle);
  --ds-on-primary: var(--ag-color-text-on-primary);

  /* surfaces */
  --ds-mist: var(--ag-color-bg-page-alt);
  --ds-card-bg: var(--ag-color-bg-surface);

  /* brand-blue translucent borders/tints — no solid --ag-* token matches
     these exact alphas, so they're expressed as rgba() decomposed from
     --ds-blue (#2E74B8 = rgb(46,116,184)) rather than a second hardcoded hex */
  --ds-line: rgba(46, 116, 184, 0.14);
  --ds-line-soft: rgba(46, 116, 184, 0.09);
  --ds-tint: rgba(46, 116, 184, 0.07);

  /* semantic */
  --ds-ok: var(--ag-color-success);
  --ds-ok-soft: var(--ag-color-success-softer);
  --ds-ok-ink: var(--ag-color-success-hover);
  --ds-warn: var(--ag-color-warning);
  --ds-warn-soft: var(--ag-color-warning-softer);
  --ds-bad: var(--ag-color-error);
  /* every other semantic color already has a -soft tint (ok/warn); bad
     never needed one until W1-T12's "What's included" icon list, which
     needs a light error-tint background for its ✕ icon (mockup's own
     .ic--no rule: background:#fdecec -- a raw hex with no --ds-* home).
     --ag-color-error-softer (design-tokens.css) is the closest existing
     semantic token to that exact mockup value. */
  --ds-bad-soft: var(--ag-color-error-softer);
  --ds-gold: var(--ag-color-rating-fill);
  --ds-gold-soft: var(--ag-color-warning-softer);
  --ds-gold-ink: var(--ag-color-warning-hover);

  /* gradients */
  --ds-grad-cta: var(--ag-gradient-button);
  --ds-grad-horizon: var(--ag-gradient-brand);
  --ds-grad-dusk: linear-gradient(180deg, rgba(30, 40, 74, 0) 40%, rgba(30, 40, 74, .55) 100%);

  /* shadows — brand-tinted glow (navy #1E284A = rgb(30,40,74); cta1 = rgb(37,99,235)),
     kept as multi-layer rgba() to match the approved look; a single-token
     --ag-shadow-* would flatten this to a generic slate shadow */
  --ds-shadow-1: 0 1px 2px rgba(30, 40, 74, .05), 0 4px 14px rgba(46, 116, 184, .08);
  --ds-shadow-2: 0 2px 6px rgba(30, 40, 74, .08), 0 14px 34px rgba(46, 116, 184, .14);
  --ds-shadow-rail: 0 6px 18px rgba(37, 99, 235, .16), 0 22px 48px rgba(30, 40, 74, .16);
  /* --ds-shadow-focus: a plain focus RING, not a card elevation shadow --
     unlike --ds-shadow-1/2/rail above (deliberately flattened multi-layer
     copies), this one aliases --ag-shadow-focus directly, the same
     single-value box-shadow ring static/css/components.css's own
     .ag-input:focus already uses site-wide (FIX ROUND 1: this file's own
     .ds-fare-alert__input:focus originally used only a border-color
     swap, leaving keyboard users a 1.5px hue change as the sole focus
     signal -- brought in line with the rest of the design system). */
  --ds-shadow-focus: var(--ag-shadow-focus);

  /* radii — reuse --ag-radius-* where the pixel value lines up exactly.
     --ds-r-sm and --ds-r-xl are plain literals instead (neither is a
     color, so the hex guard doesn't apply to either):
       - --ds-r-xl (24px): no --ag-radius-* token is this large.
       - --ds-r-sm (10px): --ag-radius-lg's PRIMARY definition is 10px,
         but design-tokens.css re-declares --ag-radius-lg a second time
         later, in the "Old provider form radius" legacy block — same
         :root specificity, later in source order, so IT wins the
         cascade and aliases --ag-radius-lg to --ag-radius-3xl (16px).
         var(--ag-radius-lg) here would silently resolve to 16px, not
         10px. That shadowing is a pre-existing, site-wide issue in
         design-tokens.css itself (tracked separately, not fixed here —
         changing it would move radii on every other page); this sheet
         just avoids inheriting the wrong value. */
  --ds-r-sm: 10px;
  --ds-r-md: var(--ag-radius-2xl);  /* 14px */
  --ds-r-lg: var(--ag-radius-4xl);  /* 18px */
  --ds-r-xl: 24px;
  --ds-r-pill: var(--ag-radius-full);
}

/* ---------- data-voice label ---------- */
.ds-dv {
  font-size: 11px; font-weight: 600; letter-spacing: .14em; text-transform: uppercase;
  color: var(--ds-ink-3);
}
/* W3-T2 gap-fill: gloss.css's own .dv--light (line 57), ported for the
   garage hero's white-on-gradient class_label -- W0-T3 deliberately left
   dv modifiers out ("gloss has them, brief doesn't name them"); this is
   the first task whose own markup actually needs one. */
.ds-dv--light { color: rgba(255, 255, 255, .72); }

/* ---------- cards ---------- */
.ds-card {
  background: var(--ds-card-bg); border: 1px solid var(--ag-color-brand-primary-softer); border-radius: var(--ds-r-lg);
  box-shadow: var(--ds-shadow-1);
}
/* Task 5 (detail-theme-rail-globe-toggle): brand-tinted card border, matching
   Task 4's legacy .ag-card treatment (border-color -> var(--ag-color-brand-primary-softer)).
   Scoped to this one consumer rather than to the --ds-line bridge var itself:
   --ds-line/--ds-line-soft/--ds-tint are shared by ~25 unrelated selectors
   (buttons, chips, facts, calendar, modal inputs, etc.) as a deliberately
   distinct rgba-decomposed hairline scale (see the token bridge comment
   above and test_no_raw_brand_hex_outside_comments's own note on why those
   stay decimal rgba() rather than becoming this exact token) -- redefining
   --ds-line to this token's .12 alpha would recolor all of them, well past
   an emphasis layer. */
.ds-card--pad { padding: 20px 22px; }
.ds-card--crown { position: relative; overflow: hidden; }
.ds-card--crown::before {
  content: ""; position: absolute; inset: 0 0 auto 0; height: 4px;
  background: var(--ds-grad-horizon);
}

/* ---------- chips ---------- */
.ds-chip {
  display: inline-flex; align-items: center; gap: 7px; padding: 7px 13px; border-radius: var(--ds-r-pill);
  background: var(--ds-card-bg); border: 1px solid var(--ds-line);
  font-size: 12.5px; font-weight: 500; color: var(--ds-ink-2); white-space: nowrap;
}
.ds-chip--tint { background: var(--ds-tint); border-color: transparent; color: var(--ds-blue); font-weight: 600; }
.ds-chip--ok { background: var(--ds-ok-soft); border-color: transparent; color: var(--ds-ok-ink); font-weight: 600; }
.ds-chip--warn { background: var(--ds-warn-soft); border-color: transparent; color: var(--ds-warn); font-weight: 600; }
.ds-chip--gold { background: var(--ds-gold-soft); border-color: transparent; color: var(--ds-gold-ink); font-weight: 600; }

/* ---------- buttons (base handles both <button> and <a class="ds-btn">) ---------- */
.ds-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 9px;
  border: 0; border-radius: var(--ds-r-md); padding: 13px 22px;
  font: inherit; font-weight: 700; font-size: 15px; letter-spacing: .01em;
  text-decoration: none; cursor: pointer;
  transition: transform var(--ag-duration-fast) var(--ag-ease-default),
              box-shadow var(--ag-duration-fast) var(--ag-ease-default),
              filter var(--ag-duration-fast) var(--ag-ease-default);
}
.ds-btn--cta {
  background: var(--ds-grad-cta); color: var(--ds-on-primary);
  box-shadow: 0 4px 14px rgba(37, 99, 235, .35), inset 0 1px 0 rgba(255, 255, 255, .25);
}
.ds-btn--cta:hover {
  filter: brightness(1.06); transform: translateY(-1px);
  box-shadow: 0 8px 22px rgba(37, 99, 235, .42), inset 0 1px 0 rgba(255, 255, 255, .25);
}
.ds-btn--cta:active { transform: translateY(0) scale(.985); }
.ds-btn--ghost { background: var(--ds-card-bg); border: 1.5px solid var(--ds-line); color: var(--ds-navy); font-weight: 600; }
.ds-btn--ghost:hover { border-color: var(--ds-blue); color: var(--ds-blue); }
.ds-btn--soft { background: var(--ds-tint); color: var(--ds-blue); font-weight: 600; }
/* W1-T4 gap-fill: gloss.css's own .btn--block (line ~125), ported verbatim.
   W0-T3 deliberately left this out ("neither is named anywhere in my
   brief") -- the rail's own CTA doesn't need it (.ds-rail__cta .ds-btn
   already gets width:100% from its wrapper, per that task's report), but
   the rail's secondary "ask the host" link sits outside that wrapper and
   is full-width in the mockup, so this is the one real, missing primitive
   this task's own markup needs. */
.ds-btn--block { width: 100%; }

/* ---------- section heading ---------- */
.ds-section { margin-top: 26px; }
.ds-section__head { display: flex; align-items: baseline; gap: 12px; margin-bottom: 14px; }
/* Task 5: brand left-rule, matching Task 4's legacy .ag-section h3 addition
   (padding-left:10px; border-left:3px solid var(--ag-color-brand-primary);
   line-height:1.25) -- reuses --ds-blue, the bridge's existing alias for
   that exact token, rather than a new literal. */
.ds-section__title {
  font-size: 19px; font-weight: 700; color: var(--ds-navy); letter-spacing: -.01em;
  padding-left: 10px; border-left: 3px solid var(--ds-blue); line-height: 1.25;
}
.ds-section__sub { font-size: 13px; color: var(--ds-ink-3); }

/* ---------- content + aside layout (the signature two-column shell) ---------- */
.ds-grid2 { display: grid; grid-template-columns: 1fr 372px; gap: 26px; align-items: start; margin-top: 22px; }

/* ---------- facts grid ---------- */
.ds-facts { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 10px; }
.ds-fact { background: var(--ds-card-bg); border: 1px solid var(--ds-line); border-radius: var(--ds-r-md); padding: 12px 14px; }
.ds-fact .ds-dv { display: block; margin-bottom: 3px; }
.ds-fact b { font-size: 14.5px; font-weight: 600; color: var(--ds-ink); }

/* ---------- icon list ("What's included", W1-T12) ----------
   Ported from the mockup's own gloss.css .iconlist/.ic* rules
   (docs/superpowers/mockups/2026-07-28-services-detail/assets/gloss.css:198-204),
   colors routed through the --ds-* bridge above. .ic--ok's mockup color
   (#0e7a37) reuses --ds-ok-ink, the same substitution the cross-sell
   cards' .ds-rcard__why rule already established for the identical "dark
   ink on a light-success background" role. .ic--no's mockup background
   (#fdecec) has no --ds-* equivalent until this task -- see --ds-bad-soft
   in the token bridge above. The neutral/"note" .ds-ic (no modifier)
   reuses --ds-line-soft, which is already rgba(46,116,184,.09) -- the
   exact value the mockup's own base .ic rule uses. */
.ds-iconlist { display: grid; grid-template-columns: 1fr 1fr; gap: 11px 22px; list-style: none; margin: 0; padding: 0; }
.ds-iconlist li { display: flex; gap: 11px; align-items: flex-start; font-size: 14px; color: var(--ds-ink-2); }
.ds-ic {
  flex: 0 0 26px; height: 26px; border-radius: 9px; display: flex; align-items: center; justify-content: center;
  background: var(--ds-line-soft); color: var(--ds-blue); font-size: 13px; font-weight: 700;
}
.ds-ic--ok { background: var(--ds-ok-soft); color: var(--ds-ok-ink); }
.ds-ic--no { background: var(--ds-bad-soft); color: var(--ds-bad); }

/* ---------- itinerary (tour-only, W1-T12) ----------
   Ported from the mockup's own gloss.css .itin, .itin__stop and
   .itin__meta rules
   (docs/superpowers/mockups/2026-07-28-services-detail/assets/gloss.css:207-215).
   The mockup's own vertical connector is a 3-stop gradient
   (cta1 -> cta2 60% -> sand); this bridge exposes cta1/sand individually
   but only the already-composed 2-stop cta1->cta2 gradient (--ds-grad-cta),
   never cta2 as a standalone color -- reused verbatim here rather than
   inventing a new --ds-cta2 bridge variable or hardcoding #4f46e5 (which
   the CSS foundation guard bans outright). At 2px wide, neither the
   dropped third stop nor the 145deg-vs-180deg angle difference is
   visually distinguishable. The stop marker's box-shadow
   (rgba(37,99,235,.15)) is cta1 decomposed to a translucent ring, the
   same "decimal rgba() of an existing bridge color" pattern --ds-line/
   --ds-tint/etc. already use above. */
.ds-itin { position: relative; padding-left: 26px; }
.ds-itin::before {
  content: ""; position: absolute; left: 8px; top: 8px; bottom: 14px; width: 2px;
  background: var(--ds-grad-cta);
}
.ds-itin__stop { position: relative; padding: 0 0 18px 14px; }
.ds-itin__stop::before {
  content: ""; position: absolute; left: -24px; top: 5px; width: 14px; height: 14px; border-radius: 50%;
  background: var(--ds-card-bg); border: 3.5px solid var(--ds-cta1);
  box-shadow: 0 0 0 3px rgba(37, 99, 235, .15);
}
.ds-itin__stop h4 { font-size: 14.5px; font-weight: 700; color: var(--ds-navy); }
.ds-itin__stop p { font-size: 13.5px; color: var(--ds-ink-2); margin-top: 3px; }
.ds-itin__meta { font-size: 11.5px; color: var(--ds-ink-3); font-weight: 600; letter-spacing: .06em; text-transform: uppercase; }

/* ---------- accordion ---------- */
details.ds-acc {
  border: 1px solid var(--ds-line); border-radius: var(--ds-r-md); background: var(--ds-card-bg);
  margin-bottom: 10px; overflow: hidden;
}
details.ds-acc summary {
  list-style: none; display: flex; justify-content: space-between; align-items: center; gap: 10px;
  padding: 14px 17px; font-weight: 600; font-size: 14px; color: var(--ds-navy); cursor: pointer;
}
details.ds-acc summary::-webkit-details-marker { display: none; }
details.ds-acc summary::after { content: "+"; font-size: 18px; color: var(--ds-blue); font-weight: 700; }
details.ds-acc[open] summary::after { content: "–"; }
details.ds-acc .ds-acc__body { padding: 0 17px 15px; font-size: 13.5px; color: var(--ds-ink-2); }

/* ---------- Reserve Rail (THE signature: booking is always one glance away) ----------
   Ported from mockup gloss.css .rail exactly (sticky shell + gradient crown +
   body padding + full internal vocabulary — price row, date/qty fields, the
   stepper, the total row, the CTA slot, cancel/urgency notes, trustline).
   No template partial in this program's plan appends CSS for the rail, so
   this foundation carries the whole component, not just the outer shell. */
.ds-rail {
  position: sticky; top: calc(var(--ag-header-height) + 20px);
  border-radius: var(--ds-r-xl); overflow: hidden;
  background: linear-gradient(180deg, rgba(255, 255, 255, .94), rgba(255, 255, 255, .88));
  backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
  border: 1px solid rgba(46, 116, 184, .18);
  box-shadow: var(--ds-shadow-rail);
}
.ds-rail::before {
  content: ""; display: block; height: 5px; background: var(--ds-grad-cta);
  box-shadow: 0 1px 10px rgba(37, 99, 235, .5);
}
.ds-rail__body { padding: 20px 22px; }
.ds-rail__price-row { display: flex; align-items: baseline; gap: 8px; }
/* Task 5: price in brand primary, matching Task 4's legacy .ag-price
   (color -> var(--ag-color-brand-primary)); --ds-blue is the bridge's
   existing alias for that token. */
.ds-rail__price { font-size: 30px; font-weight: 800; color: var(--ds-blue); letter-spacing: -.02em; }
.ds-rail__per { font-size: 12.5px; color: var(--ds-ink-3); font-weight: 500; }
.ds-rail__was { font-size: 13px; color: var(--ds-ink-3); text-decoration: line-through; }
.ds-rail__field {
  margin-top: 12px; border: 1.5px solid var(--ds-line); border-radius: var(--ds-r-md);
  padding: 10px 14px; background: var(--ds-card-bg);
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
}
.ds-rail__field:hover { border-color: rgba(46, 116, 184, .4); }
.ds-rail__lab { font-size: 10.5px; font-weight: 700; letter-spacing: .12em; text-transform: uppercase; color: var(--ds-ink-3); display: block; }
.ds-rail__val { font-size: 14px; font-weight: 600; color: var(--ds-ink); }
.ds-rail__split { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 12px; }
/* A .ds-rail__field pair inside the split sits where a single field would
   normally sit -- the split container above already carries that same
   12px top margin, so its own children must not ALSO add theirs (that
   would double the gap above the pair, but not above a standalone field
   elsewhere in the rail). */
.ds-rail__split .ds-rail__field { margin-top: 0; }
/* W3-T9: the car pickup/return pair and the transfer date/time pair are
   real <input type="date"/"time"> elements, unlike every earlier
   .ds-rail__field (an <a> wrapping plain <span> text) -- a form control's
   UA default font-family/line-height isn't inherited from the page the
   way a <span>'s is (the identical gap this file's own .ds-hero__count
   rule already documents for a <button>), so both are set explicitly
   here rather than left to inherit nothing. Border/background/padding
   stay on the PARENT .ds-rail__field above; the input itself is a bare,
   borderless fill so it reads as the SAME "value" text .ds-rail__val
   already renders for every other field, not a visibly distinct form
   control dropped into the rail. */
.ds-rail__field input[type="date"],
.ds-rail__field input[type="time"] {
  display: block; width: 100%; border: 0; padding: 0; background: transparent;
  font-family: inherit; line-height: inherit; font-size: 14px; font-weight: 600; color: var(--ds-ink);
}
.ds-rail__field input[type="date"]:focus,
.ds-rail__field input[type="time"]:focus { outline: none; }
.ds-rail__field:focus-within { border-color: var(--ds-blue); }
/* W4-T5: the flight variant's own pax-stepper caption ("Counted as
   adults -- add children or infants at checkout") -- a sibling of
   .ds-rail__field, not nested inside it (.ds-rail__field is a flex row,
   label-left/control-right; a caption belongs on its own line below,
   matching how .ds-rail__breakdown already sits as its own sibling
   rather than crammed inside the field it explains). Same small-muted-
   caption recipe .ds-extra__sub/.ds-itin__meta already use elsewhere in
   this file (11.5px, --ds-ink-3), not a new visual language. */
.ds-rail__hint { margin-top: 6px; font-size: 11.5px; color: var(--ds-ink-3); }
/* W2-T4: the stay variant's nightly breakdown rows ("$X x N nights" ->
   amount), between the rooms stepper and the total row -- mirrors the
   approved stay mockup's own inline-styled block
   (docs/superpowers/mockups/2026-07-28-services-detail/web-stay.html),
   formalized into named ds- classes rather than carried as inline style
   attributes. Empty (no children) until static/js/detail_system.js's
   AGDetail.setRange() populates it once a real check-in/checkout range
   completes -- an empty .ds-rail__breakdown renders as a zero-height,
   invisible div, not a stray gap. */
.ds-rail__breakdown { margin-top: 13px; display: grid; gap: 5px; font-size: 13px; color: var(--ds-ink-2); }
.ds-rail__bd-row { display: flex; justify-content: space-between; gap: 10px; }
.ds-rail__bd-row b { color: var(--ds-ink); font-weight: 700; }
.ds-rail__total {
  display: flex; justify-content: space-between; align-items: baseline;
  margin-top: 14px; padding-top: 13px; border-top: 1px dashed var(--ds-line);
}
/* Task 5: grand-total amount, same brand-primary treatment as the rail
   price above -- parity with Task 4's legacy .ag-price-total. */
.ds-rail__total b { font-size: 19px; color: var(--ds-blue); font-weight: 800; }
.ds-rail__cta { margin-top: 13px; }
.ds-rail__cta .ds-btn { width: 100%; }
.ds-rail__note { margin-top: 11px; display: flex; flex-wrap: wrap; gap: 7px; justify-content: center; }
.ds-rail__urgency {
  margin-top: 11px; display: flex; align-items: center; gap: 8px;
  font-size: 12.5px; color: var(--ds-warn); font-weight: 600;
  background: var(--ds-warn-soft); border-radius: var(--ds-r-sm); padding: 8px 12px;
}
/* W1-T4 review fold-in: named class instead of the rail's own inline
   style="margin-top:10px" -- the "ask the host" link sits after
   .ds-rail__note/__urgency, both of which already set their own
   margin-top, so this is the one spacing value with no home yet. */
.ds-rail__ask { margin-top: 10px; }

/* guest/qty stepper — reused by the rail's participants field and by later
   room/pax variants (stays, flights), so it's kept standalone, not rail__ scoped */
.ds-stepper { display: inline-flex; align-items: center; gap: 12px; }
.ds-stepper button {
  width: 26px; height: 26px; border-radius: 50%; border: 1.5px solid var(--ds-line);
  color: var(--ds-blue); font-weight: 800; line-height: 1;
}
.ds-stepper b { font-size: 15px; min-width: 16px; text-align: center; }

/* trust line under the rail — standalone (mockup doesn't scope it under .rail__ either) */
.ds-trustline {
  display: flex; gap: 14px; justify-content: center; align-items: center;
  margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--ds-line-soft);
}
.ds-trustline span { font-size: 11px; color: var(--ds-ink-3); display: flex; align-items: center; gap: 5px; font-weight: 500; }

/* ---------- mobile sticky book bar (the rail's small-viewport mirror) ---------- */
.ds-bookbar {
  position: sticky; bottom: 14px; z-index: 40; margin-top: 30px;
  display: flex; align-items: center; gap: 14px; justify-content: space-between;
  background: rgba(255, 255, 255, .92); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
  border: 1px solid rgba(46, 116, 184, .22); border-radius: var(--ds-r-lg);
  padding: 12px 16px; box-shadow: var(--ds-shadow-rail);
}
.ds-bookbar__price b { font-size: 19px; color: var(--ds-navy); font-weight: 800; }
.ds-bookbar__price span { font-size: 11.5px; color: var(--ds-ink-3); display: block; }

/* ---------- hero (photo mosaic) + postcard (photo-less fallback) ----------
   Ported from mockup gloss.css .hero-grid/.hero-count/.postcard* (W1-T2),
   colors routed through the --ds-* bridge above. The postcard is a designed
   empty state, not an error placeholder -- production has real listings
   with zero photos today, and this must read as intentional at thumbnail
   size (see W1-T2-brief.md). No pager/carousel JS: the count pill reuses
   the sitewide lightbox's own trigger contract (static/js/media_modal.js:
   a delegated document click on .js-media-open[data-media-id]). */
.ds-hero__grid {
  display: grid; grid-template-columns: 2.1fr 1fr; gap: 10px;
  border-radius: var(--ds-r-xl); overflow: hidden; position: relative;
}
.ds-hero__ph { position: relative; background: var(--ds-mist); min-height: 190px; }
.ds-hero__ph--main { grid-row: span 2; min-height: 392px; }
.ds-hero__ph img { width: 100%; height: 100%; object-fit: cover; position: absolute; inset: 0; }
.ds-hero__count {
  position: absolute; right: 14px; bottom: 14px; z-index: 2;
  background: rgba(30, 40, 74, .62); color: #fff;
  /* font-family/line-height only, not the `font` shorthand: a <button>'s
     UA default font-family isn't inherited from the page (unlike a
     <span>'s), so this normalizes the two pill variants -- the bare
     `font` shorthand would additionally reset font-style/variant/stretch
     nobody asked for, AND (task review finding) silently clobber the
     explicit font-size/font-weight below if it were ever placed after
     them again, since it resets every longhand it covers. */
  font-family: inherit; line-height: inherit;
  font-size: 12.5px; font-weight: 600;
  padding: 8px 14px; border-radius: var(--ds-r-pill);
  backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
  border: 0;
}
/* only the real, openable-photo variant (a <button>) gets the pointer cursor --
   the plain-label variant (a <span>, where no photo behind it has a working
   lightbox id) must not look clickable when it isn't (task review finding) */
.ds-hero__count--live { cursor: pointer; }

.ds-postcard {
  position: relative; display: flex; align-items: flex-end; min-height: 330px;
  border-radius: var(--ds-r-xl); overflow: hidden; background: var(--ds-grad-horizon);
}
.ds-postcard::before {
  content: ""; position: absolute; inset: 0;
  background:
    radial-gradient(420px 190px at 82% 8%, rgba(255, 255, 255, .20), transparent 60%),
    radial-gradient(720px 300px at 12% 108%, rgba(30, 40, 74, .55), transparent 70%);
}
/* ridge silhouette -- illustration-only "distant hills against the horizon"
   shades, not brand-palette colors (no --ds-* token targets this); kept as
   literals like the glow above, same reasoning as --ds-grad-dusk's own
   navy-decomposed rgba() in the token bridge */
.ds-postcard__ridge {
  position: absolute; inset: auto 0 56px 0; height: 110px; opacity: .5;
  background:
    radial-gradient(60% 130px at 22% 100%, #16224a 0, #16224a 60%, transparent 61%),
    radial-gradient(52% 110px at 58% 100%, #1d2f5e 0, #1d2f5e 60%, transparent 61%),
    radial-gradient(46% 90px at 88% 100%, #27437c 0, #27437c 60%, transparent 61%);
  filter: blur(1px);
}
.ds-postcard__body { position: relative; z-index: 2; padding: 26px 28px; color: #fff; width: 100%; }
.ds-postcard__kicker {
  font-size: 11px; font-weight: 700; letter-spacing: .22em; text-transform: uppercase;
  color: rgba(255, 255, 255, .75);
}
.ds-postcard__title {
  font-size: 34px; font-weight: 800; letter-spacing: -.02em; line-height: 1.12;
  text-shadow: 0 2px 14px rgba(15, 23, 42, .35);
}
.ds-postcard__cta { position: absolute; right: 22px; bottom: 22px; z-index: 3; }

/* ---------- title row: breadcrumb + h1 + trust chips + actions (W1-T3) ----------
   Ported from mockup gloss.css .dv/.titlerow/.actionrow/.btn--sm
   (docs/superpowers/mockups/2026-07-28-services-detail/assets/gloss.css:10-13,115-125),
   colors through the --ds-* bridge above. .ds-titlemeta has no gloss.css
   rule of its own (the class exists in the mockup's HTML with nothing
   targeting it) -- given the same explicit flex-wrap+gap every other
   chip/button row in this sheet already declares (.ds-actionrow,
   .ds-facts, .ds-rail__note), it gets the same treatment here rather than
   relying on .ds-chip's own inline-flex display to wrap flush with no
   gap between chips. */
.ds-breadcrumb { margin-bottom: 8px; }
.ds-breadcrumb__sep { margin: 0 6px; color: var(--ds-ink-3); }
.ds-breadcrumb__current { color: var(--ds-ink-2); }

.ds-titlerow { display: flex; justify-content: space-between; gap: 18px; align-items: flex-start; flex-wrap: wrap; }
.ds-title { font-size: 31px; font-weight: 800; color: var(--ds-navy); letter-spacing: -.02em; line-height: 1.15; }
.ds-titlemeta { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }

.ds-actionrow { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
.ds-btn--sm { padding: 9px 15px; font-size: 13px; border-radius: var(--ds-r-sm); }
/* ported from static/catalog/css/activity_detail.css's own .ag-action-status
   (min-height reserves the line's space so the row doesn't jump when the
   copy-link/favorite JS briefly writes a status message into it) */
.ds-action-status { font-size: 12px; color: var(--ds-ink-3); min-height: 14px; }

/* ---------- share disclosure (task review fix) ----------
   Zero-JS <details>/<summary>: wraps the three legacy-hook-bound share
   controls (copy-link/WhatsApp/Send) unchanged so the row reads as one
   "Share" trigger, matching the mockup's three-control shape, without any
   new share JS -- native <summary> is keyboard-focusable and toggles on
   Enter/Space with no script. */
.ds-sharewrap { position: relative; }
.ds-sharewrap > summary { list-style: none; }
.ds-sharewrap > summary::-webkit-details-marker { display: none; }
.ds-sharewrap > summary:focus-visible { outline: 2px solid var(--ds-blue); outline-offset: 2px; }
.ds-sharemenu {
  position: absolute; top: calc(100% + 6px); left: 0; z-index: 20;
  display: flex; flex-direction: column; gap: 6px; min-width: 180px;
  padding: 10px; border-radius: var(--ds-r-md);
  background: var(--ds-card-bg); border: 1px solid var(--ds-line);
  box-shadow: var(--ds-shadow-2);
}
.ds-sharemenu .ds-btn { width: 100%; justify-content: flex-start; }

/* ---------- availability calendar (W1-T6: one truth for what's bookable) ----------
   Ported from the mockup's own gloss.css .cal/.cal__* rules, colors routed
   through the --ds-* bridge above (no new literal hex introduced -- the
   mockup's inline swatch colors and its literal dark-green/slate day-text
   colors are replaced with tokens the bridge already carries:
   --ds-ok-ink for the open-day price text, same token .ds-chip--ok already
   uses for "dark ink on a light-success background"; --ds-mist for the
   default/closed-day/off-swatch background, same token .cal__day's own
   default background already resolves to; --ds-on-primary instead of a
   literal #fff for the selected day's text, matching .ds-btn--cta's own
   on-primary color).

   static/js/detail_system.js (initCalendar) builds the actual
   day-of-week headers + day cells at runtime -- _availability.html ships
   only the outer shell (head/nav/legend) plus an empty grid container;
   this sheet just supplies the look for whatever JS creates into it. */
.ds-cal { border: 1px solid var(--ds-line); border-radius: var(--ds-r-lg); overflow: hidden; background: var(--ds-card-bg); }
.ds-cal__head { display: flex; justify-content: space-between; align-items: center; padding: 13px 16px; border-bottom: 1px solid var(--ds-line-soft); }
.ds-cal__head b { font-size: 14.5px; color: var(--ds-navy); }
.ds-cal__nav { display: flex; gap: 8px; }
.ds-cal__nav button {
  width: 30px; height: 30px; border-radius: 50%; border: 1.5px solid var(--ds-line);
  background: var(--ds-card-bg); color: var(--ds-blue); font-weight: 800; cursor: pointer;
}
.ds-cal__nav button:hover { border-color: var(--ds-blue); }
.ds-cal__grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 6px; padding: 12px 14px 16px; }
.ds-cal__dow {
  font-size: 10px; font-weight: 700; letter-spacing: .1em; color: var(--ds-ink-3);
  text-align: center; padding: 4px 0; text-transform: uppercase;
}
.ds-cal__day {
  display: block; border-radius: 11px; padding: 7px 2px 6px; text-align: center;
  font-size: 12.5px; font-weight: 600; color: var(--ds-ink-2); background: var(--ds-mist);
}
.ds-cal__day small { display: block; font-size: 9.5px; font-weight: 700; color: var(--ds-ok-ink); letter-spacing: .02em; }
.ds-cal__day--open { background: var(--ds-ok-soft); color: var(--ds-ok-ink); cursor: pointer; }
.ds-cal__day--open:hover { outline: 2px solid var(--ds-ok); }
.ds-cal__day--open:focus-visible { outline: 2px solid var(--ds-blue); outline-offset: 1px; }
.ds-cal__day--sel { background: var(--ds-grad-cta); color: var(--ds-on-primary); box-shadow: 0 4px 12px rgba(37, 99, 235, .35); }
.ds-cal__day--sel small { color: rgba(255, 255, 255, .85); }
.ds-cal__day--off { opacity: .38; }
.ds-cal__legend { display: flex; flex-wrap: wrap; gap: 14px; padding: 0 16px 13px; font-size: 11px; color: var(--ds-ink-3); }
.ds-cal__legend span { display: inline-flex; align-items: center; }
.ds-cal__swatch { display: inline-block; width: 9px; height: 9px; border-radius: 3px; margin-right: 5px; }
.ds-cal__swatch--open { background: var(--ds-ok); }
.ds-cal__swatch--sel { background: var(--ds-cta1); }
.ds-cal__swatch--off { background: var(--ds-mist); border: 1px solid var(--ds-line); }
.ds-cal__noscript { padding: 0 16px 16px; font-size: 12.5px; color: var(--ds-ink-2); }

/* ---------- meeting-point map card (W1-T7: real mini-map + one Globe hand-off) ----------
   Ported from the mockup's own gloss.css .mapcard/.mapcard__* rules, colors
   routed through the --ds-* bridge above where a token exists.

   .ds-map is the SAME root static/js/detail_system.js's initMap() targets
   directly as the Mapbox GL `container` (a live map) and the root the
   --placeholder modifier below decorates (no map) -- one class, two looks,
   matching templates/catalog/detail/_map.html's own contract. Base rule
   only sets layout + a plain --ds-mist "not loaded yet" surface (same
   idle-surface token .ds-hero__ph/.ds-cal__day already use) -- Mapbox's
   own canvas covers this within a render frame once the live branch's
   script finishes loading. */
.ds-map {
  position: relative; min-height: 220px; overflow: hidden;
  border-radius: var(--ds-r-lg); border: 1px solid var(--ds-line);
  background: var(--ds-mist);
}
/* The grid-texture look is the PLACEHOLDER's own designed empty state
   (never reached by the live branch, which paints real tiles over the
   base rule's plain surface above) -- a designed fallback, not an error
   state (this task's brief: "never the string 'coming soon'"). The pale
   wash stops (#eaf2fb/#f6fafe) are illustration-only -- no --ds-* token
   targets this exact tint -- same accepted-literal precedent as
   .ds-postcard__ridge's own ridge-silhouette colors above. */
.ds-map--placeholder {
  background:
    radial-gradient(340px 190px at 72% 30%, var(--ds-tint), transparent 60%),
    linear-gradient(14deg, rgba(241, 201, 174, .30), transparent 45%),
    repeating-linear-gradient(0deg, transparent 0 46px, var(--ds-line-soft) 46px 47px),
    repeating-linear-gradient(90deg, transparent 0 46px, var(--ds-line-soft) 46px 47px),
    linear-gradient(180deg, #eaf2fb, #f6fafe);
}
/* decorative, illustrative pin -- placeholder-only (real coordinates, when
   there are any, are never plotted here; a fake pin at a real-looking
   position would misrepresent a location this card can't actually place).
   Rotated-square "teardrop" shape, same construction as the mockup's own
   .mapcard__pin: a centered flex child stays a plain circle regardless of
   the parent's rotation, so no counter-rotation is needed on ::after. */
.ds-map__pin {
  position: absolute; left: 50%; top: 42%; width: 34px; height: 34px;
  transform: translate(-50%, -50%) rotate(-45deg);
  border-radius: 50% 50% 50% 4px;
  display: flex; align-items: center; justify-content: center;
  background: var(--ds-grad-cta); box-shadow: 0 6px 14px rgba(37, 99, 235, .35);
}
.ds-map__pin::after {
  content: ""; width: 11px; height: 11px; border-radius: 50%; background: var(--ds-on-primary);
}
/* Multi-pin schematic (W3-T5: car/transfer "Pickup locations" adoption) --
   2+ real, independently-coordinated pickup points, only reachable via a
   transfer with both origin AND destination configured today
   (apps.catalog.detail_context._car_pickups' own docstring guarantees a
   car can never produce more than ONE plottable pickup). Reuses
   .ds-map__pin's own teardrop shape/colors verbatim (base rule above,
   untouched) -- only each pin's POSITION is overridden per slot, since
   that rule's single fixed left:50%/top:42% would stack every pin exactly
   on top of the others otherwise. Percentages match the approved
   mockup's own .mapcard__pin--a/--b illustrative positions (docs/
   superpowers/mockups/2026-07-28-services-detail/web-transport.html) --
   two fixed slots cover every real case today; templates/catalog/detail/
   _map.html's own header comment documents why a 3rd is intentionally not
   built (it would land at the base rule's own centered 50%/42% position --
   a third, distinct spot, NOT an overlap with pin 1's 30%/42% -- rather
   than inventing a dedicated layout for a case no real model can produce).
   */
.ds-map__pin--1 { left: 30%; top: 42%; }
.ds-map__pin--2 { left: auto; right: 22%; top: 30%; }
/* address + Directions/Globe overlay bar, anchored to the card's own
   bottom edge in BOTH branches (live map or placeholder) -- gradient-fade
   background so it reads over either a photo-like live map or the
   placeholder's own texture without a hard edge. */
.ds-map__foot {
  position: absolute; inset: auto 0 0 0; z-index: 2;
  display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; gap: 12px;
  padding: 13px 16px;
  background: linear-gradient(180deg, transparent, var(--ds-card-bg) 38%);
}
.ds-map__addr { font-size: 13px; font-weight: 600; color: var(--ds-navy); }
.ds-map__addr small { display: block; font-weight: 500; color: var(--ds-ink-3); font-size: 11.5px; }
.ds-map__actions { display: flex; flex-wrap: wrap; gap: 8px; }

/* ---------- reviews (W1-T8: invitation empty state + verified summary) ----------
   Ported from the mockup's own gloss.css .revsum/.revbar/.review/.rev-invite
   rules, colors routed through the --ds-* bridge above.

   .ds-revsum__stars uses the SAME base-plus-clipped-fill overlay technique
   this codebase's own legacy rating widget already uses in production
   (static/catalog/css/activity_detail.css's .ag-rating-stars/.fill: two
   identical five-glyph star strings stacked with absolute positioning, the
   top one clipped by a percentage-width overflow:hidden wrapper) rather
   than a new technique — a real, data-driven fill (ds.reviews.avg divided
   by 5, computed server-side via the built-in widthratio tag), not the
   mockup's own static all-five-filled "the one example happens to be a
   4.9" glyphs. */
.ds-revsum { display: grid; grid-template-columns: 150px 1fr; gap: 22px; align-items: center; }
.ds-revsum__score { text-align: center; }
.ds-revsum__big { font-size: 44px; font-weight: 800; color: var(--ds-navy); line-height: 1; }
.ds-revsum__stars { position: relative; display: inline-block; font-size: 15px; letter-spacing: 2px; }
.ds-revsum__stars-base { color: var(--ds-line); }
.ds-revsum__stars-fill {
  position: absolute; inset: 0 auto 0 0; overflow: hidden; width: 0;
  color: var(--ds-gold); white-space: nowrap;
}
.ds-revsum__count { font-size: 12px; color: var(--ds-ink-3); margin-top: 4px; }

/* per-star (or, per the brief's own given test, any other named
   dimension) breakdown bar -- .ds-revbar i is a bare, empty, non-semantic
   layout box (no text content, so its element name carries no visible
   meaning), matching gloss.css's own identical use of a bare <i> for this
   exact fill-track purpose. */
.ds-revbar { display: flex; align-items: center; gap: 10px; font-size: 12px; color: var(--ds-ink-2); margin: 5px 0; }
.ds-revbar i {
  flex: 1; height: 7px; border-radius: 5px; background: var(--ds-mist); overflow: hidden; position: relative;
}
.ds-revbar i::after {
  content: ""; position: absolute; inset: 0; width: var(--w, 0%); border-radius: 5px;
  /* #fbbf24 is a plain amber gradient stop (illustration only, not one of
     the five banned brand hex literals) -- same accepted-literal precedent
     as .ds-postcard__ridge's own ridge-silhouette colors above. */
  background: linear-gradient(90deg, var(--ds-gold), #fbbf24);
}

.ds-review { display: flex; gap: 13px; padding: 15px 0; border-top: 1px solid var(--ds-line-soft); }
.ds-review__ava {
  width: 40px; height: 40px; border-radius: 50%; flex: 0 0 40px; background: var(--ds-grad-horizon);
  box-shadow: 0 0 0 2px var(--ds-card-bg), 0 0 0 3.5px rgba(46, 116, 184, .3);
  display: flex; align-items: center; justify-content: center; color: var(--ds-on-primary); font-weight: 700; font-size: 14px;
}
.ds-review__body b { font-size: 13.5px; color: var(--ds-navy); }
.ds-review__body a { color: inherit; text-decoration: none; }
.ds-review__body a:hover { color: var(--ds-blue); text-decoration: underline; }
.ds-review__body p { font-size: 13.5px; color: var(--ds-ink-2); margin-top: 4px; }
.ds-review__body .ds-chip { margin-top: 7px; }

/* invitation empty state -- a compact perk card, not the legacy full
   -height "no reviews yet" apology (this task's brief). */
.ds-rev-invite {
  display: flex; align-items: center; gap: 18px; padding: 18px 20px; border-radius: var(--ds-r-lg);
  background: linear-gradient(120deg, rgba(37, 99, 235, .07), rgba(79, 70, 229, .07) 55%, rgba(241, 201, 174, .16));
  border: 1px dashed rgba(46, 116, 184, .35);
}
.ds-rev-invite__medal {
  width: 52px; height: 52px; flex: 0 0 52px; border-radius: 50%; background: var(--ds-grad-cta);
  box-shadow: 0 0 0 3px var(--ds-card-bg), 0 0 0 5px rgba(37, 99, 235, .25), 0 8px 18px rgba(37, 99, 235, .35);
  display: flex; align-items: center; justify-content: center; font-size: 22px; color: var(--ds-on-primary);
}
.ds-rev-invite b { color: var(--ds-navy); font-size: 14.5px; }
.ds-rev-invite p { font-size: 12.5px; color: var(--ds-ink-2); margin-top: 2px; }

/* fare-alert capture form (W4-T4) -- lives inside _fares.html's own
   .ds-rev-invite card, flag-gated by ds.fare_alerts_enabled. Reuses
   .ds-btn/.ds-btn--cta/.ds-btn--sm verbatim for the submit control (same
   "no new button style" discipline this stylesheet already follows
   elsewhere) -- only the text input itself is new. */
.ds-fare-alert { display: flex; gap: 8px; margin-top: 10px; flex-wrap: wrap; }
.ds-fare-alert__input {
  flex: 1 1 200px; min-width: 0; padding: 9px 12px; border-radius: var(--ds-r-sm);
  border: 1.5px solid var(--ds-line); background: var(--ds-card-bg); color: var(--ds-ink);
  font-family: inherit; font-size: 13px;
}
.ds-fare-alert__input:focus { outline: none; border-color: var(--ds-blue); box-shadow: var(--ds-shadow-focus); }

/* W4-T8 fix round 1 (Important review finding): [hidden] needs its own
   rule here -- the unqualified .ds-fare-alert rule above sets
   display:flex, an AUTHOR-origin declaration that beats the UA
   stylesheet's own `[hidden] { display: none }` in the cascade
   regardless of specificity (author normal-importance rules win over
   user-agent ones). Without this, static/js/fare_alert.js's own
   `form.hidden = true` on a successful submit (copied from
   stay_inquiry.js, whose own form carries no class and so has no author
   rule to out-cascade the UA default) has NO visual effect: the form --
   and its submit button, left permanently disabled reading "Sending…"
   since xhr.onload's own 200 branch never calls restoreSubmitButton(),
   correct only if the form actually disappears -- stays on screen under
   the real success copy underneath it. Same belt-and-suspenders pattern
   static/css/conversations.css's own .ag-conv-thread__reaction-picker[hidden]
   pair and static/css/globe/trip.css's own #bm-route-tools[hidden] pair
   already establish for the identical cascade issue. */
.ds-fare-alert[hidden] { display: none; }

/* W4-T8 fix round 1 (Minor review finding): full-width row, not a
   content-width item. .ds-field-error's own base rule (this stylesheet's
   "review-write modal" section, below) was written for ITS OWN context --
   _review_write_modal.html's plain, non-flex .ds-field wrapper, where a
   block-level child naturally spans the full width. Reused verbatim
   inside THIS form's own `display: flex; flex-wrap: wrap` row (above),
   an un-widened flex item instead sizes to its own error text and sits
   beside the button rather than stacking onto its own line the way the
   precedent it was copied from does. */
.ds-fare-alert .ds-field-error { flex-basis: 100%; }

.ds-rev-foot { margin-top: 14px; display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }

/* ---------- host trust card (W1-T10: real signals, not a name + a grey circle) ----------
   Ported from the mockup's own gloss.css .host/.host__ava/.host__stats rules
   (docs/superpowers/mockups/2026-07-28-services-detail/assets/gloss.css:282-287),
   colors routed through the --ds-* bridge above. Card chrome (border/
   background/shadow/padding) is NOT redeclared here -- the root stacks
   .ds-card.ds-card--pad directly alongside .ds-host, the same class-stacking
   the mockup's own markup does (class="card card--pad host"), so this block
   only adds the flex layout plus the parts unique to a host card: the
   avatar ring (a gradient circle showing initials, or a real cropped photo
   when ds.host.avatar_url is set -- same gradient-ring technique
   .ds-review__ava already uses, just larger per the mockup's 64px) and the
   stats row. ds.host is only ever rendered when the partner data is real
   (see templates/catalog/detail/_host.html) -- there is no "coming soon"
   variant to style. */
.ds-host { display: flex; gap: 16px; align-items: center; flex-wrap: wrap; }
.ds-host__ava {
  width: 64px; height: 64px; flex: 0 0 64px; border-radius: 50%; overflow: hidden;
  background: var(--ds-grad-horizon);
  box-shadow: 0 0 0 3px var(--ds-card-bg), 0 0 0 5px rgba(46, 116, 184, .35);
  display: flex; align-items: center; justify-content: center;
  color: var(--ds-on-primary); font-weight: 800; font-size: 22px;
}
.ds-host__ava img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; }
.ds-host__info { flex: 1; min-width: 180px; }
.ds-host__name { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.ds-host__name b { font-size: 15.5px; color: var(--ds-navy); }
.ds-host__name .ds-chip { font-size: 10.5px; }
.ds-host__stats { display: flex; gap: 16px; margin-top: 6px; flex-wrap: wrap; }
.ds-host__stats span { font-size: 12px; color: var(--ds-ink-2); }
.ds-host__stats b { color: var(--ds-navy); }

/* ---------- carrier trust block (W4-T7: the airline OPERATING a flight,
   a distinct real-world entity from .ds-host's own PARTNER/seller account
   above -- same card-chrome-reuse convention: chrome comes from
   .ds-card.ds-card--pad stacked directly on the root, per the mockup's
   own class="card card--pad host" reuse (docs/superpowers/mockups/
   2026-07-28-services-detail/web-flight.html:165); this block only adds
   carrier-specific layout.

   Deliberately its own BEM namespace rather than literally reusing
   .ds-host__ava/__info/__stats (which the mockup's own markup does): the
   avatar is a rounded-square carrier BADGE (an IATA code or a generic
   glyph), never a circular person photo -- apps/core/models.py's own
   Airline has no photo field at all (iata_code/icao_code/name/
   country_iso2/is_active, confirmed directly) -- so it needs its own
   border-radius rather than .ds-host__ava's 50%. --ds-r-md (14px) is the
   closest existing radius token to the mockup's literal 16px; no new
   one-off pixel value introduced for a single rule. */
.ds-carrier { display: flex; gap: 16px; align-items: center; flex-wrap: wrap; }
.ds-carrier__ava {
  width: 64px; height: 64px; flex: 0 0 64px; border-radius: var(--ds-r-md); overflow: hidden;
  background: var(--ds-grad-horizon);
  box-shadow: 0 0 0 3px var(--ds-card-bg), 0 0 0 5px rgba(46, 116, 184, .35);
  display: flex; align-items: center; justify-content: center;
  color: var(--ds-on-primary); font-weight: 800; font-size: 20px;
}
.ds-carrier__info { flex: 1; min-width: 180px; }
.ds-carrier__name { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.ds-carrier__name b { font-size: 15.5px; color: var(--ds-navy); }
.ds-carrier__name .ds-chip { font-size: 10.5px; }
.ds-carrier__stats { display: flex; gap: 16px; margin-top: 6px; flex-wrap: wrap; }
.ds-carrier__stats span { font-size: 12px; color: var(--ds-ink-2); }
.ds-carrier__stats b { color: var(--ds-navy); }

/* ---------- cross-sell rails (W1-T11: "Complete your trip") ----------
   Ported from the mockup's own gloss.css .railcards/.rcard* rules
   (docs/superpowers/mockups/2026-07-28-services-detail/assets/gloss.css),
   colors routed through the --ds-* bridge above.

   .ds-rcard__media's own base rule (a plain gradient, no photo) IS this
   card's designed "no photo yet" fallback -- an <img>, when the
   recommendation engine supplies item.image_url, layers on top and
   visually replaces it; there is no separate placeholder-icon markup to
   keep in sync (contrast .ds-map--placeholder/.ds-postcard, which both
   need a second, explicit variant because THEIR "no data" case has its
   own bespoke illustration -- a plain gradient needs none, and is never
   the legacy sitewide bare "✨" glyph).

   .ds-rcard__why's text color: the mockup's own gloss.css hardcodes a
   literal #0e7a37 here rather than one of its own CSS custom properties
   -- since this program's guard bans new hex outside comments, this
   reuses --ds-ok-ink, the SAME "dark ink on a light-success background"
   token .ds-chip--ok and .ds-cal__day small already use for an
   identical role (a small positive/affirmative text-on-tint element). */
.ds-railcards { display: grid; grid-template-columns: repeat(auto-fill, minmax(215px, 1fr)); gap: 14px; }
.ds-rcard {
  display: block; color: inherit; text-decoration: none;
  border: 1px solid var(--ds-line); border-radius: var(--ds-r-lg); overflow: hidden;
  background: var(--ds-card-bg); box-shadow: var(--ds-shadow-1);
  transition: transform var(--ag-duration-fast) var(--ag-ease-default),
              box-shadow var(--ag-duration-fast) var(--ag-ease-default);
}
.ds-rcard:hover { transform: translateY(-3px); box-shadow: var(--ds-shadow-2); text-decoration: none; }
.ds-rcard__media { height: 128px; position: relative; background: var(--ds-grad-horizon); }
.ds-rcard__media img { width: 100%; height: 100%; object-fit: cover; }
.ds-rcard__media::after { content: ""; position: absolute; inset: 0; background: var(--ds-grad-dusk); }
.ds-rcard__kind {
  position: absolute; left: 10px; top: 10px; z-index: 2;
  background: rgba(255, 255, 255, .92); color: var(--ds-navy);
  font-size: 10px; font-weight: 700; letter-spacing: .1em; text-transform: uppercase;
  padding: 4px 9px; border-radius: var(--ds-r-pill);
}
.ds-rcard__body { padding: 12px 14px 14px; }
.ds-rcard__body b { font-size: 13.5px; color: var(--ds-navy); line-height: 1.3; display: block; }
.ds-rcard__meta { font-size: 11.5px; color: var(--ds-ink-3); margin-top: 3px; }
.ds-rcard__price { margin-top: 8px; display: flex; justify-content: space-between; align-items: center; gap: 8px; }
.ds-rcard__price b { font-size: 14.5px; color: var(--ds-navy); }
.ds-rcard__why {
  font-size: 10.5px; font-weight: 600; color: var(--ds-ok-ink);
  background: var(--ds-ok-soft); border-radius: var(--ds-r-pill); padding: 3px 8px;
  white-space: nowrap;
}

/* ---------- room cards (W2-T2: photo room-cards drive the rail price) ----------
   Ported from the approved mockup's own web-stay.html <style> block
   (.room/.room--sel/.room__photo/.room__side -- this component's CSS lives
   in the STAY page's own inline mockup styles, not gloss.css, since
   gloss.css is the cross-vertical shared library and rooms are stay-only),
   colors routed through the --ds-* bridge above. Replaces
   templates/catalog/stay_detail.html's own grey single-letter
   .ag-room-card__placeholder with a designed gradient fallback labeled
   with the room's real bed type -- see templates/catalog/detail/
   _rooms.html's own header comment for the full "what this replaces"
   writeup. */
.ds-room {
  display: grid; grid-template-columns: 150px 1fr auto; gap: 16px; align-items: center;
  border: 1.5px solid var(--ds-line); border-radius: var(--ds-r-lg); padding: 13px;
  background: var(--ds-card-bg); margin-bottom: 12px; cursor: pointer;
  transition: border-color var(--ag-duration-fast) var(--ag-ease-default),
              box-shadow var(--ag-duration-fast) var(--ag-ease-default);
}
/* task review fix: the whole card is now a real click target
   (static/js/detail_system.js's initRooms(), not just the
   [data-ds-room-select] button inside it) -- cursor:pointer on the base
   rule, not just :hover, matches every other clickable surface in this
   sheet (.ds-cal__day--open, .ds-stepper button, .ds-cal__nav button). */
.ds-room:hover { border-color: rgba(46, 116, 184, .4); }
.ds-room--sel { border-color: var(--ds-cta1); box-shadow: 0 0 0 3px rgba(37, 99, 235, .14), var(--ds-shadow-1); }

.ds-room__photo {
  height: 104px; border-radius: 12px; background: var(--ds-grad-horizon);
  position: relative; overflow: hidden;
}
.ds-room__photo::after { content: ""; position: absolute; inset: 0; background: var(--ds-grad-dusk); }
.ds-room__photo img { width: 100%; height: 100%; object-fit: cover; }
/* the no-photo fallback's OWN content -- a real bed-type word ("King"),
   never a truncated single letter (see _rooms.html's own header comment)
   -- not a permanent caption also drawn over a real photo, since the
   amenity chip below already states the bed type in text either way. */
.ds-room__photo-label {
  position: absolute; left: 8px; bottom: 7px; z-index: 2; color: var(--ds-on-primary);
  font-size: 10px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase;
}

.ds-room__name { font-size: 15px; font-weight: 700; color: var(--ds-navy); }
.ds-room__amen { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 7px; }
.ds-room__amen .ds-chip { font-size: 10.5px; padding: 4px 9px; }

.ds-room__side { text-align: right; }
.ds-room__price { font-size: 19px; font-weight: 800; color: var(--ds-navy); }
.ds-room__price-unit { font-size: 11px; color: var(--ds-ink-3); font-weight: 500; }
/* scarcity -- only ever rendered by the template when ds.rooms[i].left is
   a real positive int (None = no operator-authored row for today; 0 =
   closed today via stop-sell -- neither is a count worth flagging). */
.ds-room__left { font-size: 11px; color: var(--ds-warn); font-weight: 600; margin: 2px 0 8px; }
.ds-room__side [data-ds-room-select] { margin-top: 8px; }
/* decorative selected-state checkmark, driven purely by the CLASS
   static/js/detail_system.js's initRooms() already toggles -- CSS
   generated content is skipped by screen readers in every mainstream
   AT/browser pairing, so this never needs the aria-hidden-wrapper dance
   _host.html's own "Identity verified" checkmark needed for a real DOM
   node; the button's own visible text ("Selected" vs "Select", swapped
   by JS from this partial's own server-rendered strings -- never a
   hardcoded English literal in JS) is what a screen reader announces. */
.ds-room--sel [data-ds-room-select]::before { content: "✓ "; }

/* ---------- amenities (W2-T9) ----------
   The stay page's own listing-level ds.amenities (list[str] -- distinct
   from a room's per-card amenity chips above, .ds-room__amen): a plain
   flex-wrap row of .ds-chip pills, inlined directly in
   templates/catalog/stay_detail_v2.html (no earlier task built a
   reusable partial for this -- same "inlined, not its own partial"
   precedent templates/catalog/_experience_detail_v2.html's own About/
   Included sections already set). */
.ds-amenities { display: flex; flex-wrap: wrap; gap: 8px; }

/* ---------- inquiry modal (W2-T9) ----------
   Backs templates/catalog/detail/_stay_inquiry_modal.html, opened by the
   host card's Message control (_host.html, W2-T7:
   data-open-modal="stayInquiry") via static/js/stay_inquiry.js.
   Structurally mirrors the site-wide .ag-share-modal component
   (static/css/components.css -- already proven in this exact v2
   assembly via partials/internal_share_modal.html: backdrop + centered
   dialog + close button) rather than legacy's own .ag-modal, which
   turned out NOT to be a safe reuse -- verified before assuming so: its
   real backdrop/dialog/close rules exist only inside components.css's
   `@media (max-width: 600px)` mobile-override block, with no base
   (>600px) rule in any sitewide sheet, so borrowing that class name here
   would have shipped an unstyled shell above 600px wide. .ds-modal reads
   colors from THIS file's own --ds-* bridge instead of --ag-color-*, so
   a modal opened from a v2 page looks like part of the v2 system. */
.ds-modal { position: fixed; inset: 0; z-index: 10000; display: none; align-items: center; justify-content: center; padding: 16px; }
.ds-modal.is-open { display: flex; }
.ds-modal__backdrop { position: absolute; inset: 0; background: rgba(30, 40, 74, .45); }
.ds-modal__dialog { position: relative; background: var(--ds-card-bg); border-radius: var(--ds-r-lg); max-width: 440px; width: 100%; max-height: 85vh; overflow-y: auto; box-shadow: var(--ds-shadow-2); padding: 24px; }
.ds-modal__close { position: absolute; top: 14px; right: 14px; border: 0; background: none; font-size: 22px; line-height: 1; color: var(--ds-ink-3); cursor: pointer; }
.ds-modal__close:hover { color: var(--ds-ink); }
.ds-modal__success { text-align: center; padding: 20px 0; color: var(--ds-ink-2); font-size: 14px; }
.ds-modal h4 { font-size: 17px; font-weight: 700; color: var(--ds-navy); margin: 0 28px 6px 0; }
.ds-modal p { font-size: 13px; color: var(--ds-ink-2); margin: 0 0 16px; }
.ds-modal .ds-field { margin-bottom: 12px; }
.ds-modal .ds-field label { display: block; font-size: 12.5px; font-weight: 600; color: var(--ds-ink); margin-bottom: 5px; }
.ds-modal .ds-field input,
.ds-modal .ds-field textarea {
  width: 100%; box-sizing: border-box; padding: 10px 12px; border: 1.5px solid var(--ds-line);
  border-radius: var(--ds-r-sm); font: inherit; font-size: 14px; color: var(--ds-ink); background: var(--ds-card-bg);
}
.ds-modal .ds-field input:focus,
.ds-modal .ds-field textarea:focus { outline: none; border-color: var(--ds-blue); }
/* field-error text -- a NEW ds- class, not legacy's .ag-inline-error:
   that class lives only in static/catalog/css/activity_detail.css/
   tour_detail.css (page-specific sheets this v2 page never loads), so
   reusing its name here would render as unstyled, always-visible-once-
   populated text instead of the intended hidden-until-shown, --ds-bad
   -tinted box. Sourced from the same --ds-bad/--ds-bad-soft tokens
   .ds-ic--no already uses, not legacy's hardcoded #fecaca/#fff5f5 hex. */
.ds-field-error { display: none; margin-top: 6px; padding: 8px 10px; border-radius: var(--ds-r-sm); border: 1px solid var(--ds-bad); background: var(--ds-bad-soft); color: var(--ds-bad); font-size: 12.5px; }
.ds-field-error.is-visible { display: block; }

/* ---------- review-write modal (car/transfer only, W3-T7) ----------
   Backs templates/catalog/detail/_review_write_modal.html, opened by the
   reviews module's own "Write a review" control (_reviews.html,
   [data-ds-review-write]) via AGDetail.initReviewWrite()
   (static/js/detail_system.js). Reuses .ds-modal/.ds-field/.ds-field-error
   wholesale (the W2-T9 primitives directly above -- already generic, no
   stay-specific rule among them) -- only the star-rating input below is
   new.

   .ds-starinput is a pure-CSS 5-star radio picker: five (input, label)
   pairs in REVERSE DOM order (values 5,4,3,2,1), `row-reverse`'d back to
   display 1..5 left-to-right, so the general-sibling combinator (~,
   "every matching element AFTER this one in the DOM") lights up the
   correct count of stars from one :checked rule with zero JS -- a
   well-established CSS-only star-rating technique, expressed here with
   this file's own --ds-* tokens instead of a new hex. Deliberately does
   NOT reuse .ds-revsum__stars (that component is DISPLAY-only -- a fixed
   five-glyph base string clipped by a computed-percentage overlay, built
   for a fractional AVERAGE like 4.5; an input needs five discrete,
   individually clickable/focusable targets, which a clipped-overlay
   display can never provide).

   Known, deliberate simplification: the hover rule below only ever ADDS
   filled stars on top of whatever :checked already fills, it never
   temporarily "shrinks" the display while hovering a LOWER star than the
   current selection the way legacy _rating_form.html's own JS
   (highlightStars(), reset on mouseleave) does. A pure-CSS equivalent of
   that exact behavior needs a second, higher-specificity suppression rule
   and was judged not worth the added complexity for a cosmetic-only
   hover nuance -- the actual submitted VALUE is unaffected either way
   (a click always selects exactly the star clicked, via the native radio
   input, regardless of this). */
.ds-starinput { display: flex; flex-direction: row-reverse; justify-content: flex-end; gap: 2px; }
.ds-starinput input { position: absolute; opacity: 0; width: 1px; height: 1px; overflow: hidden; }
.ds-starinput label { font-size: 27px; line-height: 1; color: var(--ds-line); cursor: pointer; }
.ds-starinput input:checked ~ label,
.ds-starinput label:hover,
.ds-starinput label:hover ~ label { color: var(--ds-gold); }
.ds-starinput input:focus-visible + label { outline: 2px solid var(--ds-blue); outline-offset: 3px; border-radius: 4px; }

/* ---------- garage hero (car-only, W3-T2) ----------
   Replaces the live car page's own literal "🚗 No photo yet" string
   (templates/catalog/car_detail.html:46) -- production has real
   photo-less listings today (see this component's own template-level
   comment, templates/catalog/detail/_garage.html), so the no-photo branch
   is a designed gradient, not an error state, matching the .ds-postcard
   precedent this component is a visual sibling of.

   .ds-garage carries the shared shell (radius/overflow/min-height) and
   --ds-grad-horizon as its OWN background -- the same "photo-less
   identity" token .ds-postcard/.ds-host__ava/.ds-review__ava/
   .ds-rcard__media already reuse for this exact role. A real photo
   (.ds-garage__photo) simply layers on top via absolute positioning and
   visually replaces the gradient; there is no second placeholder-icon
   markup to keep in sync with it (the same "a plain gradient needs no
   second variant" reasoning .ds-rcard__media's own header comment already
   documents). The bottom legibility fade (::before, --ds-grad-dusk) is
   unconditional -- it has to keep the white title/chip text readable
   whether it's sitting over the gradient or over a real photo.

   .ds-garage__body is flex (W3-T10 restored this -- see below), matching
   the approved mockup's own .garage__body, which lays its text block out
   next to a "Host: add photos" CTA link: ds.garage.add_photos_url
   (apps.catalog.detail_context._car_add_photos_url) now gives that
   control a real destination (see templates/catalog/detail/_garage.html's
   own header comment and W3-T2-report.md for the full history -- W3-T2
   deliberately omitted the control and kept this rule block-flow because
   no URL existed yet; this task closes that gap). */
.ds-garage {
  position: relative; border-radius: var(--ds-r-xl); overflow: hidden;
  min-height: 300px; display: flex; align-items: flex-end;
  background: var(--ds-grad-horizon);
}
.ds-garage::before {
  content: ""; position: absolute; inset: auto 0 0 0; height: 130px;
  background: var(--ds-grad-dusk); z-index: 1;
}
.ds-garage__photo { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
/* decorative only (aria-hidden in the template) -- the real content is the
   title/spec chips below, same "the glyph carries no semantic meaning of
   its own" reasoning already used for .ds-room--sel's checkmark. The
   drop-shadow tint reuses navy decomposed to rgba(30,40,74), the same
   brand-tinted-shadow pattern --ds-shadow-1/2/rail already establish,
   rather than a generic un-tokenized slate. */
.ds-garage__glyph {
  position: absolute; right: 6%; top: 16%; font-size: 120px;
  filter: drop-shadow(0 24px 30px rgba(30, 40, 74, .5));
}
.ds-garage__body {
  position: relative; z-index: 2; padding: 24px 28px; color: #fff; width: 100%;
  display: flex; justify-content: space-between; align-items: flex-end; gap: 12px;
}
/* fix round 1 (Minor): the text block must claim the body's FULL
   available width whether or not the CTA sibling renders -- a flex item's
   own default width is shrink-to-fit its content, not "fill the parent",
   so without this rule EVERY car v2 render (the CTA only ever shows for
   the owning partner -- the common, non-owner case is everyone else)
   would have silently laid the title/spec-chips out narrower than the
   card, an untested visual regression the flex restore below introduced.
   flex:1 makes this item consume all space the CTA (fixed-width, see
   .ds-garage__cta below) doesn't need, reproducing the original
   block-flow "always full width" behavior in both states -- CTA absent
   (this item alone fills .ds-garage__body) and CTA present (this item
   fills everything up to the CTA's own width + the 12px gap).
   min-width:0 overrides flex's own default min-width:auto, the standard
   fix for a flex item with text content that would otherwise refuse to
   shrink below its longest unbreakable word and overflow the card on a
   narrow viewport. */
.ds-garage__info { flex: 1 1 auto; min-width: 0; }
/* the "Host: add photos" link (W3-T10) -- a semi-opaque white pad so a
   ghost-styled control stays legible over a real cover photo, matching
   the approved mockup's own inline background:rgba(255,255,255,.92) on
   this exact control (docs/superpowers/mockups/2026-07-28-services-detail/
   web-transport.html); flex-shrink:0 keeps it from being squeezed by a
   long title on a narrow viewport, matching .ds-garage__body's own
   align-items:flex-end anchoring. */
.ds-garage__cta { background: rgba(255, 255, 255, .92); flex-shrink: 0; }
.ds-garage__title { font-size: 30px; font-weight: 800; letter-spacing: -.02em; }
.ds-garage__specchips { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 10px; }
/* translucent-on-dark override of the base .ds-chip -- the same "reuse
   .ds-chip, override look via a scoped parent selector" pattern
   .ds-host__name .ds-chip / .ds-room__amen .ds-chip already establish, not
   a new global chip variant. */
.ds-garage__specchips .ds-chip {
  background: rgba(255, 255, 255, .14); border-color: rgba(255, 255, 255, .25); color: #fff;
  backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
}

/* ---------- extras (car-only, priced toggles, W3-T3) ----------
   Replaces car_detail.html's own read-only "Available extras" text list
   (CarExtra rows rendered as plain, unselectable text) -- see
   templates/catalog/detail/_extras.html's own header comment for the
   exact legacy line. Visual sibling of .ds-room (W2-T2): a bordered,
   selectable row; the selected look reuses the SAME --cta1-tinted
   border+glow .ds-room--sel already uses, not a new one-off look.
   Radius uses --ds-r-md (14px) rather than a new one-off literal --
   these rows are visually lighter-weight than the big room photo-cards
   (--ds-r-lg), closer to .ds-fact's own card weight. */
.ds-extra {
  display: flex; justify-content: space-between; align-items: center; gap: 14px;
  border: 1.5px solid var(--ds-line); border-radius: var(--ds-r-md);
  padding: 11px 14px; margin-bottom: 9px; background: var(--ds-card-bg); cursor: pointer;
  transition: border-color var(--ag-duration-fast) var(--ag-ease-default),
              box-shadow var(--ag-duration-fast) var(--ag-ease-default);
}
.ds-extra:hover { border-color: rgba(46, 116, 184, .4); }
.ds-extra--sel { border-color: var(--ds-cta1); box-shadow: 0 0 0 3px rgba(37, 99, 235, .14), var(--ds-shadow-1); }
.ds-extra__body { min-width: 0; }
.ds-extra__name { font-size: 13.5px; color: var(--ds-navy); display: block; }
.ds-extra__sub { font-size: 11.5px; color: var(--ds-ink-3); display: block; margin-top: 2px; }
.ds-extra__side { display: flex; align-items: center; gap: 12px; flex: 0 0 auto; }
.ds-extra__price { font-weight: 700; color: var(--ds-ink); font-size: 13.5px; }

/* the toggle -- a REAL <button> (focusable, [aria-pressed]-driven), not
   the approved mockup's own bare decorative <span class="tgl"> -- see
   _extras.html's own header comment for why. The knob's position is
   driven by the SAME [aria-pressed] attribute JS already has to set for
   accessibility -- one piece of state, not a second CSS class kept
   separately in sync with it. */
.ds-tgl {
  width: 40px; height: 23px; border-radius: var(--ds-r-pill); background: var(--ds-line);
  position: relative; flex: 0 0 40px; padding: 0; border: 0; cursor: pointer;
}
.ds-tgl__knob {
  position: absolute; top: 2.5px; left: 3px; width: 18px; height: 18px; border-radius: 50%;
  background: var(--ds-card-bg); box-shadow: 0 1px 4px rgba(30, 40, 74, .25);
}
.ds-tgl[aria-pressed="true"] { background: var(--ds-grad-cta); }
.ds-tgl[aria-pressed="true"] .ds-tgl__knob { left: auto; right: 3px; }

/* ---------- route strip + vehicle classes (transfer-only, W3-T4) ----------
   Ported from the approved mockup's own web-transport.html <style> block
   (.routestrip/.routestrip .pt/.routestrip .line -- this component's CSS
   lives in the TRANSPORT page's own inline mockup styles, not gloss.css,
   matching .ds-room's own "lives in the vertical's own mockup page, not
   the shared library" precedent), colors routed through the --ds-*
   bridge above.

   The mockup's own .routestrip .line::after renders a fixed "car" emoji
   glyph at the connector's midpoint -- DELIBERATELY DROPPED: Transfer.Type
   has 11 real values (ferry, camel, cable_car, helicopter, ...), and a
   hardcoded car glyph would misrepresent a non-car transfer -- see
   templates/catalog/detail/_route.html's own header comment for the full
   reasoning. A plain dashed connector line carries the same "A connects
   to B" meaning without picking a vehicle type. */
.ds-routestrip {
  display: flex; align-items: center; gap: 14px; padding: 18px 20px;
  border-radius: var(--ds-r-lg); background: var(--ds-card-bg); border: 1px solid var(--ds-line);
}
.ds-routestrip__pt { text-align: center; }
.ds-routestrip__pt b { display: block; font-size: 20px; font-weight: 800; color: var(--ds-navy); }
.ds-routestrip__pt span { font-size: 11px; color: var(--ds-ink-3); }
.ds-routestrip__pt--meta { margin-left: 6px; }
.ds-routestrip__pt--meta b { font-size: 15px; }
.ds-routestrip__line {
  flex: 1; height: 3px; border-radius: 3px;
  background: repeating-linear-gradient(90deg, var(--ds-cta1) 0 12px, transparent 12px 22px);
}

/* vehicle class cards -- a plain bordered info card (visual sibling of
   .ds-fact). FIX ROUND (review finding, Important; full story in
   _route.html's own header comment): this was originally a real
   .ds-room-style select control ("one drives the rail price"). The
   backend has no class/tier parameter anywhere in its checkout path, so
   a click that repriced the rail was showing a total nothing would ever
   charge. The card is INFORMATIONAL now, not interactive -- no cursor,
   no hover affordance, no select button. `--sel` still marks exactly one
   card, but as "the class included at the displayed price," a static
   server-rendered fact, never a click target. */
.ds-classes { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 10px; margin-top: 12px; }
.ds-class {
  background: var(--ds-card-bg); border: 1.5px solid var(--ds-line); border-radius: var(--ds-r-md);
  padding: 12px 14px;
}
.ds-class--sel { border-color: var(--ds-cta1); box-shadow: 0 0 0 3px rgba(37, 99, 235, .14), var(--ds-shadow-1); }
.ds-class .ds-dv { display: block; margin-bottom: 3px; }
.ds-class__price { display: block; font-size: 14.5px; font-weight: 600; color: var(--ds-ink); }
.ds-class__pax { font-weight: 500; color: var(--ds-ink-3); }
.ds-class__pax--alone { display: block; font-size: 13.5px; font-weight: 500; color: var(--ds-ink-3); }
.ds-class__tag { margin-top: 8px; }

/* ---------- boarding-pass hero (flight-only, W4-T2) ----------
   Ported from the approved mockup's own web-flight.html <style> block
   (.fpass/.fpass__top/.fpass__route/.fpass__apt/.fpass__arc/.fpass__meta),
   colors routed through the --ds-* bridge above -- the same "component
   CSS lives in the vertical's own mockup page, not gloss.css" precedent
   .ds-routestrip's own header comment already documents.

   .ds-fpass__top's own gradient is --ds-grad-horizon -- the SAME token
   .ds-garage/.ds-postcard already use for a brand-gradient surface, not a
   new one-off gradient. .ds-fpass__meta .ds-chip's translucent-glass
   override is byte-identical to .ds-garage__specchips .ds-chip's own
   (W3-T2) -- the established "chips over a gradient/photo header" look.

   .ds-fpass__status (the fares-status-chip slot) intentionally carries no
   `position` property at all (defaults to `static`): the whole point of
   this task's own commit message ("status chips below the route, overlap
   bug dead") is that this element is a normal, in-flow flex child of
   .ds-fpass__actions, never layered on top of .ds-fpass__route the way
   the live production bug this redesign replaces does. `white-space:
   normal` overrides the base .ds-chip's own `nowrap` -- this slot's own
   documented example copy ("Fares open closer to departure — get a fare
   alert below", the approved mockup's own text) is a full sentence, not a
   short fact, and must wrap rather than force the row wide or overflow on
   a narrow viewport. See templates/catalog/detail/_pass_hero.html's own
   header comment for the full decision record, including which of the
   mockup's own three action-row controls (share/save/route-on-globe) are
   real here and why the other two are deliberately omitted rather than
   shipped as dead or duplicated controls.

   Fix round 1 (Minor, review finding): .ds-fpass__arc::after restores the
   mockup's own plane glyph (content:"✈", web-flight.html's .fpass__arc
   ::after), dropped from the first submission with no rationale recorded
   anywhere -- a real gap, not a deliberate call, since every other
   deviation from the mockup in this block IS documented. On reflection
   this glyph should never have been dropped: it is NOT the same situation
   as _route.html's own car-emoji omission, which had a real data-honesty
   reason (Transfer.Type has 11 non-car values -- ferry/camel/helicopter/
   ..., so a fixed car glyph would misrepresent most real rows). Flight
   carries no such ambiguity -- every row on this vertical genuinely IS a
   flight, so a plane glyph never misrepresents anything; it is pure
   decorative branding (no data claim attached to it at all), matching the
   mockup's own "lean into the boarding-pass DNA" framing. Restored
   verbatim, positioned identically to the mockup (top:2px, left:47%,
   21px) above the dotted connector line (::before, top:50%) and clear of
   the duration-label span (bottom:-4px) -- the three never overlap. No
   explicit color set, same as the mockup's own rule: it inherits
   var(--ds-on-primary) from .ds-fpass__top through the cascade, matching
   .ds-fpass__apt b's own identical (color-less, inherited) treatment. */
.ds-fpass { border-radius: var(--ds-r-xl); overflow: hidden; box-shadow: var(--ds-shadow-2); background: var(--ds-card-bg); border: 1px solid var(--ds-line); }
.ds-fpass__top { background: var(--ds-grad-horizon); color: var(--ds-on-primary); padding: 24px 28px; position: relative; overflow: hidden; }
.ds-fpass__top::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: radial-gradient(420px 170px at 85% 0%, rgba(255, 255, 255, .22), transparent 60%);
}
.ds-fpass__route { display: flex; align-items: center; gap: 22px; flex-wrap: wrap; position: relative; z-index: 2; }
.ds-fpass__apt { text-align: left; }
.ds-fpass__apt--arr { text-align: right; }
.ds-fpass__apt b { display: block; font-size: 40px; font-weight: 800; letter-spacing: -.02em; line-height: 1; }
.ds-fpass__apt span { display: block; font-size: 11.5px; color: rgba(255, 255, 255, .78); letter-spacing: .08em; text-transform: uppercase; font-weight: 600; }
.ds-fpass__arc { flex: 1; min-width: 160px; position: relative; height: 44px; }
.ds-fpass__arc::before {
  content: ""; position: absolute; left: 0; right: 0; top: 50%; height: 2.5px; border-radius: 3px;
  background: repeating-linear-gradient(90deg, rgba(255, 255, 255, .85) 0 12px, transparent 12px 22px);
}
.ds-fpass__arc::after {
  content: "✈"; position: absolute; left: 47%; top: 2px; font-size: 21px;
}
.ds-fpass__arc span {
  position: absolute; left: 50%; bottom: -4px; transform: translateX(-50%);
  font-size: 11px; color: rgba(255, 255, 255, .85); font-weight: 600; letter-spacing: .06em; white-space: nowrap;
}
.ds-fpass__meta { display: flex; gap: 9px; flex-wrap: wrap; margin-top: 16px; position: relative; z-index: 2; }
.ds-fpass__meta .ds-chip {
  background: rgba(255, 255, 255, .14); border-color: rgba(255, 255, 255, .25); color: #fff;
  backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
}
.ds-fpass__actions { padding: 16px 22px; display: flex; gap: 8px; justify-content: space-between; align-items: center; flex-wrap: wrap; }
.ds-fpass__actionbtns { display: flex; gap: 8px; flex-wrap: wrap; }
.ds-fpass__status { white-space: normal; text-align: left; }

/* ---------- fare-class comparison cards (flight-only, W4-T3) ----------
   Ported from the approved mockup's own web-flight.html style block
   (.fare/.fare--sel/.fare h4/.fare .p/.fare ul/.fare li/.fare .seatsleft),
   colors routed through the --ds-* bridge above. This module is a REAL
   selector (unlike .ds-class above, informational-only since W3-T4's own
   fix round): apps.catalog.views.flight_checkout already prices a
   booking from a selected FlightCabin id today -- see _fares.html's own
   header comment for the full investigation. Card border/selected-glow
   reuses .ds-room--sel/.ds-extra--sel's own literal values, not a new
   one-off look.

   Two deliberate departures from the mockup, both documented in full in
   _fares.html's own header comment: (1) the perks list reuses the SAME
   .ds-ic/.ds-ic--ok/.ds-ic--no icon-badge component the "What's
   included" list already established, inside this module's own
   narrower, single-column .ds-fare__perks -- not the mockup's own bare
   li:before glyphs, and not the wider 2-column .ds-iconlist grid (sized
   for a longer "What's included" list, not a compact comparison card);
   (2) the seats-left pill is an in-flow header-row child, never
   absolutely positioned the way the mockup's own .seatsleft corner badge
   is -- this exact page has a real, named incident for exactly this
   failure class (the fares-status ribbon overlapping the DXB code,
   W4-T2's own commit message: "overlap bug dead"), so this pill removes
   the risk entirely rather than merely containing it. The "Selected"
   checkmark is CSS-driven off .ds-fare--sel, mirroring .ds-room--sel's
   own before-pseudo-element technique -- never baked into the
   translatable button text.

   Three seats-left tiers, not the mockup's own single always-warn
   .seatsleft: neutral (plenty), low (--ds-warn -- the legacy fare
   table's own already-shipped "seats_left <= 5" threshold,
   flight_detail.html, not an invented cutoff), and sold-out (--ds-bad,
   seats_left == 0 -- a real fact worth its own more severe color, not
   folded into "low"). */
.ds-fares { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 14px; margin-top: 12px; }
.ds-fare {
  border: 1.5px solid var(--ds-line); border-radius: var(--ds-r-lg); padding: 16px 18px;
  background: var(--ds-card-bg);
  transition: border-color var(--ag-duration-fast) var(--ag-ease-default),
              box-shadow var(--ag-duration-fast) var(--ag-ease-default);
}
.ds-fare--sel { border-color: var(--ds-cta1); box-shadow: 0 0 0 3px rgba(37, 99, 235, .14), var(--ds-shadow-1); }
.ds-fare--sel [data-ds-fare-select]::before { content: "✓ "; }
.ds-fare__head { display: flex; justify-content: space-between; align-items: flex-start; gap: 8px; }
.ds-fare__name { font-size: 15px; font-weight: 700; color: var(--ds-navy); }
.ds-fare__seats {
  flex: 0 0 auto; font-size: 10.5px; font-weight: 700; white-space: nowrap;
  background: var(--ds-line-soft); color: var(--ds-ink-2); border-radius: var(--ds-r-pill); padding: 3px 9px;
}
.ds-fare__seats--low { background: var(--ds-warn-soft); color: var(--ds-warn); }
.ds-fare__seats--none { background: var(--ds-bad-soft); color: var(--ds-bad); }
.ds-fare__price { font-size: 22px; font-weight: 800; color: var(--ds-navy); margin: 6px 0 2px; }
.ds-fare__price small { font-size: 11px; color: var(--ds-ink-3); font-weight: 500; }
.ds-fare__price--pending { font-size: 13px; font-weight: 600; color: var(--ds-ink-3); margin: 10px 0 2px; }
.ds-fare__perks { list-style: none; margin: 10px 0 12px; padding: 0; display: flex; flex-direction: column; gap: 7px; }
.ds-fare__perks li { display: flex; align-items: center; gap: 9px; font-size: 12.5px; color: var(--ds-ink-2); }
.ds-fare__perks .ds-ic { flex: 0 0 22px; height: 22px; font-size: 11px; border-radius: 7px; }

/* ---------- flight segments timeline (flight-only, W4-T6) ----------
   Restyles templates/catalog/partials/_flight_timeline.html's own
   .flight-tl family (static/styles.css:1621-1719) in ds- tokens --
   templates/catalog/detail/_segments.html's own header comment has the
   full "what changed and why" story, including why several of the
   approved mockup's own illustrative details (airport full names,
   terminal numbers, seat pitch/USB-C, a computed layover wait time) are
   deliberately not rendered at all -- nothing to restyle for facts no
   field backs.

   Connector line + dot treatment reuses .ds-itin's OWN established
   "sequence of real stops, connected by one line" look almost verbatim
   (--ds-grad-cta for the line, --ds-cta1 solid-ring dots) -- the closest
   existing precedent in this file for the identical shape, just laid
   out horizontally instead of vertically, at 2px the gradient reads as
   solid brand blue (.ds-itin's own comment already established this).
   Both segment dots share the IDENTICAL look rather than a two-tone
   dep/arr pair: the approved mockup's own one-off inline styles use a
   second color for the arrival dot, but this bridge deliberately never
   exposes a standalone --ds-cta2 token (.ds-itin's own comment
   explains why), and _flight_timeline.html's own real template output
   never actually draws that distinction either -- both its dep and arr
   points render the identical .flight-tl__dot class; its sibling
   .flight-tl__stop hollow variant exists in legacy CSS but is dead,
   never applied by that partial's own markup (confirmed by reading it
   directly) -- so this restyle does not invent a dep/arr distinction
   legacy itself never drew. */
.ds-segs { display: flex; flex-direction: column; }
.ds-seg__direction {
  font-size: 11px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase;
  color: var(--ds-ink-3); margin-bottom: 10px;
}
.ds-seg__divider { border-top: 1px solid var(--ds-line); margin: 4px 0 16px; }
.ds-seg__connection {
  display: flex; align-items: center; gap: 8px; padding: 2px 0 12px 2px;
  font-size: 12px; color: var(--ds-ink-3);
}
.ds-seg__connection-dot {
  width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0;
  background: var(--ds-card-bg); border: 2px solid var(--ds-cta1);
}
.ds-seg { display: flex; align-items: center; gap: 10px; }
.ds-seg__point { display: flex; flex-direction: column; align-items: center; min-width: 48px; flex-shrink: 0; }
.ds-seg__time { font-size: 15px; font-weight: 700; color: var(--ds-navy); line-height: 1.2; }
.ds-seg__iata {
  font-size: 11.5px; font-weight: 600; letter-spacing: .05em; text-transform: uppercase;
  color: var(--ds-ink-3);
}
.ds-seg__dot {
  width: 11px; height: 11px; border-radius: 50%; flex-shrink: 0;
  background: var(--ds-card-bg); border: 3px solid var(--ds-cta1);
  box-shadow: 0 0 0 3px rgba(37, 99, 235, .15);
}
.ds-seg__line {
  flex: 1; min-width: 54px; display: flex; flex-direction: column; align-items: center; gap: 3px;
}
.ds-seg__line::before {
  content: ""; display: block; width: 100%; height: 2px; border-radius: 2px;
  background: var(--ds-grad-cta);
}
.ds-seg__duration { font-size: 11px; color: var(--ds-ink-3); white-space: nowrap; }
.ds-seg__meta { font-size: 12px; color: var(--ds-ink-3); padding-left: 58px; margin: 4px 0 18px; }
.ds-seg__empty { font-size: 13.5px; color: var(--ds-ink-2); margin: 0; }

/* ---------- responsive ---------- */
@media (max-width: 1020px) {
  .ds-grid2 { grid-template-columns: 1fr; }
  .ds-rail { position: static; }
  .ds-revsum { grid-template-columns: 1fr; }
}
@media (max-width: 640px) {
  .ds-hero__grid { grid-template-columns: 1fr; }
  .ds-hero__ph--main { min-height: 230px; }
  .ds-iconlist { grid-template-columns: 1fr; }
  .ds-room { grid-template-columns: 96px 1fr auto; gap: 10px; padding: 10px; }
  .ds-garage { min-height: 220px; }
  .ds-garage__glyph { font-size: 80px; }
  .ds-fpass__top { padding: 20px 20px; }
  .ds-fpass__apt b { font-size: 28px; }
}

/* ---------- reduced motion kill-switch ---------- */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }
}

/* ---------- body opt-in: dock clearance (visual-audit G7) ----------
   Pages that render a sticky bottom booking surface (the reserve rail on
   desktop, or the .ds-bookbar mirror on mobile) add this class to <body>.
   It sets --page-fab-clearance, the same variable the floating Messages/
   Chatbot dock already reads (templates/social/chat_widget.html's
   .ag-chat, default 0px when unset) so the dock lifts clear instead of
   overlapping the rail/bookbar. Legacy detail pages set this variable
   inline per template; v2 templates use this one shared class instead. */
/* Scoped to where the bookbar actually renders. Unconditional, this lifted the
   floating chat dock on desktop too — where there is no bottom bar — parking it
   on top of the right-hand booking column. */
@media (max-width: 980px) {
  body.ds-has-bookbar { --page-fab-clearance: 96px; }
}
