  /* ── Friends-in-sun card (replaces .social-friends row) ──────────────── */
  .friends-photo-chip-row {
    display: flex;
    align-items: center;
    gap: var(--space-lg);
    padding: var(--space-lg) var(--space-lg);
    border-radius: var(--radius-md);
    background: rgba(17,30,56,0.42);
    border: 1px solid rgba(245,194,94,0.42);
    box-shadow:
      inset 0 1px 0 rgba(255,242,235,0.15),
      0 4px 14px rgba(0,0,0,0.25);
  }
  .friends-photo-chip-row .chip-summary {
    display: flex;
    align-items: center;
    gap: var(--space-lg);
    flex: 1;
    min-width: 0;
  }
  .friends-photo-chip-row .chip-text { min-width: 0; flex: 1; }
  .friends-photo-chip-row .chip-title {
    font-family: 'Inter', sans-serif;
    font-size: 14px;
    font-weight: 700;
    color: var(--text);
    line-height: 1.3;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  .friends-photo-chip-row .chip-sub {
    font-family: 'Inter', sans-serif;
    font-size: 12px;
    font-weight: 500;
    color: var(--accent);
    margin-top: var(--space-2xs);
  }

  /* Avatar row used by both the photo chip and the friends card */
  .avatar-row { display: inline-flex; }
  .avatar-row .avatar {
    border-radius: 50%;
    box-shadow: 0 0 0 2px rgba(17,30,56, 0.95);
    margin-left: calc(var(--space-sm) * -1);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    object-fit: cover;
    flex-shrink: 0;
    color: #2a1a0c;
    font-weight: 700;
    font-family: 'Inter', sans-serif;
  }
  .avatar-row .avatar:first-child { margin-left: 0; }
  .avatar-row.sm .avatar { margin-left: calc(var(--space-xs) * -1); }
  .avatar-row .avatar.avatar-overflow {
    background: #111E38;
    color: var(--text);
  }

  /* ── Detail-panel "card": same DOM as a venue-card, scaled up ────────── */
  /* The layout (card-top → card-left + card-right + timeline) is identical to
     a venue-card — only sizes scale up. This keeps the user oriented after the
     morph: they see "the same card, bigger". */
  /* Detail-panel card: hybrid of compact list card (name+meta+v2 pills with
     anchor meta) plus the rich timeline bar at the bottom — no fill bar. */
  .dp-card {
    cursor: default;
    margin-bottom: var(--space-lg);
    /* Uniform padding (12) on every side — same inset top/bottom/left/right as
       the other cards, so the eyebrow sits an equal distance from top + left. */
    padding: var(--space-lg);
    animation: none;
    border-radius: var(--radius-md);
    overflow: visible;  /* re-allow timeline to render outside compact's clip */
  }
  .dp-card.card-compact { padding-bottom: var(--space-lg); overflow: visible; }
  .dp-card.card-compact .card-fillbar { display: none; }
  .dp-card:hover { background: var(--surface-content); }   /* kill list-card hover */
  .dp-card .card-top { gap: var(--space-lg); align-items: flex-start; }
  .dp-card .card-left { min-width: 0; gap: var(--space-xs); }
  /* Title is the venue takeover — bumped from 18 → 22 so it reads as
     hierarchically primary in the detail context (vs the same renderer
     producing a list card at 16px elsewhere). */
  .dp-card .card-new-name {
    /* Display tier (DESIGN.md) — the venue-takeover title, Inter 900 tight,
       echoing the logo mark. */
    font-size: var(--text-display);
    font-weight: var(--fw-display);
    line-height: 1.15;
    letter-spacing: var(--tracking-display);
  }
  .dp-card .card-new-meta {
    font-size: 12.5px;
    line-height: 1.4;
    margin-top: var(--space-xs);
  }
  .dp-card .card-right {
    min-width: 0;
    flex-shrink: 0;
    gap: var(--space-2xs);
    align-items: flex-end;
  }
  .dp-card .card-new-hero-main {
    font-size: 16px;
    line-height: 1.22;
  }
  .dp-card .card-new-hero-sub  {
    font-size: 12px;
    line-height: 1.35;
    margin-top: 1px;
  }
  /* Timeline block sits at the bottom — tight 4px gap below the pills row. */
  .dp-card .card-timeline-block { margin-top: var(--space-xs); }

  /* dp-card timeline matches the list card's thin variant (8px). The tall
     38px treatment is gone now that the FTS pill is no longer reparented
     into the card. */

  /* Reserves layout space in the panel before openDetailPanel replaces
     it with a freshly-rendered .dp-card. Height matches a typical .dp-card
     (card-top ~50 + card-timeline 8 + small margin + padding 18+14). */
  .dp-card-slot {
    height: 100px;
    margin-bottom: var(--space-lg);
    position: relative;
  }

  /* Sol-retning section */

  /* Info rows (busyness, noise, hours) — open layout, no heavy box */
  .info-list {
    margin-bottom: var(--space-lg);
  }

  .info-row {
    display: flex;
    align-items: center;
    gap: var(--space-lg);
    padding: var(--space-md) var(--space-xs);
    border-bottom: 1px solid rgba(17,30,56,0.08);
  }

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

  .info-icon {
    width: 28px;
    height: 28px;
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--text-secondary); /* cream — the detail panel is dark Delft; ink was invisible */
    flex-shrink: 0;
    opacity: 1;
  }

  .info-icon svg {
    width: 20px;
    height: 20px;
    stroke: currentColor;
    color: currentColor;
  }

  .info-label {
    flex: 1;
    font-size: 14px;
  }

  .info-label-strong {
    font-weight: 600;
    color: var(--panel-text);
  }

  .info-label-sub {
    font-size: 12px;
    color: var(--ink-muted);
    margin-top: var(--space-2xs);
  }

  .info-value {
    color: var(--panel-text);
    font-size: var(--text-label);
    font-weight: 600;
    text-align: right;
  }

  /* Footer buttons — text links, low visual weight */
  .secondary-row {
    display: flex;
    gap: var(--space-xl);
    justify-content: center;
    padding: var(--space-lg) 0 var(--space-xl);
    margin-top: var(--space-xs);
  }

  .secondary-link {
    font-size: 12px;
    font-weight: 500;
    color: var(--ink-muted);
    padding: var(--space-xs) 0;
    background: none;
    border: none;
    cursor: pointer;
    font-family: 'Inter', sans-serif;
    opacity: 1;
    transition: opacity 0.15s, color 0.15s;
  }

  .secondary-link:hover {
    opacity: 1;
    color: var(--panel-text);
  }

  @media (max-width: 639px) {
    #detail-panel {
      position: fixed;
      top: auto;
      left: 0; right: 0;
      bottom: 0;
      width: 100%;
      /* Half-open: fixed 58svh — calibrated on the S25 so the panel's
         visible bottom lands right at the directions button. Content
         below (share/heart/bell row, address, busyness, noise, hours)
         is in the DOM and reachable via internal scroll OR by dragging
         the panel up into .dp-fullscreen. svh keeps the cap stable
         against browser-chrome show/hide. Per-device fine-tuning is
         intentional: 58svh ≈ "shows directions button" on the user's
         primary device; other devices may show slightly more or less,
         which the scroll handles. */
      height: 58svh;
      max-height: 58svh;
      border-radius: var(--radius-lg) var(--radius-lg) 0 0;
      border-left: none; border-right: none; border-bottom: none;
      transform: translateY(100%) translateZ(0);
      opacity: 1;
      z-index: 920;
      transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), height 0.3s ease, border-radius 0.3s ease;
      will-change: transform, height;
      backface-visibility: hidden;
      /* Isolation contract — drag-to-dismiss + scroll trap (Vaul / Radix
         pattern). pan-y allows vertical gestures only; contain prevents
         scroll bubbling to the map underneath. */
      overscroll-behavior: contain;
      touch-action: pan-y;
    }
    #detail-panel.open { transform: translateY(0); }
    /* When the calendar is open, slide the detail panel back down
       (translateY 100% + opacity 0) so the calendar bottom-sheet has
       a clean stage. Matched timing to the calendar's slide-up. */
    body.cal-open #detail-panel.open {
      transform: translateY(100%) translateZ(0);
      opacity: 0;
      pointer-events: none;
      transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1),
                  opacity 0.3s ease;
    }
    /* Fade out the FTS slider too — it was visually marooned mid-screen
       because its --fts-bottom is anchored to the detail panel's open
       state and doesn't recompute during the swap. */
    body.cal-open #fts {
      opacity: 0;
      pointer-events: none;
      transition: opacity 0.22s ease;
    }
    /* Also hide the locate-btn + zoom-jog so the only floating thing
       above the calendar is the calendar itself. */
    body.cal-open #locate-btn,
    body.cal-open #zoom-jog {
      opacity: 0;
      pointer-events: none;
      transition: opacity 0.22s ease;
    }
    /* Invite sheet behaves the same way the detail panel does when the
       calendar opens — slide back down out of view so the calendar has
       a clean stage, then slide back up once the calendar closes (the
       body.cal-open class is removed, the transform reverts to the
       .open state's translateY(0)). The backdrop fades to avoid
       stacking with the calendar's own backdrop. */
    body.cal-open .dpinvite-sheet.open {
      transform: translateY(100%);
      opacity: 0;
      pointer-events: none;
      transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1),
                  opacity 0.3s ease;
    }
    body.cal-open .dpinvite-backdrop.open {
      opacity: 0;
      pointer-events: none;
      transition: opacity 0.22s ease;
    }
    #detail-panel.dp-fullscreen {
      height: var(--app-h, 100svh);
      border-radius: 0;
    }

    #dp-handle { display: flex; }
    #dp-scroll {
      padding-top: var(--space-xs);
      /* Bottom padding clears the FTS slider so the last content row
         doesn't disappear behind it during drag. Reduced from 88px to
         48px — the popup-above-thumb's full 30px clearance was
         overkill since the popup only overlaps the panel during active
         drag, not at scroll-rest. 48px = FTS height (40) + small
         buffer (8). User-reported "wasted space at scroll bottom"
         was this 88px floor. */
      padding-bottom: var(--space-4xl);
    }
    .dp-photos { margin-top: 0; }
    /* Back button style on mobile: chevron + "Venues" */
    #dp-close-btn {
      display: flex;
      align-items: center;
      gap: var(--space-xs);
      font-size: 12px;
      font-weight: 600;
      color: rgba(17,30,56,0.55);
      background: rgba(17,30,56,0.08);
      border: 1px solid rgba(17,30,56,0.18);
      border-radius: var(--radius-sm);
      padding: var(--space-xs) var(--space-md);
      width: auto;
      height: auto;
    }
    #dp-close-btn::before { content: '‹'; font-size: 16px; line-height: 1; }
    .dp-close-x    { display: none; }
    .dp-close-back { display: inline; }
    .dp-venue-name { font-size: var(--text-title); }
    #dp-scroll { overflow-y: auto; }
  }

  /* ── Intro splash overlay ─────────────────────────────────────────────────── */
  #splash {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 9997;
    background: #FFF2EB;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--space-3xl);
    /* Matches the loader's Phase 4 crossfade (PHASE4_MS=600) so the cream
       backdrop and the SVG fade out together; onComplete then sets .done. The
       longer fade also lets the map (which only develops tiles as the opaque
       cover lifts) finish rendering while the fading cream still masks it. */
    transition: background-color 0.6s ease;
    pointer-events: all;
    cursor: default;
  }
  #splash.bg-out {
    background-color: transparent;
    pointer-events: none;
  }
  #splash.done {
    pointer-events: none;
    visibility: hidden;
  }
  /* Shades Loader mount — the 4-phase brand launch animation (createShadesLoader,
     js/shades-loader.js). Self-colors (ink/cream/sun) + owns reduced-motion.
     This box is sized to land the SVG mark PIXEL-ALIGNED with the native iOS
     splash so the native→web hand-off is seamless. The native splash is the
     Splash.imageset (cream + mark) shown scaleAspectFill, height-driven on
     portrait (the mark scales with screen height). We replicate it with vh
     units, derived from the shrunk native asset (mark ~30% screen width — the
     small, centred size large apps use): sun baseline at 55.8% of image height,
     sun ⌀ 13.85% of width, mark centred. Mapping the loader viewBox (240×280,
     sun ⌀190u baseline at y=200) onto that gives a 17.5vh × 20.4vh box (aspect
     240:280) with top at 41.2vh. The loader sets the SVG to width/height:100%
     inline, so it fills this box exactly (no letterbox) and the mark lands on
     the native one. .fade-out = instant-hide fallback. */
  #splash-loader {
    position: fixed;
    top: 41.2vh;
    left: 50%;
    width: 17.5vh;
    height: 20.4vh;
    transform: translateX(-50%);
    transition: opacity 0.3s ease;
  }
  #splash-loader.fade-out {
    opacity: 0;
  }

  /* Elements hidden during intro sequence */
  .intro-hidden {
    opacity: 0;
    pointer-events: none !important;
  }

  /* ── Sliding surfaces — will-change only on panels that translate ────────── */
  /* No will-change: backdrop-filter anywhere — it bloats GPU memory.          */
  #detail-panel, #ptb-cal-float { will-change: transform; }
  /* #panel was previously hinted will-change: transform, which on iOS
     WebKit forced a compositor layer that lost backdrop-filter during
     transform animations. The panel now animates via bottom + height
     (layout properties) instead, so no compositor hint is needed. */

  /* Register --fts-bottom as a typed custom property so transitions on
     `bottom: calc(var(--fts-bottom) + ...)` actually animate when the var
     changes. Without @property, changing a CSS custom property updates
     dependent computed values instantly — locate-btn / zoom-jog were
     jumping to their new offsets on panel state change instead of sliding
     with the panel. iOS 16.4+ / Chrome 85+ both support @property. */
  @property --fts-bottom {
    syntax: '<length>';
    inherits: true;
    initial-value: 160px;
  }
  /* Same treatment for --peek-h so the no-fts fallback path on locate-btn
     (which reads var(--peek-h) directly) also transitions. */
  @property --peek-h {
    syntax: '<length>';
    inherits: true;
    initial-value: 252px;
  }

  /* ── Performance & accessibility — reduced transparency ─────────────────── */
  @media (prefers-reduced-transparency: reduce) {
    .glass-panel, .glass-card, .glass-action,
    #panel, #detail-panel, #search-dropdown,
    #profile-panel, #sort-panel, #hover-tooltip, #map-toast,
    #app-toast, .venue-card, .floating-card, .mapboxgl-popup-content,
    #qc-panel-inner, #edit-banner {
      backdrop-filter: none;
      -webkit-backdrop-filter: none;
      background: rgba(17, 30, 56, 0.97);
    }
  }

  /* ── Favorites heart button (card) ──────────────────────────────────────── */
  /* Passive heart indicator on venue card name */
  .card-fav-heart {
    margin-left: var(--space-xs);
    vertical-align: -1px;
    flex-shrink: 0;
  }

  /* (fav-filter-pill removed — favorites is now a sort mode) */

  /* (fav + bell buttons moved to .dp-header-icon) */

  /* ── Toast notification ─────────────────────────────────────────────────── */
  #app-toast {
    position: fixed;
    bottom: 80px;
    left: 50%;
    transform: translateX(-50%) translateY(20px);
    background: rgba(17,30,56,0.95);
    border: 1px solid var(--border);
    color: var(--text);
    padding: var(--space-md) var(--space-xl);
    border-radius: var(--radius-md);
    font-size: var(--text-label);
    font-family: 'Inter', sans-serif;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.2s, transform 0.2s;
    z-index: 9999;
    backdrop-filter: blur(12px);
    -webkit-backdrop-filter: blur(12px);
  }
  #app-toast.show, #app-toast.visible {
    opacity: 1;
    transform: translateX(-50%) translateY(0);
  }

  /* ── Social section (check-in, plans) ───────────────────────────────────── */
  /* No glass wrapper — sits directly on the panel so the dp-card stays the focal box. */
  .social-card {
    margin-bottom: var(--space-lg);
    display: flex;
    flex-direction: column;
    gap: var(--space-sm);
  }

  /* Primary CTA — Invite friends. Composes the canonical Primary role
     (.p-pill: 44h, 999r, accent fill, 14/700, base shadow + canonical
     hover/active). This class carries ONLY the two legitimate adaptations:
     (1) full-width — the detail panel uses the row exclusively for this one
     CTA, so 100% reads as "this is THE action"; (2) the shimmer sheen below.
     Everything else (fill, height, radius, font, hover, active) is inherited
     from .p-pill — no per-flow re-implementation. "I'm here" semantics live
     inside the invite sheet's time picker (±30 min of now → presence flip). */
  .dp-invite-cta {
    width: 100%;
    /* Height comes from .p-pill (now the 48px standard). overflow:hidden clips
       the sheen to the pill shape; position:relative is the positioning
       context for the ::after sheen layer. */
    position: relative;
    overflow: hidden;
  }
  .dp-invite-cta::after {
    content: '';
    position: absolute;
    inset: 0;
    background: linear-gradient(
      100deg,
      transparent 0%,
      transparent 35%,
      rgba(255,255,255,0.50) 50%,
      transparent 65%,
      transparent 100%
    );
    transform: translateX(-110%);
    pointer-events: none;
    animation: dp-invite-sheen 14500ms cubic-bezier(0.4, 0, 0.2, 1) infinite;
    z-index: 1;
  }
  /* The icon + label stay above the sheen layer */
  .dp-invite-cta > * { position: relative; z-index: 2; }
  @keyframes dp-invite-sheen {
    0%   { transform: translateX(-110%); }
    10%  { transform: translateX(110%); }
    100% { transform: translateX(110%); }
  }
  @media (prefers-reduced-motion: reduce) {
    .dp-invite-cta::after { animation: none; }
  }
  /* Hover / active come from .p-pill (canonical Primary state matrix —
     accent-hover fill, deeper shadow; accent-active + translateY on press).
     No per-flow override. */
  .dp-invite-cta svg { flex-shrink: 0; stroke: currentColor; }

  /* Demoted to Secondary when the venue has a pending RSVP (the plans block's
     honey "Accept" is then the screen's one Primary). Mirrors .dp-directions:
     transparent + Delft outline + ink text on the frost sheet, sheen off.
     No .p-pill on the element, so this fully styles the button. */
  .dp-invite-cta.is-secondary {
    display: inline-flex; align-items: center; justify-content: center;
    gap: var(--space-sm);
    height: 48px;  /* matches the 48px standard; no primitive on this variant */
    border-radius: var(--radius-pill);
    background: transparent;
    border: 1px solid var(--line-l-strong);
    color: var(--panel-text);
    font-size: 14px; font-weight: 600; font-family: 'Inter', sans-serif;
    box-shadow: none;
    transition: background 120ms ease-out, border-color 120ms ease-out;
  }
  .dp-invite-cta.is-secondary::after { display: none; }
  .dp-invite-cta.is-secondary:hover { background: var(--line-l-faint); border-color: var(--line-l-strong); }

  /* ── Plan conflict confirm dialog ───────────────────────────────────────── */
  /* Sits above the invite sheet (z-index higher than .dpinvite-backdrop) when
     the user is about to create a plan within ±3h of an existing one. Three
     choices: merge, send separate, cancel. */
  .plan-conflict-backdrop {
    position: fixed;
    inset: 0;
    /* Slate-tinted modal backdrop — design system tier-1 backdrop spec. */
    background: rgba(15, 30, 55, 0.55);
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 10001;
    opacity: 0;
    transition: opacity 0.18s ease;
    padding: var(--space-xl);
  }
  .plan-conflict-backdrop.open { opacity: 1; }
  .plan-conflict-dialog {
    width: 100%;
    /* Keep a gutter on small phones so the dialog never touches the edges. */
    max-width: min(340px, calc(100vw - 2 * var(--space-xl)));
    border-radius: var(--radius-lg);
    padding: var(--space-xl) var(--space-xl) var(--space-xl);
    display: flex;
    flex-direction: column;
    gap: var(--space-lg);
    transform: translateY(8px) scale(0.98);
    transition: transform 0.22s ease;
  }
  .plan-conflict-backdrop.open .plan-conflict-dialog {
    transform: translateY(0) scale(1);
  }
  .plan-conflict-title {
    font-size: 16px;
    font-weight: 700;
    color: var(--text);
    line-height: 1.25;
  }
  .plan-conflict-body {
    font-size: var(--text-label);
    color: var(--muted);
    line-height: 1.45;
  }
  .plan-conflict-actions {
    display: flex;
    flex-direction: column;
    gap: var(--space-md);
    margin-top: var(--space-xs);
  }
  .plan-conflict-btn {
    width: 100%;
    height: 42px;
    border-radius: var(--radius-pill);
    border: 1px solid rgba(156,189,231,0.22);
    background: rgba(17,30,56,0.30);
    color: var(--text);
    font-size: var(--text-label);
    font-weight: 600;
    font-family: 'Inter', sans-serif;
    cursor: pointer;
    transition: opacity 0.15s, border-color 0.15s;
  }
  .plan-conflict-btn:hover { border-color: var(--accent); color: var(--accent); }
  .plan-conflict-btn.plan-conflict-primary {
    background: var(--accent);
    border-color: var(--accent);
    color: var(--accent-on, #2a1a0c);
  }
  .plan-conflict-btn.plan-conflict-primary:hover { opacity: 0.92; color: var(--accent-on, #2a1a0c); }
  .plan-conflict-btn.plan-conflict-cancel {
    background: none;
    border: none;
    color: var(--muted);
    font-weight: 500;
  }
  .plan-conflict-btn.plan-conflict-cancel:hover { color: var(--text); }

  /* Friends row inside social card */
  .social-friends {
    display: flex;
    align-items: center;
    gap: var(--space-md);
  }
  .social-friends-label {
    font-size: 12px;
    font-weight: 600;
    color: var(--accent);
    white-space: nowrap;
  }
  .social-friends-avatars {
    display: flex;
    gap: calc(var(--space-xs) * -1);
  }
  .social-avatar {
    width: 26px; height: 26px;
    border-radius: 50%;
    object-fit: cover;
    border: 2px solid rgba(17,30,56,0.8);
    margin-left: calc(var(--space-sm) * -1);
  }
  .social-avatar:first-child { margin-left: 0; }
  .social-avatar-init {
    background: rgba(17,30,56,0.6);
    color: var(--muted);
    display: flex; align-items: center; justify-content: center;
    font-size: 10px; font-weight: 600;
  }

  /* Plan items in the detail panel — a tidy vertical card-row on the cream
     plans tile. Structure (WHEN row / creator / message / guests / foot) +
     the cream surface live in components-content.css (.dp-plans-block …); this
     file owns only the shared guest-avatar / pip / arrival-chip atoms below. */
  .detail-plan-item { display: block; }
  .plan-actions { display: flex; align-items: center; }
  /* RSVP accept/decline use the .p-pill / .d-pill role primitives (base.css);
     Preview is a .g-rnd ghost. .on-light on the foot row steps the ghost to
     ink on the cream surface. */

  /* Social form overlay (shown inside social card) */
  .social-form-overlay {
    border-top: 1px solid rgba(156,189,231,0.12);
    padding-top: var(--space-md);
  }
  .social-form-inner {
    display: flex;
    flex-direction: column;
    gap: var(--space-md);
  }
  .social-form-label {
    font-size: var(--text-caption);
    font-weight: 600;
    color: var(--muted);
    letter-spacing: 0.5px;
  }
  .social-form-input {
    width: 100%;
    padding: var(--space-md) var(--space-md);
    background: rgba(17,30,56,0.5);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    color: var(--text);
    font-size: var(--text-label);
    font-family: 'Inter', sans-serif;
    box-sizing: border-box;
  }
  .social-form-input:focus { outline: none; border-color: var(--accent); }
  .social-form-actions {
    display: flex;
    gap: var(--space-md);
  }
  .social-form-btn {
    flex: 1;
    padding: var(--space-md) var(--space-lg);
    border-radius: var(--radius-md);
    border: 1px solid rgba(156,189,231,0.20);
    background: rgba(17,30,56,0.30);
    color: var(--text);
    font-size: 12px;
    font-weight: 600;
    font-family: 'Inter', sans-serif;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-sm);
    transition: all 0.15s;
  }
  .social-form-btn:hover { border-color: var(--accent); color: var(--accent); }
  .social-form-btn svg { flex-shrink: 0; }
  .social-form-btn-accent {
    background: rgba(245,194,94,0.12);
    border-color: rgba(245,194,94,0.30);
    color: var(--accent);
  }
  .social-form-btn-accent:hover { background: rgba(245,194,94,0.20); }

  .plan-friends-list {
    display: flex;
    flex-direction: column;
    gap: var(--space-xs);
    max-height: 120px;
    overflow-y: auto;
  }
  .plan-friend-check {
    font-size: 12px;
    color: var(--text);
    display: flex; align-items: center; gap: var(--space-sm);
    cursor: pointer;
  }
  .plan-friend-check input { accent-color: var(--accent); }

  /* Going friend picker */
  .going-friend-picker {
    margin-top: var(--space-md);
    display: flex;
    flex-direction: column;
    gap: var(--space-sm);
  }
  .friends-empty {
    font-size: 12px;
    color: var(--muted);
    text-align: center;
    padding: var(--space-md);
  }
  .friend-avatar-sm {
    width: 18px; height: 18px;
    border-radius: 50%;
    object-fit: cover;
    vertical-align: -3px;
  }
  .friend-avatar-sm-init {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: rgba(17,30,56,0.6);
    color: var(--muted);
    font-size: 9px;
    font-weight: 600;
  }

  /* ── Invite sheet — sharing-system (.dpinvite-*) ──────────────────────────
     Bottom sheet for outgoing invites. Layout-only here — buttons / chips /
     surfaces all use the design-system primitives (.p-pill, .s-circ,
     .chip-pill, .card, .glass-action). IDs preserved (#invite-sheet,
     #invite-sheet-backdrop) for legacy callers. */
  .invite-backdrop,
  .dpinvite-backdrop {
    position: fixed;
    inset: 0;
    z-index: 1100;
    background: rgba(0,0,0,0);
    transition: background 0.3s ease;
  }
  .invite-backdrop.open,
  .dpinvite-backdrop.open {
    /* Light-tint backdrop — was 55% slate + 6px blur, which obscured the
       LIVE shadow render on the map underneath. The map shadows shifting
       as the user scrubs the time slider IS the app's value prop, so we
       drop the blur entirely and lower the tint to a vignette-like 22%.
       Sheet/header surfaces remain readable thanks to their own glass
       backgrounds. */
    background: rgba(15, 30, 55, 0.22);
  }

  /* Slide the underlying surfaces down out of view so the user focuses on the
     sheet sliding up. Convention: closing panels always slide DOWN, even when
     another panel is sliding UP behind them. The FTS stays visible — it's the
     canonical time control and gets repositioned above the sheet (see rule
     below) so the user can scrub to preview weather/sun across the day while
     picking recipients. */
  body.invite-sheet-open #detail-panel,
  body.invite-sheet-open #panel {
    transform: translateY(calc(100% + 24px));
    opacity: 0;
    pointer-events: none;
    transition: transform 0.32s cubic-bezier(0.32, 0.72, 0, 1), opacity 0.22s ease;
  }
  body.invite-sheet-open #qc-wrap,
  body.invite-sheet-open #floating-search,
  body.invite-sheet-open #locate-btn,
  body.invite-sheet-open #zoom-jog {
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.22s ease;
  }
  /* (Brand slide-up handled by the unified rule near line 1210 alongside
     the top-strip — same direction, same easing.) */
  /* FTS lives INSIDE the invite sheet now (reparented into
     #dpinvite-fts-slot on open). The `.fts-in-invite` class tells the
     FTS to drop the body-anchored fixed positioning it uses elsewhere
     and flow naturally inside the slot. Restored on close. */
  body.fts #fts.fts-in-invite {
    position: relative;
    margin: 0;
    height: 40px;
    z-index: auto;
  }

  /* Invite-mode popup behavior: hidden at rest, revealed only while the
     user is actively dragging (the FTS sets .fts-popup-expanded during
     drag, removes it on release). A 0.4s delay before fade-out lets the
     picked time linger briefly after release — same UX rhythm as the
     thumb's release bounce. */
  .fts-in-invite #fts-popup {
    opacity: 0;
    transition: opacity 0.4s ease 0.4s;
  }
  .fts-in-invite #fts-popup.fts-popup-expanded {
    opacity: 1;
    transition: opacity 0.15s ease 0s;
  }

  /* Idle bounce hint — only when FTS is parked inside the invite sheet
     AND the thumb is at rest (not hovered, not actively dragged, not
     mid-release). Runs every 5 s with the bounce occupying the last
     ~600 ms of the cycle; the rest sits flat at scale(1) so it reads as
     an intermittent nudge ("you can drag me") rather than a constant
     pulse. Mirrors the thumb's release-bounce easing for visual
     continuity. The :not() chain hands the thumb back to its hover /
     active / release states the moment the user touches it. */
  @keyframes fts-thumb-idle-bounce {
    0%, 88%  { transform: translate(-50%, -50%) scale(1); }
    92%      { transform: translate(-50%, -50%) scale(1.08); }
    95%      { transform: translate(-50%, -50%) scale(0.96); }
    98%      { transform: translate(-50%, -50%) scale(1.03); }
    100%     { transform: translate(-50%, -50%) scale(1); }
  }
  .fts-in-invite #fts-thumb:not(.is-hover):not(.is-active):not(.is-releasing) {
    animation: fts-thumb-glint-idle 6s ease-in-out infinite,
               fts-thumb-idle-bounce 5s cubic-bezier(0.32, 0.72, 0, 1) infinite;
  }
  @media (prefers-reduced-motion: reduce) {
    .fts-in-invite #fts-thumb:not(.is-hover):not(.is-active):not(.is-releasing) {
      animation: none;
    }
  }

  .dpinvite-sheet {
    position: absolute;
    /* Sheet contract — bottom: 0 + svh max-height is correct on every
       host now (iOS Safari/PWA, Android Chrome/PWA, desktop) thanks to
       the viewport meta's interactive-widget=resizes-content. */
    bottom: 0;
    left: 0;
    right: 0;
    max-height: 92svh;
    height: auto;
    border-radius: var(--radius-lg) var(--radius-lg) 0 0;
    /* Cream-frost content world (2026-05-27) — same material as the detail +
       accept sheets: pure-frost (transparent + heavy blur over the map) with
       ink text. No --panel-text/--ink-muted override: the eyebrow, venue name,
       meta, Meeting tile all fall back to the global ink values. */
    background: var(--glass-panel-bg);
    backdrop-filter: var(--glass-blur-frost);
    -webkit-backdrop-filter: var(--glass-blur-frost);
    border: 1px solid var(--line-l);
    box-shadow: var(--sheet-shadow);
    color: var(--panel-text);
    display: flex;
    flex-direction: column;
    transform: translateY(100%);
    transition: transform 0.32s cubic-bezier(0.4, 0, 0.2, 1);
    padding-bottom: var(--app-pad-b);
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior: contain;
    touch-action: pan-y;
  }
  .dpinvite-sheet.open { transform: translateY(0); }

  /* Drag handle wrapper — matches #panel-handle / #dp-handle padding so
     the tap target is identical across surfaces. JS wires drag-to-dismiss
     onto this wrapper. */
  .dpinvite-handle {
    display: flex;
    flex-direction: column;
    align-items: center;
    /* Tighter than v1's 16/14 — the slider sits flush above the sheet
       now, so the handle just needs enough breathing room to be
       grippable, not to act as a chrome gap between the slider and
       the sheet's first line of content. */
    padding: var(--space-md) 0 var(--space-md);
    flex-shrink: 0;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    touch-action: none;
  }
  .dpinvite-grabber {
    width: 38px;
    height: 4px;
    border-radius: var(--radius-none);
    background: rgba(156,189,231,0.28);
  }

  /* The v1 floating top-card has been folded INTO the sheet itself as
     a "moment" block at the top — that way the venue + live time
     readout sit right below the slider (which sits flush against the
     sheet's top edge), so the user reads the answer the moment they
     stop scrubbing. .dpinvite-top-chip stays as a no-op selector for
     any legacy markup that might still reference it. */
  .dpinvite-top-chip { display: none; }

  /* The "moment" block — two-column header that mirrors the accept panel.
     Left tells you WHAT (eyebrow + venue + meta); right is the live
     Meeting tile (Meeting label + time + day) which updates as the user
     scrubs the FTS inside the sheet. Same visual rhythm both sides:
     small label → bold primary → small meta. */
  .dpinvite-moment {
    padding: var(--space-xs) 0 var(--space-lg);
    border-bottom: 1px solid rgba(156,189,231,0.10);
  }
  .dpinvite-moment-row {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--space-lg);
  }
  .dpinvite-moment-left {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: var(--space-xs);
  }
  .dpinvite-moment-col {
    flex-shrink: 0;
    /* Tight to content (matches the accept panel's .dprcv-moment-col).
       Removes the previous min-width: 88 / max-width: 140 clamp so the
       column collapses to whatever the time string needs and grows
       naturally when the user bumps system text size. */
    width: max-content;
    min-width: 0;
    text-align: right;
    display: flex;
    flex-direction: column;
    /* Matches .dpinvite-moment-left's gap so the two columns share the
       same inter-row spacing — without this, the right column's rows
       sat 4 px tighter than the left and the venue / time and meta /
       day-button baselines didn't line up. */
    gap: var(--space-xs);
  }
  .dpinvite-eyebrow {
    font-size: var(--text-label);
    font-weight: 500;
    color: var(--ink-muted);
    line-height: 1.25;
  }
  .dpinvite-moment-pin {
    flex-shrink: 0;
    color: var(--accent);
    opacity: 0.9;
    display: inline-flex;
    align-items: center;
    height: 26px;
  }
  .dpinvite-moment-pin svg { display: block; }
  .dpinvite-moment .dpinvite-venue-row {
    display: flex;
    align-items: center;
    gap: var(--space-md);
    /* No margin — row spacing comes from .dpinvite-moment-left's gap, like
       the receive panel's .dprcv-title-col. */
  }
  /* Area on its own line under the venue name — mirrors .dprcv-area exactly. */
  .dpinvite-area {
    font-size: var(--text-label);
    font-weight: 600;
    color: var(--panel-text);
    line-height: 1.3;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .dpinvite-venue-line {
    font-size: var(--text-display);
    font-weight: var(--fw-display);
    color: var(--panel-text);
    line-height: 1.15;             /* matches .dpacc-venue */
    letter-spacing: var(--tracking-display);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-width: 0;
  }
  .dpinvite-meta {
    display: flex;
    align-items: center;
    gap: var(--space-md);
    /* No pin → no indent; rows align at the left edge under the venue name,
       row spacing from the column gap — mirrors .dprcv-meta. */
    font-size: var(--text-label);
    font-weight: 500;
    color: var(--ink-muted);
    line-height: 1.4;
    font-variant-numeric: tabular-nums;
    /* Whole-pill drop on overflow — see .dprcv-meta. _fitMetaPills hides
       trailing `.dpinvite-meta-item` pills + their preceding dot when
       this row's scrollWidth exceeds offsetWidth. */
    flex-wrap: nowrap;
    overflow: hidden;
    white-space: nowrap;
  }
  .dpinvite-meta-item { flex-shrink: 0; }
  .dpinvite-meta-dot { flex-shrink: 0; opacity: 0.5; }
  /* Right column — Meeting label / live time / live day. Same visual
     register as the accept panel's .dprcv-moment-col so a user moving
     between sending and receiving an invite sees a consistent shape. */
  .dpinvite-moment-label {
    /* Matches .dpinvite-eyebrow */
    font-size: var(--text-label);
    font-weight: 500;
    color: var(--ink-muted);
    line-height: 1.25;
  }
  .dpinvite-moment-time {
    /* Matches .dpinvite-venue-line; no margin — row spacing from the column
       gap so the right column aligns row-for-row with the left (like dprcv). */
    font-size: var(--text-display);
    font-weight: var(--fw-display);
    color: var(--panel-text);
    line-height: 1.15;
    font-variant-numeric: tabular-nums;
    letter-spacing: var(--tracking-display);
  }
  /* Interactive day-button — taps open the date picker (toggleQcPanel('date')),
     mirroring the FTS top-bar date chip. Styled as a tappable pill with a
     down chevron so the affordance is unambiguous. */
  button.dpinvite-moment-sub {
    appearance: none;
    border: 0;
    cursor: pointer;
    background: rgba(245, 194, 94, 0.10);
    color: var(--panel-text);
    font-size: var(--text-label);
    /* Lighter weight per user feedback — heavy 700 read as a separate
       label instead of an inline meta affordance. 500 sits in line
       with the rest of the meta row. */
    font-weight: 500;
    /* No margin — the column gap puts this (row 3) level with the left
       column's .dpinvite-area, matching the receive panel's day ↔ area row. */
    /* Chevron moved BEFORE the text (flex-direction below stacks the
       SVG first), so the padding flips: tighter on the chevron side,
       looser on the text side. */
    padding: var(--space-xs) 9px var(--space-xs) var(--space-sm);
    border-radius: var(--radius-sm);
    letter-spacing: 0.01em;
    font-variant-numeric: tabular-nums;
    text-align: center;
    align-self: flex-end;
    display: inline-flex;
    align-items: center;
    /* Chevron rendered before the text in the markup (button structure
       updated alongside this rule). No row-reverse here — markup order
       is authoritative. */
    gap: var(--space-xs);
    transition: background 0.14s ease, color 0.14s ease, transform 90ms ease-out;
    -webkit-tap-highlight-color: transparent;
  }
  button.dpinvite-moment-sub svg {
    flex-shrink: 0;
    opacity: 0.7;
    transition: opacity 0.14s ease;
  }
  button.dpinvite-moment-sub:hover {
    background: rgba(245, 194, 94, 0.18);
    color: var(--accent);
  }
  button.dpinvite-moment-sub:hover svg { opacity: 1; }
  button.dpinvite-moment-sub:active { transform: scale(0.97); }
  /* Share step: the day-picker slot becomes the "Endre ›" back button. Same
     pill, but the down chevron rotates to point right. */
  button.dpinvite-moment-sub.is-back svg { transform: rotate(90deg); }
  /* FTS slot — host for the floating time slider when the invite sheet
     is open. Reparented from <body> on open, restored on close.
     padding-top reserves room the compact FTS popup needs to float
     above the thumb (~24 px popup + 6 px tail). A negative margin-top
     pulls the slot UP under the 'Select time' label so the visible
     gap matches 'Send til' → first avatar (~8 px) — the popup itself
     renders inside the padding-top region, partially overlapping the
     label area, but the popup is opaque slate-glass so it reads as a
     deliberate stack rather than overflow. */
  .dpinvite-fts-slot {
    width: 100%;
    position: relative;
    padding-top: var(--space-3xl);
    padding-bottom: var(--space-xs);
    margin-top: -22px;
  }
  .dpinvite-fts-slot:empty { display: none; }

  .dpinvite-body {
    /* Was flex:1 — stretched the body to fill the sheet's 92svh max-height
       even when content was short, leaving large empty space below the
       composer and inflating the friend-grid scroll region. flex: 0 1 auto
       sizes to content; the sheet itself shrinks via height:auto. The
       parent .dpinvite-sheet's overflow-y:auto + max-height:92svh still
       handles overflow for long friend lists. */
    flex: 0 1 auto;
    min-height: 0;
    padding: 0 var(--space-xl) var(--space-lg);
    display: flex;
    flex-direction: column;
    gap: var(--space-lg);
    -webkit-overflow-scrolling: touch;
  }

  /* Locked day picker — once the user advances to the share page, the date is
     fixed. Disable the button: drop the chevron affordance + pointer so it
     reads as a static moment readout, not a tappable picker. The header keeps
     showing the chosen day/time (driven by .dpinvite-moment-time / -sub-text). */
  button.dpinvite-moment-sub.is-locked,
  button.dpinvite-moment-sub:disabled {
    cursor: default;
    pointer-events: none;
    background: transparent;
    padding-left: 0;
  }
  button.dpinvite-moment-sub.is-locked svg,
  button.dpinvite-moment-sub:disabled svg {
    display: none;
  }

  /* ── Invite pager — one sheet, two pages, in-place horizontal slide ─────────
     The moment header stays put above; below it a clip (.dpinvite-pager) wraps
     a sliding track (.dpinvite-track) holding two equal-width pages. "Neste"
     adds .show-share → the track slides one page-width left (translateX(-50%))
     revealing the share page. The pager's height is JS-driven to follow the
     ACTIVE page so the off-screen page never inflates the sheet. */
  .dpinvite-pager {
    /* Full-bleed to the sheet edge: cancel the body's --space-xl horizontal
       padding with a matching negative margin, then re-inset the SAME amount
       inside each .dpinvite-page. Net effect — page content still lines up with
       the header (--space-xl from the sheet edge), but the clip boundary now
       sits a full --space-xl OUTSIDE the content. So neither the on-screen
       page's shadows get cut, nor does the off-screen page's edge bleed in:
       both pages' content is --space-xl clear of the boundary on each side.
       Height is set inline by _dpinviteSyncPagerHeight (locked to the taller
       page) and transitioned. */
    overflow: hidden;
    margin: 0 calc(var(--space-xl) * -1);
    transition: height var(--dur-slow) var(--ease-emphasized);
  }
  .dpinvite-track {
    display: flex;
    width: 200%;
    /* Pages anchor to the top so a shorter page doesn't stretch and push its
       content to the middle of the (taller) track box. */
    align-items: flex-start;
    transform: translateX(0);
    transition: transform var(--dur-slow) var(--ease-emphasized);
  }
  .dpinvite-track.show-share { transform: translateX(-50%); }
  .dpinvite-page {
    box-sizing: border-box;
    flex: 0 0 50%;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: var(--space-lg);
    /* Re-inset the full-bleed pager so content aligns with the header AND
       stays --space-xl clear of the clip boundary (see .dpinvite-pager). */
    padding: 0 var(--space-xl);
  }

  /* Step footer — stacks the primary CTA (Neste) above a centred secondary
     text-link (Cancel / Back), matching the accept panel's footer rhythm
     (.dprcv-footer gap + .dprcv-cta-row margin). Back + Cancel both ride the
     shared .dprcv-cta-row / .dprcv-cta-link classes so spacing + padding are
     identical to the accept sheet's "below the I'm in button" links. */
  .dpinvite-foot {
    display: flex;
    flex-direction: column;
    gap: var(--space-md);
  }

  /* Hairline between the FTS timeline and the Neste footer (when step). Same
     faint ink line as the "eller" divider; page gap provides the breathing
     room on both sides. */
  .dpinvite-rule {
    height: 1px;
    background: var(--line-l-faint);
  }

  /* Rocks-on-ice — when the share step slides in, the avatars (then the share
     targets) land with a staggered spring, like the accept panel's confirm
     action cards. backwards fill holds the off-screen start during each item's
     delay; re-fires every time the share step is entered (.show-share toggles).
     Reuses @keyframes dprcv-card-in (translateX 40px → 0 + fade). */
  .dpinvite-track.show-share .dpinvite-avatar,
  .dpinvite-track.show-share .dpinvite-target {
    animation: dprcv-card-in 260ms var(--ease-spring) backwards;
  }
  .dpinvite-track.show-share .dpinvite-avatar:nth-child(1) { animation-delay: 110ms; }
  .dpinvite-track.show-share .dpinvite-avatar:nth-child(2) { animation-delay: 150ms; }
  .dpinvite-track.show-share .dpinvite-avatar:nth-child(3) { animation-delay: 190ms; }
  .dpinvite-track.show-share .dpinvite-avatar:nth-child(4) { animation-delay: 230ms; }
  .dpinvite-track.show-share .dpinvite-avatar:nth-child(5) { animation-delay: 270ms; }
  .dpinvite-track.show-share .dpinvite-avatar:nth-child(6) { animation-delay: 310ms; }
  .dpinvite-track.show-share .dpinvite-avatar:nth-child(n+7) { animation-delay: 350ms; }
  .dpinvite-track.show-share .dpinvite-target:nth-child(1) { animation-delay: 250ms; }
  .dpinvite-track.show-share .dpinvite-target:nth-child(2) { animation-delay: 290ms; }
  .dpinvite-track.show-share .dpinvite-target:nth-child(3) { animation-delay: 330ms; }
  @media (prefers-reduced-motion: reduce) {
    .dpinvite-track.show-share .dpinvite-avatar,
    .dpinvite-track.show-share .dpinvite-target { animation: none; }
  }
  @media (prefers-reduced-motion: reduce) {
    .dpinvite-pager,
    .dpinvite-track { transition: none; }
  }

  /* Was the "Who are you inviting?" section title — kept as no-op for
     legacy markup. The avatar row is self-evident; an explicit title
     above it just added chrome without adding information. */
  .dpinvite-section-title { display: none; }

  /* FTS callout removed — the floating "Going at 13:25" chip used to
     live here. Its job (showing the picked time + sun-until status) is
     now done by the persistent .dpinvite-when-line inside the top
     header, which doesn't ask a question and float its answer on a
     separate surface. Single source of truth, less chrome over the map. */

  /* Recent / All group chips removed — both chips showed the same count
     in any small-friend-list state, which made the toggle look meaningless.
     Hidden via display: none so the JS that still emits the strip is a
     no-op visually. We'll bring back filtering / pagination once the
     friend graph is bigger and a search affordance is added. */
  .dpinvite-group-chips { display: none; }

  /* Section label — "Velg tidspunkt" (when page) + "Send til" (share page).
     Sentence case (matches the sheet's "Inviter venner til" / "Klokken" voice),
     but given real weight: ink + 700 at the label size so it reads as a clear
     section header, not a faint eyebrow (the "eller" divider stays the quiet
     one). margin-bottom is the gap to the FTS on the when page; the share page
     overrides spacing below. */
  .dpinvite-friends-label {
    font-size: var(--text-label);
    font-weight: 700;
    color: var(--panel-text);
    line-height: 1;
    text-align: center;
    margin-top: var(--space-2xs);
    margin-bottom: var(--space-md);
  }
  /* Share page: the label is a direct flex child, so the page gap (--space-lg)
     handles the gap below — drop margin-bottom to avoid doubling it. A small
     margin-top vertically centres it against the absolute back pill. This makes
     "Send til"→avatars equal to "eller"→targets (both = one page gap). */
  .dpinvite-page-share > .dpinvite-friends-label {
    margin: var(--space-xs) 0 0;
  }

  /* Avatar row — horizontal scroll, multi-select, 40 px circles
     (was 56 px in v1, which dominated the sheet for a 4-avatar list).
     40 px keeps a generous tap target while leaving room for ~6 avatars
     in a single row at typical sheet widths, and reads as a quick
     "share to these people" picker rather than a select-your-fighter
     screen. */
  .dpinvite-avatar-row {
    /* PR D v8: was a horizontal flex with overflow-x:auto so long friend
       lists scrolled sideways. Switched to a 4-col grid (max 2 rows = 8
       tiles) per user spec. The render path caps at 8 + emits a "+N"
       tile so the grid never exceeds 2 rows. */
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: var(--space-md) var(--space-sm);
    justify-items: center;
    padding: var(--space-sm);
    margin-top: calc(var(--space-sm) * -1);
    margin-bottom: calc(var(--space-sm) * -1);
    overflow: visible;
  }
  .dpinvite-avatar-row::-webkit-scrollbar { display: none; }
  .dpinvite-avatar {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-sm);
    flex-shrink: 0;
    width: 56px;
    background: transparent;
    border: 0;
    padding: 0;
    cursor: pointer;
    user-select: none;
    -webkit-tap-highlight-color: transparent;
    /* Tap spring — the whole tile compresses a touch on press, then
       releases with a slight overshoot. Combines with the checkmark pop on
       selection so the tap reads as "I picked them" rather than a flat toggle. */
    transition: transform 220ms var(--ease-spring);
  }
  .dpinvite-avatar:active { transform: scale(0.93); }
  .dpinvite-avatar[hidden] { display: none; }
  .dpinvite-avatar-img {
    position: relative;
    width: 48px; height: 48px;
  }
  .dpinvite-avatar-img > img,
  .dpinvite-avatar-init {
    width: 48px; height: 48px;
    border-radius: 50%;
    display: block;
    object-fit: cover;
    transition: box-shadow 0.16s ease-out;
  }
  .dpinvite-avatar-init {
    display: flex; align-items: center; justify-content: center;
    color: #fff;
    font-size: 19px;
    font-weight: 700;
    box-shadow: inset 0 1px 0 rgba(255,255,255,0.25), inset 0 -1px 0 rgba(0,0,0,0.20);
  }
  .dpinvite-avatar-init.init-color-0 { background: #E5754A; }
  .dpinvite-avatar-init.init-color-1 { background: #3F75AC; }
  .dpinvite-avatar-init.init-color-2 { background: #4FA663; }
  .dpinvite-avatar-init.init-color-3 { background: #BA823F; }
  .dpinvite-avatar-init.init-color-4 { background: #8E6DBE; }
  .dpinvite-avatar-init.init-color-5 { background: #2A9C92; }
  .dpinvite-avatar-init.init-color-6 { background: #C4546F; }
  .dpinvite-avatar-init.init-color-7 { background: #7D8FA0; }
  /* PR D v8 — "+N more" overflow tile, replacing the 8th slot when the
     friend list overflows the 4×2 grid cap. Muted neutral so it reads
     as "see all" rather than competing with an actual friend tile. */
  .dpinvite-avatar-more-init {
    background: var(--line-l-faint);
    color: var(--panel-text);
    font-size: var(--text-subtitle);
  }
  .dpinvite-avatar-more { opacity: 0.95; }
  .dpinvite-avatar[aria-checked="true"] .dpinvite-avatar-img > img,
  .dpinvite-avatar[aria-checked="true"] .dpinvite-avatar-init {
    /* 2 px cream gap + 2.5 px honey ring reads cleanly at 48 px. Gap =
       cream surface (the sheet is light/frost). */
    box-shadow: 0 0 0 2px var(--surface-raised), 0 0 0 4.5px var(--accent);
  }
  .dpinvite-avatar-check {
    position: absolute; bottom: -2px; right: -2px;
    width: 16px; height: 16px;
    border-radius: 50%;
    background: var(--accent);
    color: var(--accent-on, #2a1a0c);
    display: flex; align-items: center; justify-content: center;
    border: 2px solid var(--surface-raised);
    opacity: 0;
    transform: scale(0);
    transition: opacity 160ms ease, transform 280ms var(--ease-spring);
    pointer-events: none;
  }
  .dpinvite-avatar-check svg { width: 9px; height: 9px; }
  .dpinvite-avatar[aria-checked="true"] .dpinvite-avatar-check {
    opacity: 1;
    /* var(--ease-spring) overshoots to 1.05 then settles — reads as a real pop */
    transform: scale(1);
  }
  .dpinvite-online-dot {
    position: absolute; bottom: 0; right: 0;
    width: 10px; height: 10px;
    border-radius: 50%;
    background: #5DCABA;
    border: 2px solid var(--surface-raised);
    pointer-events: none;
  }
  .dpinvite-avatar[aria-checked="true"] .dpinvite-online-dot { display: none; }
  .dpinvite-avatar-name {
    font-size: var(--text-caption);
    font-weight: 500;
    color: var(--ink-muted);
    line-height: 1.2;
    max-width: 56px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    text-align: center;
    transition: color 0.15s, font-weight 0.15s;
  }
  .dpinvite-avatar[aria-checked="true"] .dpinvite-avatar-name {
    /* Selected name = ink + bold (was honey, which washed out on the cream
       sheet). Selection is already signalled by the honey ring + check badge,
       so the name just needs to go from muted → strong ink. */
    color: var(--panel-text);
    font-weight: 700;
  }

  /* "eller" rule — separates SEND-TO-FRIENDS (in-app, above) from the external
     share targets (below), the way Instagram visually splits its people grid
     from its app row. A centred hairline label reads as an intentional fork
     ("message a friend, OR share a link"), not two stranded rows. */
  .dpinvite-or {
    display: flex;
    align-items: center;
    gap: var(--space-md);
    color: var(--ink-muted);
    font-size: var(--text-caption);
    font-weight: 600;
  }
  .dpinvite-or::before,
  .dpinvite-or::after {
    content: "";
    flex: 1;
    height: 1px;
    background: var(--line-l-faint);
  }

  /* ── Post-send confirmation ──────────────────────────────────────────────
     The slick "I'm in" beat, ported to Send: a one-shot honey gleam sweeps the
     whole sheet (reuses @keyframes dprcv-panel-sheen from the accept panel)
     while a success check springs in (@keyframes dprcv-confirm-pop). The pager
     region is swapped to .dpinvite-sent; the venue header stays put. */
  .dpinvite-sheet.is-confirming::after {
    content: ''; position: absolute; inset: 0; pointer-events: none; z-index: 6;
    border-radius: inherit;
    background: linear-gradient(115deg, transparent 32%, var(--accent) 50%, transparent 68%);
    transform: translateX(-120%); opacity: 0; mix-blend-mode: screen;
    animation: dprcv-panel-sheen 720ms var(--ease-decelerate) forwards;
  }
  .dpinvite-sent {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--space-md);
    text-align: center;
    /* Fill the pager's locked height so the success state centres in place and
       the sheet height never changes during the confirm swap. */
    min-height: 100%;
    box-sizing: border-box;
    padding: var(--space-2xl) 0;
  }
  .dpinvite-sent-check {
    width: 56px; height: 56px;
    border-radius: 50%;
    display: flex; align-items: center; justify-content: center;
    background: var(--accent);
    color: var(--accent-on, #2a1a0c);
    box-shadow: var(--glassctl-raise);
    animation: dprcv-confirm-pop var(--dur-slow) var(--ease-spring) both;
  }
  .dpinvite-sent-title { font-size: var(--text-body); font-weight: 700; color: var(--panel-text); }
  .dpinvite-sent-sub   { font-size: var(--text-caption); color: var(--ink-muted); }
  @media (prefers-reduced-motion: reduce) {
    .dpinvite-sheet.is-confirming::after { animation: none; opacity: 0; }
    .dpinvite-sent-check { animation: none; }
  }

  /* Empty state (no friends) — calm, no card surface, no icon. Just two
     centered lines explaining how the share-link flow works. The
     "Send delingslenke" CTA below is the only path forward; no second
     "Inviter" affordance to compete with it. */
  .dpinvite-empty-card {
    text-align: center;
    padding: var(--space-lg) var(--space-xs) var(--space-sm);
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-sm);
  }
  .dpinvite-empty-title { font-size: var(--text-label); font-weight: 600; color: var(--panel-text); line-height: 1.3; }
  .dpinvite-empty-sub   { font-size: 12px; color: var(--ink-muted); line-height: 1.4; max-width: 280px; }

  /* Shared .empty-state is built for full-screen lists (huge 48px padding) —
     tighten it for the bottom-sheet context so the empty share step stays
     compact. */
  .dpinvite-page-share .empty-state {
    padding: var(--space-md) var(--space-lg) var(--space-xs);
    max-width: 320px;
  }
  /* "Legg til venn" CTA — the one honey action on the empty share step (there's
     no Send button with no friends). Icon + label, auto width, centred. */
  .dpinvite-empty-cta {
    width: auto;
    gap: var(--space-xs);
    margin: 0 auto;
  }

  /* ── Share-targets row (IG-style bottom row) ─────────────────────────────
     Fixed set of labelled circles: Copy link · More apps · WhatsApp. Each is
     a glass-control circle (--glassctl tokens, same material as the top-bar
     buttons) with a small ink-muted label below. These are TERTIARY — the one
     honey CTA (Send) overlays them when 1+ friends are picked (see
     .dpinvite-share-bottom below). The row stands alone (no Send) in the
     empty / no-friends state. */
  .dpinvite-targets-row {
    display: flex;
    justify-content: center;
    gap: var(--space-sm);
    margin-top: 0;
  }
  .dpinvite-target {
    /* Fixed width per target so the row centers around the MIDDLE target's
       circle, not around the row's box (whose center drifts when labels
       have different widths — "Kopier lenke" vs "WhatsApp"). With equal
       widths, each circle sits at the center of its own equal-width
       column, and the centerline reads in line with "eller" above. v4 bumped
       column count from 3 → 5 (added SMS + Email); 64px keeps all 5 fitting
       on a 320px viewport. */
    flex: 0 0 64px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-sm);
    background: transparent;
    border: 0;
    padding: 0;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    user-select: none;
  }
  .dpinvite-target-circle {
    /* Secondary to the 48px friend avatars above — share-targets are the
       external (link) path, so they sit one notch smaller. */
    width: 44px; height: 44px;
    border-radius: 50%;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: var(--glassctl-bg);
    backdrop-filter: var(--blur-control);
    -webkit-backdrop-filter: var(--blur-control);
    border: var(--glassctl-border);
    box-shadow: var(--glassctl-raise);
    color: var(--glassctl-icon);
    transition: background 120ms, box-shadow 120ms, transform 120ms;
  }
  .dpinvite-target:hover .dpinvite-target-circle { background: var(--glassctl-bg-hover); }
  .dpinvite-target:active .dpinvite-target-circle {
    background: var(--glassctl-bg-hover);
    box-shadow: var(--glassctl-press);
    transform: scale(0.94);
  }
  .dpinvite-target-label {
    font-size: var(--text-caption);
    font-weight: 500;
    color: var(--ink-muted);
    line-height: 1.2;
    white-space: nowrap;
  }

  /* Bottom slot — the targets row and the honey Send button share ONE box so
     the Send button can overlap (not displace) the targets when a friend is
     picked. The slot is position:relative; the Send overlay is absolute over
     it. Slot height is fixed by the targets row, so toggling Send reflows
     nothing (the sheet height is stable across selection changes). */
  .dpinvite-share-bottom {
    position: relative;
    margin-top: 0;
  }
  .dpinvite-share-bottom .dpinvite-targets-row { margin-top: 0; }
  /* Clean swap (IG): when a friend is picked the targets row fully gives way to
     the full-width Send button — no ghosted/dimmed circles behind it. */
  .dpinvite-share-bottom.has-selection .dpinvite-targets-row {
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
    transition: opacity var(--dur-base) var(--ease-standard);
  }
  .dpinvite-share-bottom .dpinvite-targets-row {
    transition: opacity var(--dur-base) var(--ease-standard);
  }

  /* Send overlay — the single honey CTA, absolutely positioned over the
     targets row. Hidden (faded + nudged down) at 0 selected; slides up +
     fades in when 1+ picked. Vertically centred on the targets row so the
     pill sits over the circles, not the labels. */
  .dpinvite-send-overlay {
    position: absolute;
    left: 0; right: 0;
    top: 50%;
    transform: translateY(-50%) translateY(8px);
    opacity: 0;
    pointer-events: none;
    transition: opacity var(--dur-base) var(--ease-decelerate),
                transform var(--dur-base) var(--ease-decelerate);
  }
  .dpinvite-share-bottom.has-selection .dpinvite-send-overlay {
    opacity: 1;
    transform: translateY(-50%) translateY(0);
    pointer-events: auto;
  }
  .dpinvite-send-overlay .p-pill {
    width: 100%;
    min-width: 0;
    height: 48px;
  }
  @media (prefers-reduced-motion: reduce) {
    .dpinvite-share-bottom .dpinvite-targets-row,
    .dpinvite-send-overlay { transition: none; }
    .dpinvite-send-overlay { transform: translateY(-50%); }
  }

  /* Sticky CTA row — primary pill + circular companion (.s-circ / .s-sq).
     Uses margin-left on the companion (not `gap`) so the companion can
     collapse cleanly to width:0 + margin:0 when no friends are selected. */
  .dpinvite-cta-row {
    display: flex;
    margin-top: var(--space-xs);
    align-items: center;
  }
  .dpinvite-cta-row .p-pill { flex: 1; min-width: 0; height: 48px; }

  /* Sub-row under the main CTA reuses the receive panel's .dprcv-cta-row /
     .dprcv-cta-link / .dprcv-cta-sep (centred, "·"-separated) so it's identical
     to the accept bottom — copy-link where "Kommer senere" sits, cancel where
     "Avslå" sits. */

  /* Companion .s-circ — animated entrance/exit by transitioning width,
     margin-left, opacity, and scale. The primary pill's flex:1 reflows
     in real time as the companion width animates, so the primary appears
     to shrink/grow in lockstep — no JS needed for the primary's resize.
     The companion grows in place at the right of the row, never sliding
     across the primary's body. */
  .dpinvite-cta-row .s-circ {
    flex-shrink: 0;
    width: 44px;
    margin-left: var(--space-md);
    opacity: 1;
    transform: scale(1);
    pointer-events: auto;
    overflow: hidden;
    transition: width 0.2s cubic-bezier(0.4, 0, 0.2, 1),
                margin-left 0.2s cubic-bezier(0.4, 0, 0.2, 1),
                opacity 0.16s ease,
                transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
  }
  .dpinvite-cta-row .s-circ:not(.is-visible) {
    width: 0;
    margin-left: 0;
    opacity: 0;
    transform: scale(0.4);
    pointer-events: none;
  }

  /* Cancel — demoted from a full-width pill to a small text link. The
     swipe-down handle + backdrop tap + Esc are the primary close
     paths; the link is just an explicit fallback for users who don't
     intuit the gestures. No need to eat a whole row at the bottom. */
  .dpinvite-cancel-row {
    display: flex;
    justify-content: center;
    /* Match the accept-panel secondary row's vertical rhythm — tight
     margin above (the body's gap supplies most of the breathing room),
     no margin below. */
    margin-top: 0;
    margin-bottom: 0;
  }
  .dpinvite-cancel-row .g-rnd {
    background: transparent;
    border: 0;
    box-shadow: none;
    color: var(--muted);
    font-size: var(--text-label);
    font-weight: 500;
    /* Matches .dprcv-cta-link in the accept panel (32 px tall, 8 px
       horizontal padding) so the two surfaces share a secondary-link
       cadence. */
    height: 32px;
    padding: 0 var(--space-md);
    border-radius: var(--radius-lg);
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    transition: color 0.12s, background 0.12s;
  }
  .dpinvite-cancel-row .g-rnd:hover {
    color: var(--text);
    background: rgba(255,244,224,0.06);
  }

  /* Desktop: cap width and centre the sheet + top chip. */
  @media (min-width: 640px) {
    .dpinvite-sheet {
      max-width: 460px;
      max-height: 92svh;
      height: auto;
      left: 50%;
      transform: translateX(-50%) translateY(100%);
      border-radius: var(--radius-lg) var(--radius-lg) 0 0;
    }
    .dpinvite-sheet.open {
      transform: translateX(-50%) translateY(0);
    }
    /* Desktop variant of the cal-open swap — same translateY(100%)
       slide-down as mobile, but preserving the translateX(-50%)
       horizontal centring the desktop sheet relies on. */
    body.cal-open .dpinvite-sheet.open {
      transform: translateX(-50%) translateY(100%);
      opacity: 0;
      pointer-events: none;
      transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1),
                  opacity 0.3s ease;
    }
    body.cal-open .dpinvite-backdrop.open {
      opacity: 0;
      pointer-events: none;
      transition: opacity 0.22s ease;
    }
    .dpinvite-top-chip {
      max-width: 460px;
      left: 50%;
      right: auto;
      transform: translateX(-50%) translateY(-130%);
    }
    .dpinvite-top-chip.open {
      transform: translateX(-50%) translateY(0);
    }
  }

  /* ── Plan preview takeover ──────────────────────────────────────────────── */
  /* Hide the venue list and detail panel while the takeover is active so the
     live map (with shadows) is fully visible behind the floating cards. */
  body.plan-preview-active #panel,
  body.plan-preview-active #detail-panel,
  body.plan-preview-active #floating-search,
  body.plan-preview-active #qc-wrap,
  body.plan-preview-active #fts,
  body.plan-preview-active #fts-popup,
  /* Notif-toast-wrap intentionally NOT hidden — toasts at the top of
     the screen are useful during the accept / confirm flow (friend
     request sent / withdrawn, calendar added, etc.). They sit above
     the bottom panel and never overlap it. */
  /* Post-accept panel is also a takeover moment — same chrome-hiding
     rules so the panel is the only thing on screen. v1 left the
     search bar, venue list, FTS, and header chip visible behind the
     panel, which made the moment feel busy ('here's the app + here's
     a panel' state). */
  body.post-accept-active #panel,
  body.post-accept-active #detail-panel,
  body.post-accept-active #floating-search,
  body.post-accept-active #qc-wrap,
  body.post-accept-active #fts,
  body.post-accept-active #fts-popup,
  body.post-accept-active #locate-btn,
  body.post-accept-active #zoom-jog {
    opacity: 0 !important;
    pointer-events: none !important;
    transition: opacity 0.25s ease;
  }
  /* During plan-preview the receiver's interaction is choreographed
     by the locate-btn cycle (dive → fit-both → user → dive) — the
     zoom slider serves no purpose here (it can zoom in but can't
     restore the deliberate dive view) and visually competes with the
     dive's framing. Hidden via display:none; locate stays + lifts
     above the preview overlay. Anchored above the .pp-bottom panel
     via --pp-bottom-h (written by openPlanPreview from offsetHeight,
     kept in sync via ResizeObserver) — same mechanism the venue list
     uses via --peek-h. */
  body.plan-preview-active #locate-btn {
    z-index: 1210; /* above the plan-preview overlay (1200) */
    opacity: 1;
    pointer-events: auto;
    transition: bottom 0.22s cubic-bezier(0.25, 0.9, 0.4, 1), opacity 0.25s ease;
  }
  body.plan-preview-active #zoom-jog { display: none; }
  body.plan-preview-active #locate-btn {
    bottom: calc(var(--pp-bottom-h, 220px) + 14px);
  }
  body.plan-preview-active #zoom-jog {
    bottom: calc(var(--pp-bottom-h, 220px) + 14px + 34px + 10px);
  }
  /* Plan-preview overlay — full-viewport flex column ending in the bottom
     sheet. Class names .plan-preview / .dprcv-overlay both supported so
     legacy callers don't break during rollout. */
  .plan-preview,
  .dprcv-overlay {
    position: fixed;
    inset: 0;
    z-index: 1200;
    /* none while opening/closing so the slide animation doesn't capture
       stray taps; auto once .open so the transparent area above the
       bottom panel BLOCKS clicks to the map underneath. v1 left it as
       none with only the bottom panel having auto — clicks above the
       panel passed through to the canvas-overlay pins, letting the user
       select OTHER venues while the accept page was on screen. */
    pointer-events: none;
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
  }
  .dprcv-overlay.open { pointer-events: auto; }
  .dprcv-overlay.open .dprcv-bottom { transform: translateY(0); }

  /* Bottom sheet — glass-panel surface. Slides up on open, down on close
     (translateY 100% ↔ 0), matching the post-accept panel + design-system
     sheet pattern. v1 used a 20px slide + fade which read as a sluggish
     'fade away' when handing off to the post-accept panel — user said
     'I am getting this strange slow sliding down and fadeout'. */
  .dprcv-bottom {
    pointer-events: auto;
    margin: 0;
    /* See viewport tokens block at top of CSS. --app-pad-b is the
       standards-based bottom-content padding — env(safe-area-inset-
       bottom) with a 12px floor, collapses to 8px when a field is
       focused (keyboard up). */
    padding: 0 var(--space-xl) var(--app-pad-b);
    /* Sheet contract — svh is the stable smallest visible viewport.
       Never clipped by Safari's address bar. Shrinks correctly when
       the Android keyboard opens (interactive-widget=resizes-content
       in the viewport meta makes Android match iOS behavior). */
    max-height: 92svh;
    overflow-y: auto;
    overscroll-behavior: contain;
    touch-action: pan-y;
    -webkit-overflow-scrolling: touch;
    border-radius: var(--radius-lg) var(--radius-lg) 0 0;
    background: var(--glass-panel-bg);
    /* Light frosted receive page (first impression) — was cream text on a
       brightness(0.92)-dimmed frost, which read dark. Drop the dim + flip the
       base text to ink so it reads light like the accept panel (.dpacc-panel).
       Most child text already uses --panel-text / --ink-muted (ink); only this
       base fallback colour was cream. */
    backdrop-filter: var(--glass-blur-frost);
    -webkit-backdrop-filter: var(--glass-blur-frost);
    border-top: var(--glass-border);
    box-shadow: var(--sheet-shadow);
    color: var(--panel-text);
    /* Light frost ⇒ the destructive decline needs the darker red for contrast
       (root --color-error is the dark-surface red-500). */
    --color-error: var(--red-600);
    /* Match #panel.mobile-hidden's slide-down feel: cubic-bezier(0.32,
       0.72, 0, 1) for a smoother ease-out, +20 px extra throw past the
       viewport edge so the perceptible motion finishes before the
       transition's tail. v1's Material easing (0.4, 0, 0.2, 1) decel-
       erates slowly at the end — user saw the panel 'hang briefly'
       just before clearing the screen. */
    transform: translateY(calc(100% + 20px));
    transition: transform 0.34s cubic-bezier(0.32, 0.72, 0, 1);
    will-change: transform;
    display: flex;
    flex-direction: column;
    gap: var(--space-md);
  }
  /* Drag handle wrapper — matches .pp-handle / #panel-handle / #dp-handle. */
  .dprcv-handle {
    display: flex;
    justify-content: center;
    align-items: center;
    padding: var(--space-md) 0 var(--space-xs);
    cursor: pointer;
    touch-action: none;
    flex-shrink: 0;
  }
  .dprcv-grabber {
    width: 36px; height: 4px; border-radius: var(--radius-none);
    background: rgba(156,189,231,0.40);
    flex-shrink: 0;
  }

  /* Floating "from friend" top pill — wraps a .glass-action card. */
  .dprcv-top-pill {
    position: fixed;
    top: calc(env(safe-area-inset-top, 0px) + 12px);
    left: 0; right: 0;
    z-index: 1201;
    display: flex;
    justify-content: center;
    pointer-events: none;
    transform: translateY(-130%);
    transition: transform 0.32s cubic-bezier(0.4, 0, 0.2, 1);
  }
  .dprcv-overlay.open .dprcv-top-pill { transform: translateY(0); }
  .dprcv-top-pill-card {
    pointer-events: auto;
    display: inline-flex;
    align-items: center;
    gap: var(--space-md);
    padding: var(--space-sm) var(--space-lg) var(--space-sm) var(--space-sm);
    border-radius: var(--radius-pill);
  }
  .dprcv-top-pill-av {
    width: 28px; height: 28px;
    border-radius: 50%;
    flex-shrink: 0;
    overflow: hidden;
    display: flex; align-items: center; justify-content: center;
    color: #fff;
    font-size: var(--text-label);
    font-weight: 700;
  }
  .dprcv-top-pill-av img { width: 100%; height: 100%; object-fit: cover; }
  .dprcv-top-pill-name {
    font-size: var(--text-label); font-weight: 700; color: var(--text);
    line-height: 1.2;
  }
  .dprcv-top-pill-sub {
    font-size: var(--text-caption); font-weight: 500; color: var(--muted);
    margin-left: var(--space-xs);
  }

  /* Title block — sentence-case eyebrow + venue name (with pin glyph) + meta.
     Matches the invite-sheet design language: no ALL-CAPS, pin anchors
     the venue identity, secondary text demoted to muted weight. */
  .dprcv-title-block {
    display: flex;
    flex-direction: column;
    gap: var(--space-xs);
    margin-top: var(--space-2xs);
  }
  /* Two-column header — left tells you WHAT (eyebrow + venue + meta),
     right tells you WHEN (Meeting label + time + day). Both columns share
     the same visual rhythm: small label → bold primary → small meta. */
  .dprcv-title-row {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    /* 10 px (was 14) gives the meta line more horizontal room before
       truncation kicks in. */
    gap: var(--space-md);
  }
  .dprcv-title-col {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: var(--space-xs);
  }
  .dprcv-moment-col {
    flex-shrink: 0;
    /* Width is max-content (sized to its widest child — typically the
       big time), but capped so the right column never eats more than
       ~38% of the row. Earlier behaviour let "Mandag · in 30 min" expand
       moment-col enough to truncate the meta string in title-col on
       narrow screens. */
    width: max-content;
    max-width: 38%;
    min-width: 0;
    text-align: right;
    display: flex;
    flex-direction: column;
    /* Matches .dprcv-title-col's gap so the two columns share the same
       inter-row spacing — without this, the right column's rows sat
       4 px tighter than the left and the venue / time and meta / sub
       baselines didn't line up. */
    gap: var(--space-xs);
  }
  /* Empty 4th row balancing the title-col's 4 rows (eyebrow / venue / area / meta). */
  .dprcv-moment-spacer { font-size: var(--text-label); line-height: 1.4; min-height: calc(13px * 1.4); }
  .dprcv-eyebrow {
    font-size: var(--text-label);
    font-weight: 500;
    color: var(--ink-muted);
    line-height: 1.25;
    /* Reserve the row height even when content collapses to &nbsp; so the
       venue + meta stay vertically aligned with the right-column "Meets at"
       label. Without this, an empty eyebrow shifted the entire title-col up
       in self-invite / race-condition cases. */
    min-height: calc(13px * 1.25);
  }
  .dprcv-eyebrow strong { color: var(--panel-text); font-weight: 700; }
  .dprcv-venue-row {
    display: flex;
    align-items: center;
    gap: var(--space-md);
  }
  /* Area row (row 3) — its own line under the venue name, like the venue card
     (coarse area, slightly stronger than the meta). */
  .dprcv-area {
    font-size: var(--text-label);
    font-weight: 600;
    color: var(--panel-text);
    line-height: 1.3;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .dprcv-venue-pin {
    flex-shrink: 0;
    color: var(--accent);
    opacity: 0.9;
    /* Match the venue text's cap-height. Without an explicit height
       the 16x16 SVG floats relative to the Display-tier title and reads
       as 'glued to the top'. line-height:1 + height:matching-cap pins
       the glyph to the text's vertical centre. */
    height: 26px;
    display: inline-flex;
    align-items: center;
  }
  .dprcv-venue-pin svg { display: block; }
  .dprcv-venue {
    font-size: var(--text-display);
    font-weight: var(--fw-display);
    color: var(--panel-text);
    line-height: 1.15;
    letter-spacing: var(--tracking-display);
    min-width: 0;
    /* Truncate long names with ellipsis rather than wrap to a second
       line. With the tight right column on this header, wrapping
       pushes the meta down too far and breaks the visual rhythm. */
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .dprcv-meta {
    display: flex;
    align-items: center;
    flex-wrap: nowrap;
    /* Pin removed — rows align under the venue name; spacing comes from the
       title-col gap (card-like rhythm), no per-row margin. */
    font-size: var(--text-label);
    font-weight: 500;
    color: var(--ink-muted);
    font-variant-numeric: tabular-nums;
    line-height: 1.4;
    /* Whole-pill drop on overflow: _fitMetaPills (js/app.js) measures
       scrollWidth vs offsetWidth after layout and hides trailing
       `.dprcv-meta-item` pills (+ their preceding dot) until they fit.
       Cleaner than mid-text ellipsis on the trailing pill. */
    overflow: hidden;
    white-space: nowrap;
  }
  .dprcv-meta-item { flex-shrink: 0; }
  .dprcv-meta-dot {
    flex-shrink: 0;
    opacity: 0.5;
    margin: 0 var(--space-xs);
  }
  .dprcv-meta-walk,
  .dpinvite-meta-walk {
    display: inline-flex;
    align-items: center;
    gap: var(--space-xs);
    vertical-align: middle;
  }
  .dprcv-meta-walk svg,
  .dpinvite-meta-walk svg { flex-shrink: 0; opacity: 0.8; }

  /* Hero block — used to be a 2-column row (Meet/Sun) above the timeline.
     The two-column header above now carries Meeting/time/day on the right;
     sun info moved into .dprcv-sun-chip between header and bar. This
     wrapper just hosts the timeline now and intentionally adds no top
     padding so the chip + bar read as one connected unit. */
  .dprcv-hero {
    /* No padding of its own — the gap below the bar comes from the CTA
       footer's padding-top (space-lg), kept tight so the action region's
       height stays constant across the accept↔confirm slide. The pane no
       longer pads inline (the shadow gutter moved to .dprcv-footer-track), so
       the timeline sits at the header inset and lines up with the CTA. */
    padding: 0;
    display: flex;
    flex-direction: column;
  }
  /* Right-column meeting tile — every row matches the left column's
     corresponding row in font-size, weight, and color so the header
     reads as two columns of identical rhythm, not two stacks with
     different typography. */
  .dprcv-hero-label {
    /* Matches .dprcv-eyebrow */
    font-size: var(--text-label);
    font-weight: 500;
    color: var(--ink-muted);
    line-height: 1.25;
  }
  .dprcv-arrival-time {
    /* Matches .dprcv-venue (row 2). No margin-top — both columns rely
       solely on the shared flex `gap: space-xs`, so each right-column row
       sits on the same baseline as its left-column counterpart. An extra
       margin here pushed row 2 (and everything below) down out of step. */
    font-size: var(--text-display);
    font-weight: var(--fw-display);
    color: var(--panel-text);
    line-height: 1.15;
    font-variant-numeric: tabular-nums;
    letter-spacing: var(--tracking-display);
  }
  .dprcv-arrival-sub {
    /* Row 3 — aligns with the left column's .dprcv-area, so it matches
       area's line-height (1.3), not .dprcv-meta's (1.4), and carries no
       margin-top (shared flex gap only). */
    font-size: var(--text-label);
    font-weight: 500;
    color: var(--ink-muted);
    letter-spacing: 0;
    line-height: 1.3;
    font-variant-numeric: tabular-nums;
  }

  /* Sun summary chip — single strip between header and bar. Form:
     "Sol til 20:40 · ☀ 2t 40m · 21°". Sits in the accept pane above the bar
     (so it slides out with the timeline on confirm). Tight, deterministic
     spacing: a small gap above (divider → chip) and below (chip → bar) so the
     dark chip text never collides with the dark timeline canvas. */
  .dprcv-sun-chip {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-sm);
    flex-wrap: wrap;
    padding: var(--space-md) 0 0;
    margin-top: var(--space-sm);
    margin-bottom: var(--space-sm);
    border-top: 1px solid rgba(156,189,231,0.10);
    font-size: 14px;
    font-weight: 700;
    /* Ink text on the light receive page — honey-as-text fails contrast on
       the cream frost. The honey stays as the sun GLYPH (below), the brand
       "honey = sun" cue, mirroring the honey pin + ink venue name. */
    color: var(--panel-text);
    letter-spacing: 0.01em;
    font-variant-numeric: tabular-nums;
    line-height: 1;
  }
  .dprcv-sun-chip[aria-hidden="true"] { display: none; }
  .dprcv-sun-chip-glyph {
    display: inline-flex;
    align-items: center;
    color: var(--accent);
  }
  .dprcv-sun-chip-glyph svg { width: 15px; height: 15px; display: block; }
  .dprcv-sun-chip-label { color: var(--panel-text); }
  .dprcv-sun-chip-sep   { color: var(--ink-muted); opacity: 0.6; font-weight: 500; }
  .dprcv-sun-chip-detail { color: var(--panel-text); opacity: 0.92; }

  /* Timeline wrapper — interactive canvas (paint via drawAllCardTimelines).
     Drag handlers in JS update timeFromEl, which re-fires the painter.
     padding-top: 30 reserves room for the FTS-style scrubber bubble that
     floats above the marker; the bar (48 px) sits below. */
  .dprcv-timeline {
    position: relative;
    /* No padding above the bar — the sun chip sits flush against it,
       reading as "this chip describes THIS bar". The scrubber bubble
       overflows above the bar (into the sun-chip area) when active,
       which is acceptable because the bubble is only visible during
       a user-initiated scrub. */
    height: 48px;
    padding-top: 0;
    margin-top: 0;
    border-radius: 0;
  }
  /* In-bar weather row — absolute icons centred over the canvas bar area
     (offset from the wrapper top by padding-top + half bar height). One
     glyph per same-state weather band, populated by _populateTimelineWeather. */
  .dprcv-timeline-weather {
    position: absolute;
    left: 0;
    right: 0;
    /* Bar fills the wrapper now (no padding-top), so the wx row matches. */
    top: 0;
    height: 48px;
    pointer-events: none;
    z-index: 2;
  }
  .dprcv-timeline-wx-icon {
    position: absolute;
    top: 50%;
    transform: translate(-50%, -50%);
    width: 16px;
    height: 16px;
    color: rgba(255, 244, 224, 0.92);
    /* Slate-dark icons on light (sunny) bands need a faint dark backdrop
       to stay legible; honey-cream icons on dark (overcast / rain) bands
       are crisp on their own. text-shadow handles both with one ramp. */
    filter: drop-shadow(0 1px 1.5px rgba(0, 0, 0, 0.35));
  }
  .dprcv-timeline-wx-icon[data-state="shade"] { color: rgba(255, 244, 224, 0.75); }
  .dprcv-timeline-wx-icon svg { width: 100%; height: 100%; display: block; }
  /* Scrubber — cream pill spanning the full bar height + a glass time
     label hovering above. Hidden by default; .is-active reveals it.
     User taps / drags the bar → JS sets left:% based on timeFromEl,
     updates the label text, and adds .is-active. The pill stays put
     after a drag so the user can compare its position to the event
     icons above. */
  .dprcv-timeline-scrubber {
    position: absolute;
    /* Bar fills the wrapper top-to-bottom; scrubber spans the same range. */
    top: 0;
    left: 0;
    width: 0;
    height: 48px;
    pointer-events: none;
    opacity: 0;
    /* Slow slide for taps (clicks should slide to the tapped point, not
       snap). .is-dragging switches to instant so the pill tracks the
       finger during drags without lag. */
    transition: opacity 0.18s ease, left 0.22s cubic-bezier(0.32, 0.72, 0, 1);
    z-index: 3;
  }
  .dprcv-timeline-scrubber.is-active { opacity: 1; }
  .dprcv-timeline-scrubber.is-dragging {
    transition: opacity 0.18s ease, left 0s linear;
  }
  .dprcv-timeline-scrubber-pill {
    position: absolute;
    top: 0;
    left: -1.5px; /* centre the 3 px pill on x=0 of the scrubber */
    width: 3px;
    height: 100%;
    background: #FAF1DD;
    border-radius: var(--radius-none);
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45);
  }
  /* Scrubber bubble — the visual body comes from the shared .fts-popup
     class (slate-glass card + compact↔expanded morph). Only the tail
     stays scrubber-specific: a CSS clip-path triangle, since the
     scrubber never clamps against viewport edges (the FTS popup's SVG
     polygon trick isn't needed here). */
  .dprcv-timeline-scrubber-label::after {
    content: '';
    position: absolute;
    top: 100%;
    left: 50%;
    transform: translateX(-50%);
    width: 10px;
    height: 6px;
    background: rgba(17,30,56, 0.86);
    clip-path: polygon(0 0, 100% 0, 50% 100%);
    pointer-events: none;
    transition: width 0.18s cubic-bezier(0.32, 0.72, 0, 1),
                height 0.18s cubic-bezier(0.32, 0.72, 0, 1);
  }
  .dprcv-timeline-scrubber-label.fts-popup-expanded::after {
    width: 12px;
    height: 7px;
  }
  .dprcv-timeline-canvas.card-timeline-canvas {
    width: 100%;
    height: 48px;
    display: block;
    border-radius: var(--radius-md);
    cursor: grab;
    touch-action: none;
  }
  .dprcv-timeline-canvas.card-timeline-canvas:active { cursor: grabbing; }
  body.plan-preview-active .dprcv-timeline-canvas {
    display: block !important;
    opacity: 1 !important;
    pointer-events: auto !important;
  }

  /* Detail-panel timeline scrubber — the SAME hidden scrubber as the accept
     panel, hosted inside the detail card's .card-timeline (48px,
     position:relative; see components-content.css). The bare .dprcv-timeline-
     scrubber* rules above already position absolutely within that positioned
     ancestor; these rules only (a) make the detail card's own canvas draggable
     (the accept-specific .dprcv-timeline-canvas selector doesn't match here) and
     (b) let the floating bubble overflow above the bar without being clipped by
     the labels row. Cream-frost world: the slate-glass bubble (.fts-popup) +
     cream pill read as a raised object on the card, matching the accept panel. */
  #detail-panel .card-timeline { overflow: visible; }
  #detail-panel .card-timeline-block { overflow: visible; }
  #detail-panel .card-timeline .card-timeline-canvas {
    cursor: grab;
    touch-action: none;
  }
  #detail-panel .card-timeline .card-timeline-canvas:active { cursor: grabbing; }

  /* Attendees row — uses .card surface; layout-only here. */
  .dprcv-attendees {
    display: flex;
    align-items: center;
    gap: var(--space-md);
    padding: var(--space-md) var(--space-lg);
  }
  .dprcv-att-stack {
    display: flex;
    flex-shrink: 0;
  }
  .dprcv-att-stack > * + * { margin-left: -10px; }
  .dprcv-att-av {
    width: 28px; height: 28px;
    border-radius: 50%;
    box-shadow: inset 0 0 0 2px var(--bg);
    display: flex; align-items: center; justify-content: center;
    color: #fff;
    font-size: 12px;
    font-weight: 700;
    flex-shrink: 0;
  }
  .dprcv-att-info { flex: 1; min-width: 0; }
  .dprcv-att-line {
    font-size: var(--text-label);
    font-weight: 600;
    color: var(--panel-text);
    line-height: 1.3;
  }
  .dprcv-quote {
    font-size: 12px;
    font-weight: 500;
    color: var(--ink-muted);
    margin-top: var(--space-2xs);
    line-height: 1.35;
    overflow: hidden;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
  }

  /* Primary CTA wrapper — uses .p-pill; tall variant for the hero. */
  /* Sticky CTA footer — Accept button + secondary row stay at the
     bottom of the scrolling sheet so they're always visible even
     when the venue + timeline + meta combined would otherwise push
     them off-screen on small phones. The parent .dprcv-bottom is
     overflow-y: auto + display: flex column, which is what position:
     sticky needs to anchor at bottom: 0 of the scrollport.
     Transparent background — the previous version painted a darker
     glass + brightness(0.92) on top of the parent panel, which read
     as a "strange black footer band" against the sheet's main color.
     The CTAs sit directly on the sheet's own glass-panel surface
     now, matching the old screenshot. */
  .dprcv-footer {
    position: sticky;
    bottom: 0;
    /* Bottom padding clears the Android gesture bar / iOS home indicator.
       --app-pad-b already brings safe-area inset, but on Android phones
       with 0-inset gestures, "Coming later · Decline" was sitting flush
       against the OS handle. Add an explicit baseline so the secondary
       row has at least ~14 px of clearance regardless of platform inset. */
    padding: var(--space-lg) 0 var(--space-lg);
    z-index: 2;
    display: flex;
    flex-direction: column;
    gap: var(--space-md);
  }
  .dprcv-cta-primary { width: 100%; height: 52px; font-size: var(--text-body); }
  /* Destructive cancel — full-width red button sized like the primary CTA so
     the host has a clearly visible "Cancel plan" action. Color-error stroke
     + transparent fill so it doesn't compete with the honey primary above.
     Active arm state (Sikker?) fills the body. */
  .dprcv-cta-cancel {
    width: 100%;
    height: 48px;
    font-size: var(--text-body);
    background: transparent;
    color: var(--color-error);
    border: 1.5px solid var(--color-error);
    cursor: pointer;
    transition: background 120ms, color 120ms, transform 90ms;
  }
  /* :hover background lives alongside .settings-row.destructive:hover in
     components-content.css to keep that rgba literal a single occurrence. */
  .dprcv-cta-cancel:active { transform: scale(0.98); }
  .dprcv-cta-cancel[data-armed="1"] {
    background: var(--color-error);
    color: var(--text);
  }
  .dprcv-cta-cancel[disabled] { opacity: 0.55; cursor: default; }

  /* Secondary row — v1 had two equal-width .s-rnd / .g-rnd buttons that
     read as weighty alternatives to the primary CTA. The accept moment
     is meant to lead with a single confident "I'm in", with the
     alternatives demoted to discoverable but not prominent options.
     v2: text links, separated by a middot, centred under the primary
     pill. Mirrors the same demotion we did to the invite sheet's
     Cancel row. */
  .dprcv-cta-row {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-md);
    margin-top: var(--space-sm);
  }

  /* ── Accept → confirm IN-SHEET push (Phase 7) ─────────────────────────────
     The venue header stays put; only the action region slides. Two panes in a
     horizontal track inside the footer; accept out left, confirm in from right. */
  /* Clip the off-screen pane during the horizontal slide, but ONLY on the x
     axis — overflow-y stays visible so the scrubber bubble (which pops up above
     the bar) and the CTA's top/bottom shadow aren't cut off. overflow:clip (not
     hidden) is what permits a per-axis pair without forcing the other to auto.
     The space-xl horizontal SHADOW GUTTER (negative margin + matching padding)
     extends the x-clip box beyond the pane edges WITHOUT moving content (stays
     at the header inset), giving the CTA / card side shadows room. The exiting
     accept pane is pushed the extra gutter-width past the clip (see .show-confirm)
     so no sliver remains; the confirm pane is empty pre-accept so its sliver is blank. */
  .dprcv-footer-track {
    overflow: hidden; /* fallback for engines without overflow:clip */
    overflow-x: clip;
    overflow-y: visible;
    margin-inline: calc(var(--space-xl) * -1);
    padding-inline: var(--space-xl);
  }
  /* The accept pane (timeline + CTA) is always mounted and is the taller of
     the two, so it drives the track height — which therefore stays CONSTANT
     across the accept↔confirm slide (the confirm pane's cards are shorter and
     never grow the track). min-height is just a floor for the pre-paint frame
     before the timeline canvas has laid out. align-items defaults to stretch,
     so the shorter confirm pane fills the full height and its
     justify-content can position the cards within it. */
  .dprcv-action-track { display: flex; min-height: 152px; transition: transform var(--dur-slow) var(--ease-emphasized); }
  .dprcv-action-pane {
    box-sizing: border-box; flex: 0 0 100%; min-width: 0;
    display: flex; flex-direction: column; justify-content: flex-end;
    /* No inline padding: content sits at the header inset (the shadow gutter is
       on .dprcv-footer-track, outside the content). transition so the exiting
       pane can animate its extra gutter-width push in sync with the track. */
    transition: transform var(--dur-slow) var(--ease-emphasized);
  }
  /* Cards + Close anchor to the BOTTOM, where the accept CTA + secondary row
     sat — so the swap reads as the action block staying put (button → cards,
     secondary row → Close) while the sun chip + timeline slide out above.
     padding-bottom matches the accept footer's, so Close lands at the same
     height as the "Coming later · Decline" row; because cards→Close uses the
     same gap as CTA→secondary, the cards' bottom then aligns with the "I'm in"
     button's bottom and the cards span up into the timeline zone. */
  .dprcv-confirm-pane { gap: var(--space-md); justify-content: flex-end; padding-bottom: var(--space-lg); }
  .dprcv-action-track.show-confirm { transform: translateX(-100%); }
  /* Push the exiting accept pane two gutter-widths past the clip: one gutter to
     clear the pane edge, a second so the CTA's drop-shadow (which bleeds ~14px
     past that edge) also clears the clip — otherwise the button's shadow lingers
     in the left gutter on the confirm page. */
  .dprcv-action-track.show-confirm .dprcv-action-pane:not(.dprcv-confirm-pane) {
    transform: translateX(calc(var(--space-xl) * -2));
  }
  /* rocks-on-ice — the confirm action cards land with a gentle staggered spring
     as the pane arrives (cards unchanged; entrance only). backwards fill holds
     the off-screen start during the stagger, releases after. */
  .dprcv-action-track.show-confirm .dpacc-action-card { animation: dprcv-card-in var(--dur-slow) var(--ease-spring) backwards; }
  .dprcv-action-track.show-confirm .dpacc-action-card:nth-child(1) { animation-delay: 200ms; }
  .dprcv-action-track.show-confirm .dpacc-action-card:nth-child(2) { animation-delay: 270ms; }
  .dprcv-action-track.show-confirm .dpacc-action-card:nth-child(3) { animation-delay: 340ms; }
  .dprcv-action-track.show-confirm .dpacc-action-card:nth-child(4) { animation-delay: 410ms; }
  .dprcv-action-track.show-confirm .dpacc-action-card:nth-child(5) { animation-delay: 480ms; }
  @keyframes dprcv-card-in { from { transform: translateX(40px); opacity: 0; } 60% { opacity: 1; } to { transform: translateX(0); opacity: 1; } }

  /* CTA hover inside the clipped track stays at --shadow-1: --shadow-2's 28px
     blur would exceed the space-xl shadow gutter and clip again. */
  .dprcv-footer-track .dprcv-cta-primary:hover:not([disabled]) { box-shadow: var(--shadow-1); }

  /* "Confirmed" success title above the action cards (the eyebrow now reads
     "Going to · <venue>"). Bold ink title + a success-status check that springs
     in (honey stays reserved for the primary action card). Sits where the
     timeline was, so it reads as the section header for the actions below. */
  .dprcv-confirm-title {
    display: flex; align-items: center; gap: var(--space-xs);
    font-size: var(--text-body); font-weight: 700; color: var(--panel-text);
    letter-spacing: -0.01em;
  }
  .dprcv-confirm-title-check {
    display: inline-flex; align-items: center; color: var(--color-success);
    animation: dprcv-confirm-pop var(--dur-slow) var(--ease-spring) both;
  }
  .dprcv-confirm-title-check svg { width: 16px; height: 16px; }
  @keyframes dprcv-confirm-pop { 0% { transform: scale(0); opacity: 0; } 60% { opacity: 1; } 100% { transform: scale(1); opacity: 1; } }

  /* One-shot large DIAGONAL honey gleam sweeping across the WHOLE sheet as the
     confirm pane lands (skeleton-sheen energy, scaled up). The sheet is already
     a containing block (transform when open) so the ::after anchors to it; the
     wide diagonal band travels left→right and is clipped to the sheet's rounded
     top. Screen blend keeps it a gleam over the frosted lens, not a wash. */
  .dprcv-bottom.is-confirming::after {
    content: ''; position: absolute; inset: 0; pointer-events: none; z-index: 6;
    border-radius: inherit;
    background: linear-gradient(115deg, transparent 32%, var(--accent) 50%, transparent 68%);
    transform: translateX(-120%); opacity: 0; mix-blend-mode: screen;
    animation: dprcv-panel-sheen 720ms var(--ease-decelerate) forwards;
  }
  @keyframes dprcv-panel-sheen {
    0% { transform: translateX(-120%); opacity: 0; }
    18% { opacity: 0.55; }
    100% { transform: translateX(120%); opacity: 0; }
  }

  @media (prefers-reduced-motion: reduce) {
    .dprcv-action-track,
    .dprcv-action-pane { transition: none; }
    .dprcv-action-track.show-confirm .dpacc-action-card { animation: none; }
    .dprcv-confirm-title-check { animation: none; }
    .dprcv-bottom.is-confirming::after { animation: none; opacity: 0; }
  }
  .dprcv-cta-link {
    display: inline-flex;
    align-items: center;
    gap: var(--space-xs);
    background: transparent;
    border: 0;
    box-shadow: none;
    color: var(--ink-muted);
    font-family: 'Inter', sans-serif;
    font-size: var(--text-label);
    font-weight: 500;
    height: 32px;
    padding: 0 var(--space-md);
    border-radius: var(--radius-lg);
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    transition: color 0.12s ease-out, background 0.12s ease-out, transform 90ms ease-out;
  }
  .dprcv-cta-link svg { width: 13px; height: 13px; opacity: 0.7; }
  .dprcv-cta-link:hover,
  .dp-card .dp-evt-more:hover {
    color: var(--panel-text);
    background: rgba(17,30,56,0.06);
  }
  .dprcv-cta-link:active { transform: scale(0.97); }
  /* Decline = destructive role (DESIGN.md). Red text, separated from the
     honey primary; keeps the shared faint hover background. */
  .dprcv-cta-link.is-decline { color: var(--color-error); }
  .dprcv-cta-link.is-decline:hover { color: var(--color-error); }
  .dprcv-cta-sep {
    color: var(--ink-muted);
    opacity: 0.4;
    font-size: var(--text-label);
    user-select: none;
    pointer-events: none;
  }

  /* "Coming later" offset chips — popover-style grid that slides up
     above the CTA row when the user taps 'Coming later'. 4-column
     grid → 2 rows of 4 equal-width chips. Always in flow (display:
     grid, max-height 0 → 100px) so the panel's height grows smoothly
     and slides upward by the chip strip's height — same animatable
     transition as the rest of the panel. v1 used display: none → grid
     which caused the panel to jump in height with no animation. */
  .pp-later-strip {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    max-height: 0;
    padding: 0;
    opacity: 0;
    overflow: hidden;
    transition: max-height 0.28s cubic-bezier(0.32, 0.72, 0, 1),
                padding 0.22s cubic-bezier(0.32, 0.72, 0, 1),
                opacity 0.2s ease;
  }
  .pp-later-strip.open {
    max-height: 60px; /* single row of 36 px + padding */
    padding: var(--space-lg) 0 var(--space-xs);
    opacity: 1;
  }
  .pp-later-strip .pp-later-chip,
  .pp-later-strip .pp-later-custom {
    flex: 1 1 0;
    min-width: 0;
    height: 36px;
    padding: 0 var(--space-xs);
    font-size: 12.5px;
    font-weight: 600;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    /* Light/chrome sheet → outline chip (step to ink), per the component-state
       matrix. Overrides the dark .chip-pill default. */
    background: var(--surface-control);
    border: 1px solid var(--line-l-strong);
    color: var(--panel-text);
    box-shadow: none;
    /* Morph animation: Custom grows, presets shrink to fit. */
    transition: flex-grow 0.30s cubic-bezier(0.32, 0.72, 0, 1),
                background 0.18s ease-out, border-color 0.18s ease-out, color 0.18s ease-out;
  }
  /* Selected = Delft-blue fill + cream text. NOT honey — honey is reserved for
     the one CTA per screen ("Jeg blir med") + the sun signal (DESIGN.md). */
  .pp-later-strip .pp-later-chip.is-selected,
  .pp-later-strip .pp-later-custom.is-selected {
    background: var(--surface-content);
    border-color: transparent;
    color: var(--text);
  }
  /* ── Custom element: morphs in place from "Annet" chip → inline [− value +]
     stepper. The label + stepper crossfade while the element widens (flex-grow)
     and the presets shrink. One cohesive control on a single row. ── */
  .pp-later-strip .pp-later-custom {
    position: relative;
    padding: 0;
    cursor: pointer;
  }
  .pp-later-strip .pp-later-custom.is-active { flex-grow: 2.6; }
  .pp-later-strip .pp-custom-label,
  .pp-later-strip .pp-custom-stepper {
    position: absolute;
    inset: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    transition: opacity 0.2s ease;
  }
  .pp-later-strip .pp-custom-stepper { opacity: 0; pointer-events: none; }
  .pp-later-strip .pp-later-custom.is-active .pp-custom-label   { opacity: 0; pointer-events: none; }
  .pp-later-strip .pp-later-custom.is-active .pp-custom-stepper { opacity: 1; pointer-events: auto; }
  .pp-later-strip .pp-step-btn {
    width: 40px;
    align-self: stretch;
    flex-shrink: 0;
    border: none;
    background: transparent;
    color: inherit;
    font-size: var(--text-subtitle);
    font-weight: 700;
    line-height: 1;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    transition: background 120ms ease-out;
  }
  /* Dividers in Jordy (the active Custom surface is Delft). */
  .pp-later-strip .pp-step-btn:first-child { border-right: 1px solid var(--line-d); }
  .pp-later-strip .pp-step-btn:last-child  { border-left: 1px solid var(--line-d); }
  .pp-later-strip .pp-step-btn:hover  { background: var(--line-d-faint); }
  .pp-later-strip .pp-later-stepper-val {
    flex: 1 1 auto;
    text-align: center;
    font-size: var(--text-label);
    font-weight: 700;
    color: inherit;
    font-variant-numeric: tabular-nums;
  }
  @media (prefers-reduced-motion: reduce) {
    .pp-later-strip .pp-later-chip,
    .pp-later-strip .pp-later-custom,
    .pp-later-strip .pp-custom-label,
    .pp-later-strip .pp-custom-stepper { transition: none; }
  }

  @media (min-width: 640px) {
    .dprcv-bottom {
      max-width: 460px;
      margin-left: auto;
      margin-right: auto;
      border-radius: var(--radius-lg);
      margin-bottom: var(--space-lg);
    }
    .dprcv-top-pill { left: 50%; right: auto; transform: translateX(-50%) translateY(-130%); }
    .dprcv-overlay.open .dprcv-top-pill { transform: translateX(-50%) translateY(0); }
  }
  /* Plan-card "Preview" — a tertiary ghost link (.g-rnd, ink via .on-light).
     Sized down from the 48px ghost default to a compact pill that sits
     comfortably in the dense plan foot row. */
  .plan-preview-btn.g-rnd {
    height: 36px;
    padding: 0 var(--space-md);
    border-radius: var(--radius-pill);
    font-size: var(--text-label);
    gap: var(--space-xs);
  }
  .plan-preview-btn.g-rnd svg { flex-shrink: 0; }
  /* Invitations inbox in the profile panel */
  .invitations-section { padding-bottom: var(--space-xs); }
  .invitations-section .profile-section-label-sub {
    margin-top: var(--space-md);
    opacity: 0.85;
  }
  /* Stacked: info block on top, a full-width action row below. The action
     buttons are role primitives at the 44px dense-row target (Preview = .g-rnd
     ghost, Accept = .p-pill, Decline = .d-pill destructive), so three of them
     need the full width rather than a cramped right-aligned cluster. */
  .inbox-row {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: var(--space-sm);
    padding: var(--space-md) var(--space-xl);
    border-top: 1px solid rgba(17,30,56,0.08);
  }
  .inbox-row-info { min-width: 0; }
  .inbox-row-title {
    font-size: var(--text-label);
    font-weight: 600;
    color: var(--panel-text);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .inbox-row-meta {
    font-size: var(--text-caption);
    color: var(--ink-muted);
    margin-top: 1px;
  }
  /* Equal-width role buttons fill the row; height normalized to the 44px
     dense-row floor (.g-rnd is 36 by default — lift it to match the pills). */
  .inbox-row-actions {
    display: flex;
    gap: var(--space-sm);
  }
  .inbox-row-actions > * { flex: 1; height: 44px; }
  /* Friend-add prompt banner inside the detail panel */
  .friend-prompt-banner {
    display: flex;
    align-items: center;
    gap: var(--space-md);
    padding: var(--space-md) var(--space-lg);
    margin: var(--space-md) 0 var(--space-xs);
    border-radius: var(--radius-md);
    background: rgba(245,194,94,0.13);
    border: 1px solid rgba(245,194,94,0.35);
    font-size: var(--text-label);
    color: var(--panel-text);
  }
  .friend-prompt-banner-text { flex: 1; line-height: 1.35; }
  .friend-prompt-banner-add,
  .friend-prompt-banner-skip {
    border: none;
    border-radius: var(--radius-pill);
    padding: var(--space-sm) var(--space-lg);
    font-family: 'Inter', sans-serif;
    font-size: 12px;
    font-weight: 600;
    cursor: pointer;
    flex-shrink: 0;
  }
  .friend-prompt-banner-add {
    background: var(--accent);
    color: var(--accent-on, #2a1a0c);
  }
  .friend-prompt-banner-skip {
    background: transparent;
    color: var(--ink-muted);
  }
  .friend-prompt-banner-skip:hover { color: var(--panel-text); }

  /* ── Accepted panel — sharing-system redesign (.dpacc-*) ─────────────────
     Replaces the sequential question carousel with parallel action cards
     (Calendar / Directions / Open / Add friend / Share). IDs preserved
     (#post-accept-overlay, #post-accept-panel) for legacy callers. */
  /* Transparent host — matches the venue-list ↔ detail-panel pattern
     (no full-screen backdrop wash). The panel below is the only thing
     drawing on screen; tapping outside the panel still closes via the
     overlay.onclick handler.

     z-index 1250 sits ABOVE the plan-preview overlay (1200) so during
     the accept → confirm crossover slide the incoming confirm panel
     covers the outgoing accept panel rather than the other way round
     (incoming-on-top reads as 'new layer arrives' instead of 'old
     layer falls away then new layer appears'). */
  .post-accept-overlay,
  .dpacc-overlay {
    position: fixed;
    inset: 0;
    z-index: 1250;
    background: transparent;
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
    pointer-events: none;
  }
  .post-accept-overlay.open,
  .dpacc-overlay.open {
    pointer-events: auto;
  }
  .dpacc-panel {
    width: 100%;
    max-width: 460px;
    margin: 0 auto;
    /* Sheet contract — standards-based safe-area padding (var(--app-pad-b)
       = max(env(safe-area-inset-bottom), 12px), collapses to 8px when a
       form field is focused). svh cap keeps a sliver of map visible above
       the panel and never lets content push past the visible viewport. */
    padding: 0 var(--space-xl) var(--app-pad-b);
    max-height: 88svh;
    overflow-y: auto;
    overscroll-behavior: contain;
    touch-action: pan-y;
    border-radius: var(--radius-lg) var(--radius-lg) 0 0;
    background: var(--glass-panel-bg);
    backdrop-filter: var(--glass-blur-panel);
    -webkit-backdrop-filter: var(--glass-blur-panel);
    border-top: var(--glass-border);
    box-shadow: var(--sheet-shadow);
    transform: translateY(100%);
    transition: transform 0.32s cubic-bezier(0.4, 0, 0.2, 1);
    display: flex;
    flex-direction: column;
    gap: var(--space-lg);
  }
  .dpacc-panel.open { transform: translateY(0); }
  .dpacc-handle {
    display: flex;
    justify-content: center;
    padding: var(--space-md) 0 var(--space-xs);
    flex-shrink: 0;
  }
  .dpacc-grabber {
    width: 36px; height: 4px; border-radius: var(--radius-none);
    background: rgba(156,189,231,0.40);
  }

  /* Floating "you're in" toast pill — solid accent confirmation. */
  .dpacc-toast-pill {
    position: fixed;
    top: calc(env(safe-area-inset-top, 0px) + 12px);
    left: 0; right: 0;
    z-index: 1251;
    display: flex;
    justify-content: center;
    pointer-events: none;
    transform: translateY(-130%);
    transition: transform 0.36s cubic-bezier(0.4, 0, 0.2, 1);
  }
  .dpacc-overlay.open .dpacc-toast-pill { transform: translateY(0); }
  .dpacc-toast-card {
    pointer-events: auto;
    display: inline-flex;
    align-items: center;
    gap: var(--space-md);
    padding: var(--space-md) var(--space-xl) var(--space-md) var(--space-md);
    background: var(--accent);
    color: var(--accent-on, #2a1a0c);
    border-radius: var(--radius-pill);
    box-shadow: 0 6px 20px rgba(255,140,80,0.30);
    font-size: var(--text-label);
    font-weight: 700;
    font-family: 'Inter', sans-serif;
  }
  .dpacc-toast-check {
    width: 22px; height: 22px;
    border-radius: 50%;
    background: var(--accent-on, #2a1a0c);
    color: var(--accent);
    display: flex; align-items: center; justify-content: center;
    flex-shrink: 0;
  }

  /* Confirmation header — eyebrow / venue / subtitle / "Endre svar" link */
  .dpacc-header-row {
    display: flex;
    /* center the 'Change response' link with the venue name (was
     flex-start which floated it above the eyebrow). */
    align-items: center;
    justify-content: space-between;
    gap: var(--space-lg);
    padding: 0 var(--space-xs);
  }
  .dpacc-header-text { flex: 1; min-width: 0; }
  /* Sentence-case eyebrow (was ALL CAPS in v1). Same design-language
     fix as the invite sheet + accept screen. */
  .dpacc-eyebrow {
    font-size: var(--text-label);
    font-weight: 500;
    color: var(--ink-muted);
    line-height: 1.25;
    /* Indent so the eyebrow aligns with the venue name, not the pin. */
    padding-left: var(--space-2xl);
  }
  /* Venue row — pin glyph + name. Pin nudged up to match the text's
     optical centre (pin glyph mass sits below its geometric centre
     because the tip points down). Same fix as the accept-page header. */
  .dpacc-venue-row {
    display: flex;
    align-items: center;
    gap: var(--space-md);
    margin-top: var(--space-sm);
  }
  .dpacc-venue-pin {
    flex-shrink: 0;
    color: var(--accent);
    opacity: 0.9;
    height: 26px;
    display: inline-flex;
    align-items: center;
  }
  .dpacc-venue-pin svg { display: block; }
  .dpacc-venue {
    font-size: var(--text-display);
    font-weight: var(--fw-display);
    color: var(--panel-text);
    line-height: 1.15;
    text-wrap: balance;
    letter-spacing: var(--tracking-display);
    min-width: 0;
  }
  .dpacc-subtitle {
    font-size: var(--text-label);
    font-weight: 500;
    color: var(--ink-muted);
    margin-top: var(--space-xs);
    /* Indent the subtitle past the pin so it aligns with the venue name,
       not the pin. Reads as a clean header block — pin = icon, name +
       subtitle = text column. Mirrors the accept-page header. */
    padding-left: var(--space-2xl);
    font-variant-numeric: tabular-nums;
    line-height: 1.4;
  }
  /* Change-response trigger — now lives on the eyebrow row instead of
     hanging in the header's far-right corner as a pill. Reads as a
     text link with a forward chevron, not a button. */
  .dpacc-eyebrow-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-lg);
  }
  .dpacc-change-rsvp {
    display: inline-flex;
    align-items: center;
    gap: var(--space-xs);
    font-size: 12px;
    font-weight: 600;
    color: var(--ink-muted);
    background: transparent;
    border: none;
    cursor: pointer;
    padding: var(--space-xs) 0;
    white-space: nowrap;
    font-family: 'Inter', sans-serif;
    transition: color 0.12s;
    /* Cancel any inherited padding-left from .dpacc-eyebrow's own
       indent rule (it lives inside .dpacc-eyebrow-row now). */
    margin-right: calc(var(--space-2xs) * -1);
  }
  .dpacc-change-rsvp svg { opacity: 0.85; }
  .dpacc-change-rsvp:hover { color: var(--panel-text); }
  .dpacc-change-rsvp:hover svg { opacity: 1; }

  /* Action carousel — horizontally scrolling tiles. Most use .card; the
     primary tile uses solid --accent (one Primary per screen rule). */
  .dpacc-action-row {
    display: flex;
    gap: var(--space-md);
    overflow-x: auto;
    /* Wider inline padding (with matching negative margin) so the row's
       overflow clip doesn't cut the primary card's drop-shadow at the left/
       right edge; the negative margin extends into the footer-track shadow
       gutter, so cards still start at the header inset. */
    padding: var(--space-xs) var(--space-md) var(--space-sm);
    margin: 0 calc(var(--space-md) * -1);
    -webkit-overflow-scrolling: touch;
  }
  .dpacc-action-card {
    flex-shrink: 0;
    width: 110px;
    height: 96px;
    border-radius: var(--radius-md);
    color: var(--panel-text);
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    justify-content: space-between;
    padding: var(--space-lg);
    cursor: pointer;
    font-family: 'Inter', sans-serif;
    /* Secondary by default. The accepted panel is a LIGHT sheet, so use the
       on-light secondary recipe (.on-light .s-rnd): transparent fill +
       strong Delft outline + ink text, so the tile reads as a crisp
       tappable silhouette with high-contrast dark text. The leading card
       overrides to honey via .dpacc-action-primary. Before this the tiles
       were transparent/border:none (no surface → looked disabled); a brief
       dark-glass attempt muddied them with cream text on the light panel. */
    border: 1px solid var(--line-l-strong);
    background: transparent;
    box-shadow: none;
    transition: transform 0.05s;
  }
  .dpacc-action-card:active { transform: scale(0.99); }
  /* Pending friend-request state — between tap and the 4 s debounce
     firing the actual upsert. Subtle inset stripe at the top edge
     ticks down 0→100% over 4 s as a visual undo window, plus a
     gentle accent tint so the card reads as 'in flight'. */
  .dpacc-action-card[data-pending="1"] {
    position: relative;
    overflow: hidden;
  }
  .dpacc-action-card[data-pending="1"]::after {
    content: '';
    position: absolute;
    left: 0;
    bottom: 0;
    height: 2px;
    width: 100%;
    background: currentColor;
    opacity: 0.45;
    transform-origin: right center;
    animation: dpacc-friend-debounce 3s linear forwards;
  }
  @keyframes dpacc-friend-debounce {
    from { transform: scaleX(1); }
    to   { transform: scaleX(0); }
  }
  /* Three-phase commit animation per user spec:
       1. Fade out the friend card in place (opacity 0).
       2. Cards to the right slide left into the new positions (FLIP).
       3. As they slide, the new leading card transitions from glass
          surface → honey accent (CSS bg/color transitions).
     The fading state is purely opacity; the FLIP translateX is set
     inline by JS so we can use the FLIP technique (record old
     positions → remove card → invert transform → animate to 0). */
  .dpacc-action-card.is-fading {
    opacity: 0;
    transition: opacity 0.22s ease;
    pointer-events: none;
  }
  /* Animate bg + color when classes flip (e.g. .card → .dpacc-action-
     primary on promotion). Transform stays fast (0.05 s) so the
     :active press feedback isn't sluggish. The FLIP slide for the
     friend-card commit uses Web Animations API directly so it
     doesn't override these CSS transitions. */
  .dpacc-action-card {
    transition: background 0.34s ease, color 0.34s ease, border-color 0.34s ease, transform 0.05s;
  }
  /* Primary tile — solid accent, accent-on text. Drop the glass border so
     the honey block reads clean (the base border is for the glass tiles). */
  .dpacc-action-primary {
    background: var(--accent);
    color: var(--accent-on, #2a1a0c);
    border-color: transparent;
    box-shadow: 0 2px 8px rgba(0,0,0,0.35);
  }
  .dpacc-action-primary:hover { background: var(--accent-hover); }
  .dpacc-action-icon {
    width: 30px; height: 30px;
    border-radius: var(--radius-sm);
    background: var(--line-l-faint);
    border: 1px solid var(--line-l);
    display: flex; align-items: center; justify-content: center;
    color: var(--ink-muted);
  }
  .dpacc-action-primary .dpacc-action-icon {
    background: rgba(42,26,12,0.22);
    border-color: rgba(42,26,12,0.32);
    color: var(--accent-on, #2a1a0c);
  }
  .dpacc-action-text { text-align: left; }
  .dpacc-action-title {
    font-size: 12.5px;
    font-weight: 700;
    line-height: 1.15;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    max-width: 86px;
  }
  .dpacc-action-sub {
    font-size: var(--text-caption);
    font-weight: 500;
    color: var(--ink-muted);
    margin-top: var(--space-2xs);
    font-variant-numeric: tabular-nums;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    max-width: 86px;
  }
  .dpacc-action-primary .dpacc-action-sub { color: rgba(42,26,12,0.65); }

  /* Attendees row — uses .card surface; layout-only here. */
  .dpacc-attendees {
    display: flex;
    align-items: center;
    gap: var(--space-lg);
    padding: var(--space-lg) var(--space-lg);
  }
  .dpacc-att-stack { display: flex; flex-shrink: 0; }
  .dpacc-att-stack > * + * { margin-left: -10px; }
  .dpacc-att-av {
    width: 32px; height: 32px;
    border-radius: 50%;
    box-shadow: inset 0 0 0 2px var(--bg);
    display: flex; align-items: center; justify-content: center;
    color: #fff;
    font-size: 14px;
    font-weight: 700;
    flex-shrink: 0;
  }
  .dpacc-att-info { flex: 1; min-width: 0; }
  .dpacc-att-count {
    font-size: var(--text-label);
    font-weight: 700;
    color: var(--text);
    line-height: 1.3;
  }
  .dpacc-att-names {
    font-size: 12px;
    font-weight: 500;
    color: var(--muted);
    margin-top: var(--space-2xs);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  /* Close — demoted from full-width .s-rnd pill to a small centred
     text link, matching the same demotion we did to the invite
     sheet's Cancel button and the accept screen's Decline. The
     post-accept panel has multiple dismissal paths (handle drag,
     backdrop tap, action-card 'open', Change response link), so a
     prominent Close pill is overkill. */
  .dpacc-close-row {
    display: flex;
    justify-content: center;
    margin-top: var(--space-xs);
    padding-bottom: env(safe-area-inset-bottom, 0px);
  }
  .dpacc-close-link {
    background: transparent;
    border: 0;
    box-shadow: none;
    color: var(--muted);
    font-family: 'Inter', sans-serif;
    font-size: var(--text-label);
    font-weight: 500;
    height: 36px;
    padding: 0 var(--space-lg);
    border-radius: var(--radius-lg);
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    transition: color 0.12s ease-out, background 0.12s ease-out, transform 90ms ease-out;
  }
  .dpacc-close-link:hover {
    color: var(--text);
    background: rgba(255,244,224,0.06);
  }
  .dpacc-close-link:active { transform: scale(0.97); }

  /* Per-invitee time chip on plan-preview attendees row */
  .pp-av-wrap {
    position: relative;
    display: inline-block;
  }
  .pp-av-time {
    position: absolute;
    bottom: -8px;
    left: 50%;
    transform: translateX(-50%);
    background: var(--accent);
    color: var(--accent-on, #2a1a0c);
    font-size: 9px;
    font-weight: 700;
    padding: 1px var(--space-xs);
    border-radius: var(--radius-pill);
    line-height: 1.2;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    pointer-events: none;
    box-shadow: 0 1px 4px rgba(0,0,0,0.35);
  }
  /* Off-plan-arrival summary row in the inbox "Your plans" section */
  .inbox-row-arrivals {
    margin-top: var(--space-2xs);
    font-size: var(--text-caption);
    color: var(--accent);
    font-variant-numeric: tabular-nums;
    line-height: 1.3;
  }
  /* Per-invitee tiny arrival-time chip on the detail-panel plan card.
     Honey-dim on cream (deep amber on dim honey) — solid honey washes out on
     the cream plans tile, so it mirrors the WHEN chip treatment. */
  .plan-invitee .pi-time {
    position: absolute;
    bottom: -7px;
    left: 50%;
    transform: translateX(-50%);
    background: var(--accent-dim);
    border: 1px solid var(--accent-border);
    color: var(--accent-on-light);
    font-size: 8.5px;
    font-weight: 700;
    padding: 0 var(--space-xs);
    border-radius: var(--radius-pill);
    line-height: 1.4;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    pointer-events: none;
  }
  /* Invitee status pips on the plan card. Avatars sit on the cream tile, so
     the pip ring is cream and the initial circle is Delft-on-cream. */
  .plan-invitees {
    display: flex;
    align-items: center;
    gap: var(--space-md);
    flex-wrap: wrap;
  }
  .plan-invitee {
    position: relative;
    width: 24px; height: 24px;
    border-radius: 50%;
    overflow: visible;
  }
  .plan-invitee img,
  .plan-invitee .pi-init {
    width: 24px; height: 24px;
    border-radius: 50%;
    display: block;
  }
  .plan-invitee img { object-fit: cover; }
  .plan-invitee .pi-init {
    background: var(--line-l);          /* faint ink wash on the cream tile */
    color: var(--panel-text);
    font-size: 10px;
    font-weight: 700;
    display: flex; align-items: center; justify-content: center;
  }
  .plan-invitee .pi-pip {
    position: absolute;
    right: -2px; bottom: -2px;
    width: 10px; height: 10px;
    border-radius: 50%;
    border: 2px solid var(--card-cream-bg);  /* ring matches the cream tile */
  }
  .pi-pip-accepted { background: var(--color-success); }
  .pi-pip-declined { background: var(--color-error); }
  .pi-pip-pending  { background: var(--ink-muted); }

  /* ── Card friend badges ─────────────────────────────────────────────────── */
  .card-friend-badge {
    display: inline-flex;
    gap: var(--space-2xs);
    margin-left: var(--space-sm);
    vertical-align: middle;
  }
  .card-friend-dot {
    width: 16px; height: 16px;
    border-radius: 50%;
    object-fit: cover;
    border: 1px solid var(--border);
    display: inline-block;
    vertical-align: middle;
  }
  .card-friend-dot-init {
    background: rgba(17,30,56,0.6);
    color: var(--muted);
    font-size: 8px; font-weight: 600;
    display: inline-flex; align-items: center; justify-content: center;
  }
  /* "Friends going" badge — same shape as the live-checkin badge but blue
     border to distinguish "planned" from "here now" (orange). */
  .card-going-badge {
    display: inline-flex;
    gap: var(--space-2xs);
    margin-left: var(--space-sm);
    vertical-align: middle;
  }
  .card-going-dot {
    width: 16px; height: 16px;
    border-radius: 50%;
    object-fit: cover;
    border: 1px solid rgba(156,189,231,0.55);
    display: inline-block;
    vertical-align: middle;
  }
  .card-going-dot-init {
    background: rgba(17,30,56,0.6);
    color: rgba(156,189,231,0.95);
    font-size: 8px; font-weight: 600;
    display: inline-flex; align-items: center; justify-content: center;
  }

  /* ── Friends modal ──────────────────────────────────────────────────────── */
  .friends-modal-overlay {
    position: fixed;
    inset: 0;
    z-index: 2000;
    background: var(--scrim);
    display: none;
    align-items: center;
    justify-content: center;
    padding: var(--space-xl);
  }
  .friends-modal-overlay.open { display: flex; }
  .friends-modal-card {
    width: 100%;
    max-width: 400px;
    max-height: calc(var(--app-h, 100svh) * 0.8);
    overflow-y: auto;
    padding: var(--space-xl);
    border-radius: var(--radius-lg);
    /* Modal surface (DESIGN.md Phase 2.5): Delft 90% + dark hairline + pop
       shadow-3, over the --scrim overlay. Was transparent --glass-panel-bg +
       raw blur(20px). Cream text. */
    background: var(--surface-modal);
    border: 1px solid var(--line-d);
    box-shadow: var(--shadow-3);
    backdrop-filter: var(--blur-surface);
    -webkit-backdrop-filter: var(--blur-surface);
  }
  .friends-modal-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: var(--space-xl);
  }
  .friends-modal-header h3 {
    margin: 0;
    font-size: 16px;
    color: var(--text);
  }
  .friends-modal-close {
    background: none;
    border: none;
    color: var(--muted);
    font-size: var(--text-subtitle);
    cursor: pointer;
    padding: var(--space-xs);
  }
  .friends-section-label {
    font-size: var(--text-caption);
    color: var(--muted);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.5px;
    margin: var(--space-lg) 0 var(--space-sm);
  }
  .friends-empty {
    font-size: var(--text-label);
    color: var(--muted);
    padding: var(--space-md) 0;
  }
  .friend-item {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: var(--space-md) 0;
    border-bottom: 1px solid rgba(156,189,231,0.08);
  }
  .friend-item-left {
    display: flex;
    align-items: center;
    gap: var(--space-md);
    min-width: 0;
    flex: 1;
  }
  .friend-avatar {
    width: 32px; height: 32px;
    border-radius: 50%;
    object-fit: cover;
    flex-shrink: 0;
  }
  .friend-avatar-initials {
    background: rgba(17,30,56,0.6);
    color: var(--muted);
    display: flex; align-items: center; justify-content: center;
    font-size: var(--text-label); font-weight: 600;
  }
  .friend-item-info {
    min-width: 0;
  }
  .friend-item-name {
    font-size: var(--text-label);
    color: var(--text);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  .friend-checkin-info {
    font-size: var(--text-caption);
    color: var(--accent);
  }
  .btn-icon-sm {
    background: none;
    border: none;
    color: var(--muted);
    cursor: pointer;
    font-size: 14px;
    padding: var(--space-xs);
    opacity: 0.5;
    transition: opacity 0.15s;
  }
  .btn-icon-sm:hover { opacity: 1; color: var(--color-error); }

  /* Add friend input */
  .friends-add-section { margin-top: var(--space-lg); }
  .friends-add-row {
    display: flex;
    gap: var(--space-sm);
  }
  .friends-search-input {
    flex: 1;
    padding: var(--space-md) var(--space-md);
    background: rgba(17,30,56,0.5);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    color: var(--text);
    font-size: var(--text-label);
    font-family: 'Inter', sans-serif;
  }
  .friends-search-input:focus { outline: none; border-color: var(--accent); }
  .friend-add-result {
    font-size: 12px;
    margin-top: var(--space-xs);
    min-height: 16px;
  }
  .friend-add-result.success { color: var(--color-success); }
  .friend-add-result.error { color: var(--color-error); }

