/* =========================================================
   DISCOVER FEED (issue #30)
   Converted to Departure Fever (DESIGN.md names Discover explicitly among
   the surfaces the system is normative on) — converted whole, per the
   migration posture rule, not patched piecemeal. Shares this file with
   #shared-trip-screen/#user-profile-screen (/discover/:code, /u/:username), so
   those public pages pick up the same conversion for free.
   ========================================================= */

/* ── Screen shell ──────────────────────────────────────── */
#discover-screen, #shared-trip-screen, #user-profile-screen, #invite-screen, #pending-approval-screen {
  display: none;
  flex-direction: column;
  height: 100dvh;
  height: 100vh;
  overflow: hidden;
  /* Same halftone/wash ground as .home-screen and .app.active — <body>
     itself stays the retired grey (shared with any still-unconverted
     screen), so every converted top-level screen paints its own ground. */
  background-color: var(--df-ground);
  background-image: var(--df-halftone), var(--df-wash);
  background-size: 6px 6px, auto, auto;
  background-attachment: fixed;
  color: var(--df-ink);
  font-family: var(--df-sans);
}

/* Pinned Field Green, same bar the marketing nav and the in-app tab bar use
   (DESIGN.md: "a visitor meets the product's chrome before logging in and
   the landing → app transition has no seam") — not --vn-teal, which flips to
   mint at night and would be unreadable as a full-bleed plane. */
.discover-topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 var(--space-3xl);
  height: 60px;
  background: var(--topbar-bg);
  flex-shrink: 0;
}

/* Quiet icon control on a green field (DESIGN.md: "the quiet button becomes
   pale text at 85% opacity"). Kept at its natural ~24px glyph size but with
   the same ::before hit-area trick as .discover-like-btn below so the actual
   tap target still clears --tap-min without inflating the bar's height. */
.discover-back-btn {
  position: relative;
  background: none;
  border: none;
  cursor: pointer;
  color: var(--topbar-fg);
  font-size: 20px;
  min-width: var(--tap-min);
  min-height: var(--tap-min);
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background var(--dur-feedback) var(--df-ease);
}
.discover-back-btn:hover { background: rgba(255,255,255,0.14); }
.discover-back-btn:active { background: rgba(255,255,255,0.22); }

.discover-topbar-title {
  font-family: var(--df-display);
  font-size: var(--fs-xl);
  font-weight: 800;
  letter-spacing: -0.02em;
  color: var(--topbar-fg);
}

/* ── Scrollable body ───────────────────────────────────── */
.discover-body {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  padding: var(--space-3xl);
  position: relative;
}

/* ── Masonry grid (CSS columns) ────────────────────────── */
.discover-grid {
  columns: 2;
  column-gap: var(--space-2xl);
}

/* ── Card: the stub ────────────────────────────────────────
   Same geometry and press states as .home-trip-card (My Trips) — Cut Stock,
   2px ink keyline, 12px radius, hard offset shadow, the stub pressing into
   its own shadow on tap. A photo-led social card is still a pressable stub
   here, not the inert "card" component (that one doesn't move). */
.discover-card {
  position: relative;
  break-inside: avoid;
  margin-bottom: var(--space-2xl);
  background: var(--df-panel);
  border: 2px solid var(--df-ink);
  border-radius: var(--df-radius-stub);
  box-shadow: var(--df-press);
  overflow: hidden;
  transition: transform var(--dur-feedback) var(--df-ease),
              box-shadow var(--dur-feedback) var(--df-ease);
}
.discover-card:hover {
  transform: translate(2px, 2px);
  box-shadow: var(--df-press-hover);
}
/* :active covers the ad card (a single <a> — the whole pressable element);
   :has() covers a trip card, where the actual pressable element is the
   nested .discover-card-open button. */
.discover-card:active,
.discover-card:has(.discover-card-open:active) {
  transform: translate(4px, 4px);
  box-shadow: var(--df-press-active);
}
/* A loading skeleton is not pressable — it must not offer the stub's press. */
.discover-card-skeleton:hover,
.discover-card-skeleton:active {
  transform: none;
  box-shadow: var(--df-press);
}

/* The card's main action — everything except the like/save row below. A
   real <button> (not the whole card as a <div onclick>), same convention as
   .home-trip-card-open: reachable by keyboard, announced to a screen reader.
   Like/save are sibling buttons rather than nested inside, since a <button>
   may not contain another. */
.discover-card-open {
  display: block;
  width: 100%;
  background: none;
  border: none;
  appearance: none;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
  padding: 0;
}

.discover-card-img {
  width: 100%;
  display: block;
  object-fit: cover;
  /* height varies by photo aspect ratio — masonry effect */
  min-height: 80px;
  max-height: 280px;
}

/* No cover photo: a deliberate typographic mark, not an emoji standing in
   for a flag. Windows Chrome ships no flag glyphs at all — DISCOVER_FLAGS'
   regional-indicator pairs silently render as raw letters there, fine next
   to a readable country name (the eyebrow below) but unreadable alone at
   this size. Setting the country code in Martian Mono on purpose means
   every visitor sees the same intentional mark instead of some seeing a
   flag and others seeing an unexplained "TH". */
.discover-card-img-placeholder {
  width: 100%;
  min-height: 100px;
  background: var(--vn-teal-light);
  display: flex;
  align-items: center;
  justify-content: center;
}
.discover-card-cc {
  font-family: var(--df-mono);
  font-size: var(--fs-3xl);
  font-weight: 700;
  letter-spacing: 0.08em;
  color: var(--vn-teal);
}

.discover-card-body {
  padding: var(--space-2xl) var(--space-2xl) var(--space-xl);
}

/* DESIGN.md names this exact call site: "Trail Green ... Discover eyebrows." */
.discover-card-destination {
  font-family: var(--df-mono);
  font-size: var(--fs-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--vn-teal);
  margin-bottom: var(--space-2xs);
}

.discover-card-name {
  font-family: var(--df-display);
  font-size: var(--fs-lg);
  font-weight: 800;
  letter-spacing: -0.02em;
  color: var(--df-ink);
  line-height: 1.2;
  margin-bottom: var(--space-xs);
}

.discover-card-meta {
  font-size: var(--fs-sm);
  color: var(--df-ink-soft);
  margin-bottom: var(--space-sm);
}

.discover-card-host {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  font-size: var(--fs-sm);
  color: var(--df-ink-soft);
}

.discover-card-avatar {
  width: 18px;
  height: 18px;
  border-radius: 50%;
  object-fit: cover;
  flex-shrink: 0;
}

.discover-card-avatar-init {
  background: var(--vn-teal);
  color: var(--df-brand-fg);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--fs-xs);
  font-weight: 700;
}

/* ── Search bar (issue #77) ─────────────────────────────── */
.discover-search-row {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  margin-bottom: var(--space-2xl);
}

/* Scoped to this one field rather than the shared global input rule (which
   doesn't even cover type="search" — this control was silently on raw
   browser UA styling before). Deliberately not touching the app-wide input
   rule: that's shared infrastructure well outside Discover's own surface. */
.discover-search-input {
  width: 100%;
  margin-bottom: 0;
  height: var(--tap-min);
  padding: 0 var(--space-2xl);
  font-family: var(--df-sans);
  font-size: var(--fs-md);
  color: var(--df-ink);
  background: var(--df-panel);
  border: 2px solid var(--df-ink);
  border-radius: var(--df-radius-stub);
  box-sizing: border-box;
}
.discover-search-input::placeholder { color: var(--df-ink-soft); }
.discover-search-input:focus {
  outline: none;
  box-shadow: 0 0 0 3px var(--vn-teal), 0 0 0 5px var(--df-panel);
}
@media (max-width: 680px) {
  /* iOS Safari auto-zoom floor — same reasoning as the shared input rule. */
  .discover-search-input { font-size: var(--fs-lg); }
}

/* ── Saved filter toggle (issue #193, /discover/saved) ────── */
.discover-saved-toggle {
  display: inline-flex;
  align-items: center;
  gap: var(--space-sm);
  flex-shrink: 0;
  min-height: var(--tap-min);
  background: var(--df-panel);
  border: 2px solid var(--df-ink);
  border-radius: var(--radius-pill);
  padding: 0 var(--space-2xl);
  font-family: var(--df-sans);
  font-size: var(--fs-md);
  font-weight: 600;
  color: var(--df-ink);
  cursor: pointer;
  box-shadow: var(--df-press);
  transition: transform var(--dur-feedback) var(--df-ease),
              box-shadow var(--dur-feedback) var(--df-ease);
}
.discover-saved-toggle:hover {
  transform: translate(2px, 2px);
  box-shadow: var(--df-press-hover);
}
.discover-saved-toggle:active {
  transform: translate(4px, 4px);
  box-shadow: var(--df-press-active);
}
.discover-saved-toggle.active {
  background: var(--vn-teal);
  border-color: var(--vn-teal);
  color: var(--df-brand-fg);
}

/* ── Like/save actions (issue #76) ─────────────────────────── */
.discover-card-actions {
  display: flex;
  align-items: center;
  gap: var(--space-lg);
  padding: var(--space-lg) var(--space-2xl) var(--space-xl);
  border-top: 2px solid var(--df-hair);
}

/* Visible box stays small to fit the card footer — the ::before pseudo
   expands the actual tappable area without inflating the row, same
   technique as .stop-menu-btn (css/style.css). Capped at 32px rather than
   the full --tap-min (44px): these two buttons sit only 10px apart
   (.discover-card-actions' gap), and a full 44px zone on each would
   overlap enough to make taps near the boundary land on the wrong one —
   32px still clears the WCAG 2.2 AA 24×24 floor with room to spare. */
.discover-like-btn, .discover-save-btn, .discover-share-btn {
  background: none;
  border: none;
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  padding: var(--space-2xs);
  font-family: var(--df-sans);
  font-size: var(--fs-md);
  color: var(--df-ink-soft);
  position: relative;
}
.discover-like-btn::before, .discover-save-btn::before, .discover-share-btn::before {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: max(32px, 100%);
  height: max(32px, 100%);
  transform: translate(-50%, -50%);
}

.discover-like-btn.active { color: var(--vn-coral-mid); }
.discover-save-btn.active { color: var(--vn-amber-mid); }
.discover-like-count { font-size: var(--fs-sm); font-variant-numeric: tabular-nums; }
.discover-save-btn { margin-left: auto; }

/* ── Sponsored placements (issue #79) ──────────────────────── */
.discover-ad-card {
  display: block;
  color: inherit;
  text-decoration: none;
  cursor: pointer;
}

.discover-ad-tag {
  font-family: var(--df-mono);
  font-size: var(--fs-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--df-ink-soft);
  margin-bottom: var(--space-2xs);
}

/* ── Guest wall ─────────────────────────────────────────── */
#discover-guest-wall, #shared-trip-cta, #user-profile-cta {
  display: none;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: var(--space-4xl) var(--space-3xl);
  margin-top: var(--space-2xl);
  background: var(--df-panel);
  border: 2px solid var(--df-ink);
  border-radius: var(--df-radius-card);
  box-shadow: var(--df-press-large);
  text-align: center;
  gap: var(--space-md);
}

/* Shared by all three guest walls (#discover-guest-wall, #shared-trip-cta,
   #user-profile-cta) — a drawn Tabler glyph, never an emoji. The three used
   to print ✈️/🗺️/🗺️, which render at a different weight, size and baseline
   on every platform and are read aloud as "airplane"/"world map" by a screen
   reader on top of a title that already says it; the glyph is decorative and
   marked aria-hidden in the markup instead. Same reasoning already written
   down for PLACE_TYPES (js/state.js) and the feed card's country-code
   placeholder above. Discover's wall takes ti-telescope, the icon My Trips'
   own Discover action card uses; the two sign-up CTAs take ti-map-2, the
   product's logo mark — which is what the map emoji was reaching for anyway.

   Ink, not green: this glyph isn't pressable, and green in this system is
   the one signal for something that is (the button below it). Matches
   .dtl-empty's icon, so the two states a visitor can meet on this same
   screen agree. */
.discover-guest-wall-icon {
  font-size: var(--fs-4xl);
  line-height: 1;
  color: var(--df-ink-soft);
  margin-bottom: var(--space-xs);
}

.discover-guest-wall-title {
  font-family: var(--df-display);
  font-size: var(--fs-xl);
  font-weight: 800;
  letter-spacing: -0.02em;
  color: var(--df-ink);
}

.discover-guest-wall-sub {
  font-size: var(--fs-md);
  color: var(--df-ink-soft);
  margin-bottom: var(--space-xs);
}

/* ── Pending approval screen (issue #628) ─────────────────── */
.pending-approval-status {
  font-size: var(--fs-md);
  color: var(--df-ink-soft);
  margin: var(--space-xl) 0;
}

/* ── Empty / loading states ────────────────────────────── */
/* #discover-loading shares .discover-grid's masonry columns (set in the
   HTML class list) so its skeleton cards line up with the real feed one
   layout swap later — see renderDiscoverLoadingSkeleton() in discover.js. */
#discover-loading { display: none; }

#discover-empty {
  display: none;
  text-align: center;
  padding: var(--space-5xl) var(--space-3xl);
  color: var(--df-ink-soft);
  font-size: var(--fs-md);
}

#shared-trip-error, #user-profile-error, #invite-error {
  display: none;
  text-align: center;
  padding: var(--space-5xl) var(--space-3xl);
  color: var(--df-ink-soft);
  font-size: var(--fs-md);
}

/* renderInviteError() (js/invite.js) gives this one a ti-ticket-off icon —
   the same ticket the rest of this screen draws, struck through, rather
   than a generic warning glyph. Matches .dtl-empty's icon-then-message
   shape (below) so the two dead-ends a visitor can meet on this trip agree. */
#invite-error i {
  display: block;
  font-size: var(--fs-4xl);
  color: var(--df-ink-soft);
}
#invite-error p {
  max-width: 34ch;
  margin: var(--space-xl) auto 0;
  line-height: 1.6;
}

/* #674 PR4 (D7) — the dead end's "who to ask" line and its one CTA
   (.invite-claim-btn, already a full-width sticker button — capped here so
   it doesn't stretch to this padded screen's full width). */
#invite-error .invite-dead-end-hint {
  max-width: 34ch;
  margin: var(--space-md) auto 0;
  font-size: var(--fs-sm);
}
#invite-error .invite-claim-btn {
  max-width: 320px;
  margin: var(--space-2xl) auto 0;
}

#shared-trip-loading, #user-profile-loading, #invite-loading {
  display: none;
  padding: var(--space-4xl);
}

/* ── Invite preview (issue #553/#564, /i/:token) ──────────
   renderInvitePreview() (js/invite.js). This is the single highest-intent
   public page in the product — a named person inviting you by trip, opened
   cold with zero account friction — so unlike the read-only /discover/:code
   masthead it doesn't get to be plainer, only smaller: it reuses the exact
   same .dtl stub/stamp composition below (2026-08-22 critique P0/P1), wrapped
   in its own .dtl scope so it inherits that block's --dtl-stock/--dtl-tape
   custom properties without redeclaring them. */
.invite-dtl { margin-bottom: 0; }
.invite-dtl .dtl-stub { margin-bottom: var(--space-3xl); }

/* The counterfoil — the part of a real ticket you actually tear off and
   hand over. Invited-by, any note, the name field and the claim button now
   live inside the same .dtl-stub card, behind their own perforation, rather
   than as loose boxes on the page ground: accepting reads as completing the
   ticket, not filling out a form stapled underneath it. Bleeds to the
   stub's own edges and back up under its bottom padding, the same technique
   .dtl-facts uses elsewhere in this file. */
.invite-counterfoil {
  margin: var(--space-4xl) calc(-1 * var(--space-4xl)) calc(-1 * var(--space-4xl));
  padding: var(--space-2xl) var(--space-4xl) var(--space-4xl);
  border-top: 2px dashed var(--df-ink);
}

.invite-invited-by {
  color: var(--df-ink-soft);
  font-size: var(--fs-sm);
  margin-bottom: var(--space-lg);
}

/* Neutral notes inside the counterfoil — a punch down to the page's own
   --df-ground rather than the stub's --df-panel fill, same convention
   .pack-item-progress (css/style.css) uses for a chip nested in a card. 2px
   --df-ink keyline and the 12px stub radius, not the 1px/8px legacy values
   this rule shipped with until the critique above: a half-converted panel
   this close under a fully converted stub is exactly the "reads as a bug,
   not a transition" case DESIGN.md's migration posture calls out by name. */
.invite-seat-name, .invite-approval-note {
  background: var(--df-ground);
  border: 2px solid var(--df-ink);
  border-radius: var(--df-radius-stub);
  padding: var(--space-lg) var(--space-xl);
  font-size: var(--fs-sm);
  margin-bottom: var(--space-md);
}

/* The cover photo as an Instant Print, full width and first in the stub —
   unlike the corner-tucked .dtl-print stack on /discover/:code, this is the
   one photo whose job is to make someone want to go, not a supporting
   thumbnail, so it earns the larger of the system's two Instant Print sizes.
   Reuses .dtl's own --dtl-stock/--dtl-tape tokens and DESIGN.md's Lift shadow
   rather than inventing a third depth model. */
.invite-print {
  position: relative;
  margin: var(--space-2xl) 0 0;
  padding: 10px 10px 30px;
  background: var(--dtl-stock);
  border-radius: var(--df-radius-print);
  box-shadow: var(--df-press-lift);
  transform: rotate(-1.4deg);
}
.invite-print::before,
.invite-print::after {
  content: '';
  position: absolute;
  top: -7px;
  width: 46px;
  height: 15px;
  background: var(--dtl-tape);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.25);
}
.invite-print::before { left: 10%; transform: rotate(-4deg); }
.invite-print::after { right: 10%; transform: rotate(3.4deg); }
.invite-print img {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  border-radius: 1px;
  background: color-mix(in srgb, var(--df-ink) 12%, var(--dtl-stock));
}

/* The name field, scoped the same way .discover-search-input is above rather
   than touching the shared legacy input rule — that's infrastructure well
   outside this one screen. */
#invite-name-input {
  width: 100%;
  height: var(--tap-min);
  padding: 0 var(--space-2xl);
  font-family: var(--df-sans);
  font-size: var(--fs-md);
  color: var(--df-ink);
  background: var(--df-panel);
  border: 2px solid var(--df-ink);
  border-radius: var(--df-radius-stub);
  box-sizing: border-box;
}
#invite-name-input:focus {
  outline: none;
  box-shadow: 0 0 0 3px var(--vn-teal), 0 0 0 5px var(--df-panel);
}
/* A seat invite prefills and locks this field (js/invite.js) — a muted fill
   is the only signal that it isn't editable, since a native `readonly`
   otherwise looks identical to an empty, typeable field at a glance. */
#invite-name-input[readonly] {
  background: var(--df-ground);
  color: var(--df-ink-soft);
  cursor: default;
}
@media (max-width: 680px) {
  /* iOS Safari auto-zoom floor — same reasoning as the shared input rule. */
  #invite-name-input { font-size: var(--fs-lg); }
}

/* The one button this whole page exists to deliver (DESIGN.md's Creative
   North Star: "make someone want to go, then hand them the button") — its
   own full sticker component rather than the retired flat .btn-primary this
   rule used to just resize. Matches button-large: full pill, 2px ink
   keyline, hard offset shadow, Trail Green fill with --df-brand-fg
   foreground (never literal white — the Mint Flip Rule, #432). */
.invite-claim-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  min-height: 56px;
  margin-top: var(--space-lg);
  padding: 0 30px;
  border: 2px solid var(--df-ink);
  border-radius: var(--radius-pill);
  background: var(--vn-teal);
  color: var(--df-brand-fg);
  font-family: var(--df-sans);
  font-size: var(--fs-md);
  font-weight: 700;
  cursor: pointer;
  box-shadow: var(--df-press-large);
  transition: transform var(--dur-feedback) var(--df-ease), box-shadow var(--dur-feedback) var(--df-ease), background var(--dur-feedback) var(--df-ease);
}
.invite-claim-btn:hover { background: var(--vn-teal-mid); transform: translate(2px, 2px); box-shadow: var(--df-press-hover); }
.invite-claim-btn:active { transform: translate(4px, 4px); box-shadow: var(--df-press-active); }
.invite-claim-btn:disabled { opacity: 0.55; cursor: default; transform: none; box-shadow: var(--df-press-large); }

/* ── Identity fork (#674 PR2) ──────────────────────────────
   A signed-in visitor's own account, or a guest's upgrade offer — states
   which identity is about to take the seat, before it's taken (D4). */
.invite-identity {
  font-size: var(--fs-sm);
  color: var(--df-ink-soft);
  margin-bottom: var(--space-lg);
  line-height: 1.6;
}
.invite-identity strong { color: var(--df-ink); }

/* A plain text control, not a button-shaped one — "Not you? Switch
   account", "Create a free account instead", and the demoted "Continue as
   guest" door all read as asides next to the counterfoil's real buttons,
   never competing with them for weight. */
.invite-link-btn {
  padding: 0;
  border: 0;
  background: none;
  font-family: inherit;
  font-size: inherit;
  font-weight: 700;
  color: var(--vn-teal-mid);
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 2px;
}
.invite-link-btn:hover { color: var(--vn-teal); }

/* The demoted guest door (D3) sits on its own line, centered like the
   buttons above it rather than inline like the identity-region asides. */
.invite-guest-link {
  display: block;
  width: 100%;
  text-align: center;
  min-height: var(--tap-min);
  margin-top: var(--space-sm);
}

/* Stated once, at the point of choice (#674 PR2 screen spec) — right above
   the guest door it explains, not buried in a modal a different flow owns. */
.invite-guest-limits {
  color: var(--df-ink-soft);
  font-size: var(--fs-xs);
  line-height: 1.6;
  margin: var(--space-lg) 0 0;
  text-align: center;
}

/* The account doors' secondary button — same sticker shape as
   .invite-claim-btn (2px ink keyline, hard offset shadow) but an outline
   fill so it reads as the second-choice action next to a teal primary,
   never a second primary. */
.invite-secondary-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  min-height: 56px;
  margin-top: var(--space-md);
  padding: 0 30px;
  border: 2px solid var(--df-ink);
  border-radius: var(--radius-pill);
  background: var(--df-panel);
  color: var(--df-ink);
  font-family: var(--df-sans);
  font-size: var(--fs-md);
  font-weight: 700;
  cursor: pointer;
  box-shadow: var(--df-press);
  transition: transform var(--dur-feedback) var(--df-ease), box-shadow var(--dur-feedback) var(--df-ease), background var(--dur-feedback) var(--df-ease);
}
.invite-secondary-btn:hover { background: var(--df-ground); transform: translate(2px, 2px); box-shadow: var(--df-press-hover); }
.invite-secondary-btn:active { transform: translate(4px, 4px); box-shadow: var(--df-press-active); }

/* ── Action card (home footer) ────────────────────────────
   The indigo hover fill and indigo icon that used to live here were removed
   when My Trips converted to Departure Fever (css/style.css, .home-action-card).
   Three footer buttons hovering to three different hues spent the palette's
   colour *assignments* — green means pressable, coral means stamped, amber
   means routed, indigo codes the hotel category — on decoration, and read as
   three co-equal actions when New Trip is the primary. The three now share
   the system's press feedback, with the green fill on New Trip alone.
   Nothing replaces these rules; this file simply stops overriding a
   converted screen. */

/* ── Trip details ──────────────────────────────────────────
   renderDiscoverDetail() (js/discover.js) backs the public /discover/:code
   page (js/shared-trip.js) — what both a shared link and a tap on a Discover
   card open as of #659, replacing the separate Discover detail modal this
   comment used to describe. This was the last Field-Journal seam left on a
   converted screen: a fully converted feed that opened a soft-shadowed,
   system-font panel on top of itself, which the migration posture calls out
   by name as worse than either look alone.

   The composition is a ticket someone kept. A stub carries the trip name and
   the destination struck across it as a rubber stamp — the ONE shout on this
   screen, the same die and the same landing as the landing hero's
   .lp-stampword — with the trip's facts printed below a torn perforation and
   a small stack of taped instant prints beside them. The itinerary below is
   DESIGN.md's dashed route made literal: each day is an amber waypoint pin
   on a dashed amber line, because "a numbered sequence renders as a journey
   drawn on a map" is exactly what a day-by-day itinerary is.

   Photographs are the point. Every Places-picked stop resolves a real Google
   Places photo through /api/geo/places-photo (see discoverStopPhotoSrc() in
   js/discover.js), so this screen shows the trip rather than tabulating it. */

.dtl {
  /* Instant Stock and its fine-print ink. Pinned in BOTH themes on purpose,
     the same way css/landing.css pins --lp-stock: a photographic print is
     white at midnight too, which is what makes the prints the brightest
     objects on the night page — correct, for photographs of somewhere sunny.
     Anything set on this stock therefore stays dark in both themes. */
  --dtl-stock: #fdf6ea;
  --dtl-stock-ink: #5f6b63;
  --dtl-tape: rgba(186, 117, 23, 0.42);
  /* The second ink pass landing out of register behind the stamp. */
  --dtl-ghost: color-mix(in srgb, var(--vn-teal) 30%, transparent);
  --dtl-slam: cubic-bezier(0.34, 1.56, 0.64, 1);
  /* Route Amber Ink — pinned dark because --vn-amber itself LIGHTENS after
     dark, so a token that followed the theme would put pale ink on a pale
     fill. This is the value DESIGN.md names for copy set on an amber fill. */
  --dtl-pin-ink: #1c1509;
  --dtl-pin-size: 30px;
  /* DESIGN.md's Lift, proportioned down for a row-sized print. The sanctioned
     blurred shadow is written for a hero print; its 40px blur at -26px spread
     simply washes out under a 64px thumbnail, so the same two layers are kept
     at thumbnail scale rather than swapping in a different depth model. */
  --dtl-lift-sm: 0 1px 0 rgba(9, 20, 13, 0.1), 0 9px 16px -11px rgba(9, 20, 13, 0.7);

  /* Queried below instead of the viewport — the ~900px /discover/:code page
     (this renderer's one remaining call site since #659 retired the narrower
     ~536px modal it used to also back) still varies its own width by
     surrounding layout, and a viewport media query can't see that. */
  container-type: inline-size;
  container-name: dtl;

  font-family: var(--df-sans);
  color: var(--df-ink);
}

@media (prefers-color-scheme: dark) {
  /* Only the tape changes — the stock is pinned above by design, and every
     brand ink is reached through a --vn-* token that already flips. */
  .dtl { --dtl-tape: rgba(232, 163, 61, 0.28); }
}

/* ── The stub ──────────────────────────────────────────── */
.dtl-stub {
  position: relative;
  background: var(--df-panel);
  border: 2px solid var(--df-ink);
  border-radius: var(--df-radius-stub);
  box-shadow: var(--df-press-large);
  padding: var(--space-4xl);
  margin-bottom: var(--space-5xl);
  /* A taped print sits at an angle; clipping it to the stub is what keeps
     the tilt from pushing a horizontal scrollbar into the modal. */
  overflow: hidden;
}

.dtl-name {
  font-family: var(--df-display);
  font-size: var(--fs-2xl);
  font-weight: 800;
  line-height: 1.1;
  letter-spacing: -0.02em;
  text-wrap: balance;
  color: var(--df-ink);
}

/* Its own line, always — the same load-bearing rule as the landing hero's
   stamp. Sharing a line with the name would re-wrap the whole masthead
   around a word whose length nobody controls. */
.dtl-stampline {
  margin-top: var(--space-2xl);
  padding-left: var(--space-md);
}

.dtl-stamp {
  display: inline-block;
  max-width: 100%;
  padding: 0.04em 0.2em 0.09em;
  border: 0.055em solid currentColor;
  border-radius: 0.14em;
  box-shadow: inset 0 0 0 0.022em currentColor;
  font-family: var(--df-display);
  font-size: clamp(1.75rem, 8vw, 2.625rem);
  font-weight: 800;
  line-height: 0.98;
  letter-spacing: -0.03em;
  text-transform: uppercase;
  text-wrap: balance;
  color: var(--vn-coral);
  text-shadow: 0.028em 0.036em 0 var(--dtl-ghost);
  /* The resting transform lives here, NOT in the keyframes — an interrupted
     or unsupported animation still has to leave the word correctly struck. */
  transform: rotate(-2.6deg);
  transform-origin: 30% 60%;
}

/* ── The print stack ───────────────────────────────────── */
/* Fanned rather than laid out in a row: this is a handful of photos dropped
   on a table, and the overlap is what makes it read as a stack instead of a
   gallery. Draws nothing of its own, so a trip whose photos all 404 (the
   onerror in discoverDetailPrints() removes the whole print) collapses to an
   empty box rather than three broken frames. */
.dtl-prints {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  align-items: flex-start;
  /* Capped and pushed to the trailing edge rather than centred and full
     width. The stamp is the one shout on this screen; a photo stack running
     the stub's whole width outweighs it, and two things shouting is DESIGN.md's
     definition of half the energy, not double. Tucked into the stub's bottom
     corner it reads as what it is — the photos someone kept with the ticket. */
  max-width: 232px;
  margin-left: auto;
  margin-top: var(--space-4xl);
  padding-top: var(--space-md);
}

.dtl-print {
  position: relative;
  flex: 0 1 34%;
  min-width: 68px;
  max-width: 92px;
  margin: 0;
  padding: 6px 6px 20px;
  background: var(--dtl-stock);
  border-radius: var(--df-radius-print);
  box-shadow: var(--df-press-lift);
  transition: transform var(--dur-state) var(--df-ease);
}
/* Tilt cycled by position, and each print overlaps the one before it. The
   angle is not decoration: two torn strips of tape hold it there. */
.dtl-print:nth-child(1) { transform: rotate(-3.4deg); }
.dtl-print:nth-child(2) { transform: rotate(2.2deg); }
.dtl-print:nth-child(3) { transform: rotate(-1.4deg); }
.dtl-print + .dtl-print { margin-left: -26px; }

/* Tape. */
.dtl-print::before,
.dtl-print::after {
  content: '';
  position: absolute;
  top: -6px;
  width: 38px;
  height: 13px;
  background: var(--dtl-tape);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.25);
}
.dtl-print::before { left: 8%; transform: rotate(-4deg); }
.dtl-print::after { right: 8%; transform: rotate(3.4deg); }

.dtl-print img {
  display: block;
  width: 100%;
  aspect-ratio: 4 / 5;
  object-fit: cover;
  border-radius: 1px;
  background: color-mix(in srgb, var(--df-ink) 12%, var(--dtl-stock));
}
/* Format is rhythm — the same aspect-ratio cycle the landing wall runs, cut
   to three because that is how many prints this stack holds. */
.dtl-print:nth-child(2) img { aspect-ratio: 1 / 1; }
.dtl-print:nth-child(3) img { aspect-ratio: 5 / 4; }

/* What you actually do when you look at a crooked photo on a wall: you
   straighten it. Raising z-index is what makes the fanned stack browsable
   rather than only decorative. */
.dtl-print:hover {
  transform: rotate(0deg) scale(1.05);
  z-index: 2;
}

/* Google Places requires the source to be credited wherever its photos are
   shown. Per-photographer names need the authenticated
   /geo/places-photo-meta lookup, which /discover/:code has no session for —
   so the credit names the source, set as fine print under the stack, which
   is the slot DESIGN.md reserves for exactly this line. */
.dtl-print-credit {
  flex-basis: 100%;
  margin-top: var(--space-xl);
  font-family: var(--df-mono);
  font-size: var(--fs-xs);
  letter-spacing: 0.08em;
  text-align: right;
  color: var(--df-ink-soft);
}

/* ── The torn perforation and the ticket's facts ────────── */
.dtl-facts {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-lg) var(--space-4xl);
  /* Bleeds to the stub's full width, then re-adds its padding, so the tear
     runs edge to edge the way a real perforation does. */
  margin: var(--space-4xl) calc(-1 * var(--space-4xl)) 0;
  padding: var(--space-2xl) var(--space-4xl) 0;
  border-top: 2px dashed var(--df-ink);
}

.dtl-fact { min-width: 0; }

.dtl-fact dt {
  font-family: var(--df-mono);
  font-size: var(--fs-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--df-ink-soft);
}

.dtl-fact dd {
  margin: var(--space-xs) 0 0;
  font-family: var(--df-mono);
  font-size: var(--fs-base);
  font-weight: 700;
  color: var(--df-ink);
}

/* ── Sections ──────────────────────────────────────────── */
.discover-detail-section {
  margin-bottom: var(--space-5xl);
}

.discover-detail-section-title {
  margin: 0 0 var(--space-2xl);
  font-family: var(--df-mono);
  font-size: var(--fs-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--df-ink-soft);
}

/* ── The route ─────────────────────────────────────────── */
.dtl-days {
  list-style: none;
  margin: 0;
  padding: 0;
}

.discover-detail-day {
  position: relative;
  display: grid;
  grid-template-columns: var(--dtl-pin-size) minmax(0, 1fr);
  gap: var(--space-xl);
  padding-bottom: var(--space-4xl);
}
.discover-detail-day:last-child { padding-bottom: 0; }

/* The dashed route, running from under this pin to the next one. It
   terminates ON the last pin rather than past it — a route ends at its final
   waypoint. */
.discover-detail-day::before {
  content: '';
  position: absolute;
  top: var(--dtl-pin-size);
  bottom: 0;
  left: calc(var(--dtl-pin-size) / 2 - 1.5px);
  width: 3px;
  background: repeating-linear-gradient(
    to bottom,
    var(--vn-amber) 0 9px,
    transparent 9px 18px
  );
}
.discover-detail-day:last-child::before { display: none; }

/* Amber, not green: a waypoint takes the colour of the route it sits on, and
   green in this system means "you can press this". */
.dtl-pin {
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--dtl-pin-size);
  height: var(--dtl-pin-size);
  border: 2px solid var(--df-ink);
  border-radius: 50%;
  background: var(--vn-amber);
  color: var(--dtl-pin-ink);
  font-family: var(--df-display);
  font-size: var(--fs-md);
  font-weight: 800;
  line-height: 1;
}
.dtl-pin i { font-size: var(--fs-base); }

.dtl-day-body { min-width: 0; }

.discover-detail-day-label {
  font-family: var(--df-display);
  font-size: var(--fs-lg);
  font-weight: 800;
  letter-spacing: -0.02em;
  line-height: var(--dtl-pin-size);
  color: var(--df-ink);
}

/* Issue #154 Phase 3 — sub-heading for a plan section within a day the host
   opted into publicVisible. Screen-only surface, so color is fine here
   (unlike the PDF export, which is label-text-only for grayscale printing). */
.discover-detail-plan-label {
  font-family: var(--df-mono);
  font-size: var(--fs-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  margin: var(--space-xl) 0 var(--space-xs);
  padding-left: var(--space-md);
  border-left: 3px solid transparent;
}
.discover-detail-plan-label.plan-a { color: var(--plan-a-accent); border-left-color: var(--plan-a-accent); }
.discover-detail-plan-label.plan-b { color: var(--vn-sky-text); border-left-color: var(--vn-sky); }
.discover-detail-plan-label.plan-shared { color: var(--df-ink-soft); border-left-color: var(--df-hair); }

/* ── A stop ────────────────────────────────────────────── */
.discover-detail-stop {
  display: flex;
  align-items: flex-start;
  gap: var(--space-xl);
  padding: var(--space-lg) 0;
  border-bottom: 2px solid var(--df-hair);
}
.discover-detail-stop:last-child { border-bottom: none; }

.dtl-stop-main {
  flex: 1;
  min-width: 0;
}

.dtl-stop-head {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  flex-wrap: wrap;
}

.dtl-stop-name {
  flex: 1;
  /* Not min-width: 0 — that lets the name shrink to a sliver beside the time
     on a narrow row. A floor here makes the TIME wrap to its own line
     instead, which is the right thing to give up first. */
  min-width: 9ch;
  font-size: var(--fs-md);
  font-weight: 600;
  color: var(--df-ink);
}

/* Martian Mono: a time printed on a ticket, not a sentence. Stays on the
   trailing edge whether it shares the name's line or wraps below it. */
.discover-detail-stop-time {
  flex-shrink: 0;
  margin-left: auto;
  font-family: var(--df-mono);
  font-size: var(--fs-xs);
  color: var(--df-ink-soft);
  font-variant-numeric: tabular-nums;
}

.discover-detail-stop-note {
  margin-top: var(--space-xs);
  font-size: var(--fs-sm);
  line-height: 1.5;
  color: var(--df-ink-soft);
}

/* A print, straight — this one is tucked into a list, not taped to a wall,
   and DESIGN.md's Crooked-On-Purpose Rule straightens anything with no
   material reason to tilt. */
.dtl-stop-thumb {
  flex-shrink: 0;
  width: 64px;
  height: 64px;
  object-fit: cover;
  padding: 3px 3px 8px;
  background: var(--dtl-stock);
  border-radius: var(--df-radius-print);
  box-shadow: var(--dtl-lift-sm);
}

/* ── The ledger ────────────────────────────────────────── */
.discover-detail-expenses {
  background: var(--df-ground);
  border: 2px solid var(--df-ink);
  border-radius: var(--df-radius-stub);
  overflow: hidden;
}

.discover-detail-expense-row {
  display: flex;
  justify-content: space-between;
  gap: var(--space-xl);
  padding: var(--space-md) var(--space-2xl);
  font-size: var(--fs-md);
  border-bottom: 2px solid var(--df-hair);
}
.discover-detail-expense-row:last-child { border-bottom: none; }

.discover-detail-expense-total {
  font-family: var(--df-display);
  font-size: var(--fs-lg);
  font-weight: 800;
  letter-spacing: -0.02em;
  color: var(--df-ink);
  background: var(--df-panel);
  border-top: 2px solid var(--df-ink);
}

.discover-expense-cat-group { border-bottom: 2px solid var(--df-hair); }
.discover-expense-cat-group:last-of-type { border-bottom: none; }

.discover-expense-cat-header {
  display: flex;
  justify-content: space-between;
  gap: var(--space-xl);
  padding: var(--space-md) var(--space-2xl);
  font-family: var(--df-mono);
  font-size: var(--fs-xs);
  font-weight: 700;
  color: var(--df-ink-soft);
  background: var(--df-panel);
  text-transform: uppercase;
  letter-spacing: 0.08em;
}
.discover-expense-cat-subtotal { font-weight: 700; font-variant-numeric: tabular-nums; }

.discover-expense-item {
  padding-left: var(--space-5xl);
  color: var(--df-ink-soft);
}
.discover-expense-desc { font-size: var(--fs-sm); }
.discover-expense-amt { font-size: var(--fs-sm); font-variant-numeric: tabular-nums; }
.discover-expense-amt.masked { color: var(--df-ink-soft); font-style: italic; }

/* The category glyph carries its own hue by reusing the shared .cat-* class,
   which is the single source of truth for category colour — but that class
   is written for a pill, so its tint background is dropped here and only the
   `color` half is kept. Tabler glyphs throughout: the emoji this ledger used
   to print rendered at a different weight and size on every platform, and on
   Windows Chrome several of them did not render at all — the same gap the
   feed card's country-code placeholder is already written around. */
.dtl-ledger-cat {
  display: inline-flex;
  align-items: center;
  gap: var(--space-md);
  min-width: 0;
}
.dtl-ledger-cat i {
  background: none;
  padding: 0;
  font-size: var(--fs-lg);
  line-height: 1;
}

/* ── Actions ───────────────────────────────────────────── */
.discover-detail-actions {
  display: flex;
  gap: var(--space-xl);
  margin-top: var(--space-4xl);
  padding-top: var(--space-4xl);
  border-top: 2px dashed var(--df-ink);
}

/* ── Nothing shared yet ────────────────────────────────── */
.dtl-empty {
  padding: var(--space-5xl) var(--space-2xl);
  text-align: center;
  color: var(--df-ink-soft);
}
.dtl-empty i {
  font-size: var(--fs-4xl);
  color: var(--df-ink-soft);
}
.dtl-empty p {
  max-width: 34ch;
  margin: var(--space-xl) auto 0;
  font-size: var(--fs-md);
  line-height: 1.6;
}

/* ── Motion ────────────────────────────────────────────── */
/* One authored moment on this screen: the destination lands as a struck
   stamp when the details open. Guarded behind no-preference so a reader who
   opted out never has a hidden start state that JS would have to undo — the
   resting transform in .dtl-stamp is what it lands on either way. */
@media (prefers-reduced-motion: no-preference) {
  .dtl-stamp.is-struck { animation: dtl-strike 420ms var(--dtl-slam) both; }
}

@keyframes dtl-strike {
  0%   { opacity: 0; transform: rotate(-11deg) scale(1.85); }
  55%  { opacity: 1; transform: rotate(-2.6deg) scale(0.95); }
  78%  { transform: rotate(-2.6deg) scale(1.025); }
  100% { transform: rotate(-2.6deg) scale(1); }
}

/* Everything that travels resolves to its FINISHED state, never a hidden
   one. A print's resting tilt is layout, not motion — it stays, which is why
   the hover here lands back on each print's own resting angle rather than on
   `none`, which would flatten the stack. */
@media (prefers-reduced-motion: reduce) {
  .dtl-print { transition: none; }
  .dtl-print:nth-child(1):hover { transform: rotate(-3.4deg); }
  .dtl-print:nth-child(2):hover { transform: rotate(2.2deg); }
  .dtl-print:nth-child(3):hover { transform: rotate(-1.4deg); }
}

/* ── Wider than a phone ──────────────────────────────────
   The stub folds into two columns: the ticket (name + struck destination) on
   the left, the print stack squared up beside it rather than stranded below
   with a field of empty stock between them. Facts always take their own row
   under the perforation — including when there are no prints at all, which
   is why they span rather than flow. */
@container dtl (min-width: 460px) {
  .dtl-stub {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    column-gap: var(--space-4xl);
    align-items: start;
  }
  .dtl-name { font-size: var(--fs-3xl); }
  .dtl-prints {
    /* Proportional, not a flat cap: a flat 280px left the trip name a 173px
       column and broke it over three lines at the narrower widths the
       retired Discover modal used to render at (#659), while the same value
       is right on the ~860px /discover/:code page this now always renders
       at. cqw keeps the ticket half of the masthead readable regardless. */
    max-width: min(280px, 44cqw);
    margin-top: var(--space-md);
  }
  .dtl-print { min-width: 84px; max-width: 124px; }
  .dtl-facts { grid-column: 1 / -1; }
  .dtl-stop-thumb { width: 76px; height: 76px; }
  /* .invite-print and .invite-counterfoil (js/invite.js) both otherwise land
     in this grid's first column only, one row after the name+stamp row —
     .invite-counterfoil happened to still look full-bleed because its own
     negative side margins overflow an empty second column, but that's an
     accident of column 2 being unoccupied in that particular row, not an
     actual span; explicit is correct regardless of what else is on the row. */
  .invite-print,
  .invite-counterfoil { grid-column: 1 / -1; }
}

/* ── Desktop tweaks ─────────────────────────────────────── */
@media (min-width: 680px) {
  .discover-grid { columns: 3; }
  /* width:100% matters here, not just max-width — .discover-body is a flex
     item (column-direction parent, #discover-screen/#shared-trip-screen/
     #user-profile-screen), and a flex item with `margin: auto` on its cross
     axis but no explicit width shrinks to its content's natural width
     instead of stretching, then centers that shrunk box — a real bug found
     via /u/:username (issue #196): a short list-row with justify-content:
     space-between visually bunched together because its container had
     collapsed to ~300px instead of stretching up to 900px. Discover's own
     masonry grid never surfaced this — its multi-column content is wide
     enough to reach 900px on its own regardless. */
  .discover-body { max-width: 900px; width: 100%; margin: 0 auto; }
}

@media (min-width: 1024px) {
  .discover-grid { columns: 4; }
}
