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

/* Visually hidden but present for screen readers/document outline — e.g. a
   page's real <h1> where the visual design carries the title some other way. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

:root {
  --vn-teal: #10702A;
  --vn-teal-light: #E3FBEA;
  --vn-teal-mid: #1E9E52;
  /* Tints for a control sitting ON a --vn-teal field (the day-box header's
     Plan B trigger and spend chip today). They exist because the obvious
     idiom — a translucent white fill and border — bakes in "the field behind
     me is dark", which is only true in light mode: --vn-teal flips to a
     bright mint after dark, where white-on-white leaves the control with no
     visible edge at all. Same reasoning as the --surface-2 foreground rule
     documented on --vn-teal in the dark block below; these are its fill and
     keyline counterparts, so a control on a brand field never needs a
     literal rgba(255,255,255,…) again. */
  /* ⚠️ The alpha here is contrast-critical, not taste. The foreground on
     these controls is --surface-2, which is pure white in light mode, and
     every point of white tint added to the deep-green field underneath
     raises the ground toward that same white: 0.20 measures 4.09:1 and
     fails, 0.12 measures 4.84:1. The hover therefore moves the KEYLINE
     rather than pushing more white into the fill — a brighter edge is
     legible feedback that cannot walk the text into a failure. */
  --on-brand-fill: rgba(255, 255, 255, 0.12);
  --on-brand-fill-hover: rgba(255, 255, 255, 0.14);
  --on-brand-line: rgba(255, 255, 255, 0.6);
  --on-brand-line-hover: rgba(255, 255, 255, 0.95);
  --vn-amber: #BA7517;
  --vn-amber-light: #FAEEDA;
  --vn-amber-mid: #EF9F27;
  /* --vn-amber fails WCAG AA as text (3.72:1 / 3.41:1, issue #354 Phase 2)
     despite passing as a fill/border — this darkened variant is for `color`
     only. Keep --vn-amber itself unchanged; it's correct for backgrounds/
     borders/chips. */
  --vn-amber-text: #8A5610;
  --vn-coral: #993C1D;
  --vn-coral-light: #FAECE7;
  --vn-coral-mid: #D85A30;
  /* Issue #354 Phase 5 — several rules already read `var(--error)`
     expecting this token to exist, but it was never defined anywhere, so
     every one of those was silently always falling back to the literal.
     Defined once here (not per-theme) since that's exactly the behavior
     those call sites already had — a genuinely distinct "danger red" hue
     from --vn-coral's branded terracotta, used only for destructive
     drop-zone/delete affordances. */
  --error: #e53935;
  --error-light: #fdecea;
  /* Issue #545 harden pass — --error itself measures ~3.98:1 as text on
     --df-panel, clearing the ~3:1 UI-component/border floor but missing the
     4.5:1 text floor (e.g. .agent-diff-badge's "Removed" state, which
     renders this as `color`, not just a border). Same fill-vs-text split as
     --vn-sky-text/--vn-coral-text above: --error stays correct as a border/
     background hue, this is the text-safe ink for when it's read as copy. */
  --error-text: #B3261E;
  --text-primary: #1a1a1a;
  --text-secondary: #666666;
  /* WCAG AA fix (issue #354 Phase 2) — #999999 was 2.85:1 / 2.61:1 against
     white/--surface-1, well under the 4.5:1 floor for body text. */
  --text-muted: #6E6E6E;
  --surface-1: #f5f5f5;
  --surface-2: #ffffff;
  --border: #e0e0e0;
  --border-strong: #d0d0d0;
  --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;

  /* Type scale — the single source of truth for font sizes app-wide.
     Use these (via var(--fs-*)) instead of hardcoding px in new styles,
     grouped by semantic role rather than by pixel value. */
  --fs-xs: 11px;    /* micro meta text: badges, timestamps, tiny hints */
  --fs-sm: 12px;    /* secondary/muted helper text, small labels */
  --fs-base: 13px;  /* form labels, section subheadings, default UI text */
  --fs-md: 14px;    /* body copy, input text, primary card text */
  --fs-lg: 16px;    /* emphasized values, sub-headings */
  --fs-xl: 18px;    /* modal titles, panel titles */
  --fs-xxl: 22px;   /* deprecated — legacy alias, use --fs-2xl below in new/updated rules */

  /* Extends the scale upward (issue #354 Phase 3) — 24/26/28/32/34/36/46/48px
     were all in use (mostly the landing page) above the old --fs-xxl ceiling
     with no token covering any of them. */
  --fs-2xl: 22px;   /* screen/page titles — same value as --fs-xxl, the token new/updated rules should use */
  --fs-3xl: 26px;   /* absorbs 24 / 26 / 28px */
  --fs-4xl: 32px;   /* absorbs 32 / 34 / 36px */
  --fs-5xl: 46px;   /* absorbs 46 / 48px — landing hero */

  /* WCAG 2.2 SC 2.5.8 (Target Size Minimum) floor for any tappable control.
     Apply as min-height/min-width, never height/width, so a button's own
     content-driven sizing still wins when it's already larger. */
  --tap-min: 44px;

  /* Radius scale — the single source of truth for border-radius. */
  --radius-xs: 6px;     /* micro badges/tags/small icon buttons */
  --radius-sm: 10px;    /* controls: inputs, buttons, form elements */
  --radius-md: 16px;    /* cards: any bordered content block with padding */
  --radius-pill: 999px; /* fully-rounded chips/pills */

  /* Spacing scale — the single source of truth for padding/margin/gap.
     2px-based and derived from values already in use (see issue #354) so
     adoption is a rename, not a reflow. A 4px/8pt grid was rejected: the
     three most-used values in this codebase are 8/10/6px, and snapping to
     4px would reflow ~234 declarations by 2px each. --hairline is for
     borders/dividers only — never use it for layout rhythm. */
  --space-0:    0;
  --hairline:   1px;
  --space-2xs:  2px;
  --space-xs:   4px;
  --space-sm:   6px;
  --space-md:   8px;
  --space-lg:  10px;
  --space-xl:  12px;
  --space-2xl: 14px;
  --space-3xl: 16px;
  --space-4xl: 20px;
  --space-5xl: 32px;

  /* Shadow scale — resting-state elevation for cards (the app was
     previously almost pure flat/border-only). */
  --shadow-resting: 0 1px 2px rgba(15,23,42,0.06), 0 1px 3px rgba(15,23,42,0.08);
  --shadow-hover:   0 4px 12px rgba(15,23,42,0.10);

  /* Motion scale — the single source of truth for durations/easing, same
     convention as the type and spacing scales above. Three tiers, chosen by
     how far the thing being animated actually travels:
       feedback — acknowledge a tap; anything longer reads as latency
       state    — a thing changed in place (expand, indicator, chip)
       view     — a panel, sheet or overlay arrived
     --ease-out is an exponential decelerate: fast departure, long settle, so
     an arrival looks confident rather than mechanical. Deliberately NOT a
     bounce/elastic curve — this is a trip planner people use one-handed on
     the move, and overshoot on a list of times reads as instability.
     --trace-tick is the stagger unit for the itinerary route trace only (see
     the Itinerary route trace section); it is not a general delay token. */
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --dur-feedback: 120ms;
  --dur-state: 200ms;
  --dur-view: 280ms;
  --trace-tick: 38ms;

  /* Additional semantic hues, formalizing two colors that were
     previously one-off hardcoded hex values (cat-hotel, cat-activity). */
  --vn-indigo: #4338CA;
  --vn-indigo-light: #EEF2FF;
  --vn-green: #166534;
  --vn-green-light: #F0FDF4;
  --vn-green-mid: #22883f;
  --vn-purple: #7E22CE;
  --vn-purple-light: #F5E8FF;
  --vn-sky: #0284C7;
  --vn-sky-light: #E0F2FE;
  /* --vn-sky fails WCAG AA as text (4.10:1 / 3.76:1, issue #354 Phase 2) —
     same fill/border-vs-text split as --vn-amber-text above. */
  --vn-sky-text: #046494;
  /* The rest of the fill-vs-text split, completed. Amber and sky already had
     text-safe variants; coral, indigo and purple were still setting the raw
     hue as .cat-pill's `color`, which holds in light mode but not against
     their own dark-mode tint: measured 3.97:1 (shopping), 4.32:1
     (accommodation) and 4.4955:1 (attraction) on real pixels. In light mode
     these are identical to the base hue, so nothing changes there — the
     token exists so the dark block below has somewhere to diverge. */
  --vn-coral-text: #993C1D;
  --vn-indigo-text: #4338CA;
  --vn-purple-text: #7E22CE;

  /* Day plan flip (issue #154) — its own color channel, deliberately NOT
     reusing any --vn-* above: every one of those is already an itinerary
     category accent (amber=food, teal=transport, coral=shopping,
     indigo=hotel, green=activity, purple=attraction — see .cat-pill /
     .itin-cal-block.cat-*), so reusing one would collide with the category
     pill on the same stop card. Plan A gets a new warm-brown pair; Plan B
     reuses --vn-sky, the one hue with no category already claiming it.
     Brown vs blue (not green vs brown) is deliberate: green already reads
     as "brand/success" throughout this app, and green-vs-brown is close to
     the worst pairing for red-green colorblindness. */
  --plan-a-accent: #8A5A2B;
  --plan-a-bg: #F6EEE4;

  /* Fixed (non theme-aware) top-nav-bar colors — the bar is always the
     literal brand green regardless of light/dark mode, so its own
     foreground tones are pinned rather than following --vn-teal (which
     itself flips to a bright mint in dark mode).

     These two were transposed from the day they landed (391fb91): --topbar-bg
     held #54ff82, the *dark-mode* step of --vn-teal, which painted the bar
     neon mint in both themes and left the white pills sitting on it (logo
     badge, profile trigger, notification bell) with almost no separation —
     the exact opposite of what the comment above described. #354 Phase 2
     then audited --topbar-fg-muted against that wrong background and
     concluded no alpha could clear AA; that constraint was an artifact of
     the transposition, not of the design. */
  --topbar-bg: #10702A;
  --topbar-fg: #ffffff;
  /* 4.98:1 against --topbar-bg — a real muted tier is reachable again now
     that the background is the brand green rather than the mint (see above).
     Kept as its own token so call sites keep naming "muted" intent and a
     future tweak has one place to change. */
  --topbar-fg-muted: rgba(255, 255, 255, 0.85);
  /* The bar's own accent: brand green for glyphs and text that sit on the
     white pills *inside* the bar, where --topbar-fg would be white-on-white.
     Pinned for the same reason as the pair above — the pills are fixed white
     in both themes, so a theme-aware --vn-teal would flip to mint on them
     and drop to ~1.4:1. 6.23:1 on #fff. */
  --topbar-accent: #10702A;

  /* ==================== Departure Fever — converted app surfaces ==========
     DESIGN.md's world is normative everywhere, and the in-app screens are
     being converted to it one whole screen at a time (My Trips is the first;
     the auth gate was converted before it, in css/landing.css). These are
     that system's ground/stock/ink/type/geometry values for screens inside
     the app shell, and they live at :root rather than on a screen class so
     the next screen to convert reads them without redeclaring anything.

     Why these are NOT the landing page's --lp-* tokens: the poster and the
     app are two surfaces, and DESIGN.md's Aliased Ink Rule is explicit that a
     surface owns its ground, stock, type and geometry while the *brand*
     belongs to the product. So the two ground values below deliberately
     restate the poster's apricot/green-black, and every ink a CONTROL is
     painted in is an alias of --vn-* and cannot drift off-brand. If the
     palette moves, --vn-* is still the one place it moves.

     ⚠️ --df-brand follows --vn-teal, which is a DEEP GREEN in light mode and
     a BRIGHT MINT in dark (issue #432). Anything filled with it takes
     --df-brand-fg as its foreground, never a literal white. A full-bleed
     plane of green takes --topbar-bg (Field Green), never --df-brand. */
  --df-ground: #ffe3c4;      /* Noon Apricot — the day ground, sunlight not paper */
  --df-panel: #fff7ec;       /* Cut Stock — what a card, stub or panel is cut from */
  --df-ink: #101a14;         /* Green-Black — body text, every keyline, every shadow */
  --df-ink-soft: #4a5851;    /* 5.9:1 on --df-ground */
  --df-hair: rgba(16, 26, 20, 0.16);
  /* Ghost-chip fill for a small secondary control sitting on a neutral
     --df-panel field (day header actions) — the ink-tint equivalent of
     --on-brand-fill, which only works on a solid --vn-teal/coral fill. */
  --df-chip-fill: rgba(16, 26, 20, 0.06);
  --df-chip-fill-hover: rgba(16, 26, 20, 0.12);

  --df-brand: var(--vn-teal);
  --df-brand-fg: var(--surface-2);
  --df-coral: var(--vn-coral);
  --df-amber: var(--vn-amber);

  /* The halftone dot screen. A real 6px radial grid, not noise — the printed
     artefact is the whole visual argument, which is also why nothing on a
     converted screen may put a backdrop-filter over it (it would blur the
     dots inside the bar and leave them crisp below it: a ghost band). */
  --df-halftone: radial-gradient(rgba(16, 26, 20, 0.16) 0.9px, transparent 1px);
  --df-wash:
    radial-gradient(85% 60% at 15% -5%, rgba(239, 159, 39, 0.30), transparent 60%),
    radial-gradient(75% 55% at 92% 4%, rgba(153, 60, 29, 0.11), transparent 62%);

  --df-display: 'Bricolage Grotesque Variable', 'Arial Black', Impact, sans-serif;
  --df-sans: 'Figtree Variable', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  --df-mono: 'Martian Mono Variable', ui-monospace, 'SF Mono', Menlo, monospace;
  --df-ease: cubic-bezier(0.22, 1, 0.36, 1);

  /* Radii are chosen by what the object IS, not by its size: pills for
     anything pressable, 14px for cards, 12px for stubs, 3px for a photo
     print's stock edge. Deliberately separate from --radius-* above, which
     is the retired system's size-driven scale. */
  --df-radius-stub: 12px;
  --df-radius-card: 14px;
  --df-radius-print: 3px;

  /* Depth is hard offset ink — a flat block of colour with an opaque,
     zero-blur shadow, offset diagonally, like a sticker. Pressing moves the
     object INTO its own shadow. No blur radius exists anywhere in this
     system except under a photographic print. */
  --df-press:        4px 4px 0 var(--df-ink);
  --df-press-large:  5px 5px 0 var(--df-ink);
  --df-press-hover:  2px 2px 0 var(--df-ink);
  --df-press-active: 0 0 0 var(--df-ink);
  /* The one sanctioned blurred shadow in the system — a physical print
     lifted off the page. Ink-based, not pure black, same reasoning as every
     other shadow here; doesn't flip with theme (a print is a separate
     object in both, and this is DESIGN.md's literal token). */
  --df-press-lift: 0 1px 0 rgba(9, 20, 13, 0.1), 0 22px 40px -26px rgba(9, 20, 13, 0.75);

  /* Full-screen scrim behind a lit-up photo (js/itinerary.js's viewPhoto()/
     viewPlacePhoto()) — undocumented rgba(0,0,0,*) literals before this,
     found by the design critique's detector pass (issue #545 follow-up).
     Ink-based like every other shadow/overlay in the system, not literal
     black; doesn't flip with theme, matching Instant Stock's own
     never-inverts precedent — a lightbox dims the app around the photo the
     same way in both themes. */
  --df-scrim: rgba(16, 26, 20, 0.92);
  --df-scrim-soft: rgba(16, 26, 20, 0.82);
}

@media (prefers-color-scheme: dark) {
  :root {
    /* Night market. Only the ground/stock/ink change — no brand ink is
       restated here, because --df-brand/-coral/-amber alias --vn-* tokens
       this same block already flips further down. Restating them would be a
       second source of truth for one colour. */
    --df-ground: #0c1410;
    --df-panel: #151d17;
    --df-ink: #ffeeda;         /* Lamplight — warm, never a cold grey */
    --df-ink-soft: #9aa79f;
    --df-hair: rgba(255, 238, 218, 0.18);
    --df-chip-fill: rgba(255, 238, 218, 0.08);
    --df-chip-fill-hover: rgba(255, 238, 218, 0.16);
    --df-halftone: radial-gradient(rgba(255, 238, 218, 0.09) 0.9px, transparent 1px);
    --df-wash:
      radial-gradient(85% 60% at 15% -5%, rgba(232, 163, 61, 0.13), transparent 60%),
      radial-gradient(75% 55% at 92% 4%, rgba(226, 114, 74, 0.15), transparent 62%);

    --text-primary: #f0f0f0;
    --text-secondary: #b3b3b3;
    /* WCAG AA fix (issue #354 Phase 2) — #808080 passed against
       --surface-1 (#1a1a1a, 4.41:1) but failed against --surface-2
       (#242424, 3.93:1); this audit was missed when --text-muted was
       first set, since only light mode had been measured. */
    --text-muted: #949494;
    --surface-1: #1a1a1a;
    --surface-2: #242424;
    --border: #3a3a3a;
    --border-strong: #4a4a4a;

    /* ⚠️ --vn-teal is a *bright mint* in dark mode, not a dark green — so
       anything painted on top of a --vn-teal (or --vn-coral) fill must use
       var(--surface-2) as its foreground, never a literal white/#fff.
       Literal white on this value is 1.31:1; --surface-2 flips to #242424
       here and gives 11.83:1 (and stays #ffffff / 6.23:1 in light mode).
       Issue #432 — 13 rules had hardcoded white and every primary button in
       the app was unreadable in dark mode. css/landing.css already used the
       var(--surface-2) form, which is why its CTA was unaffected. */
    --vn-teal: #54ff82;
    --vn-teal-light: rgba(84,255,130,0.16);
    --vn-teal-mid: #7CFFA0;
    /* The dark half of the on-brand tints (see the light block). The field
       below them is bright mint here, so the tint has to be ink, not white —
       derived from --text-primary's #1a1a1a with a green bias so it reads as
       a shadow on the brand rather than a grey wash. */
    --on-brand-fill: rgba(12, 26, 16, 0.16);
    --on-brand-fill-hover: rgba(12, 26, 16, 0.28);
    --on-brand-line: rgba(12, 26, 16, 0.45);
    --on-brand-line-hover: rgba(12, 26, 16, 0.8);
    --vn-amber: #E8A33D;
    --vn-amber-light: rgba(232,163,61,0.16);
    --vn-amber-mid: #F2B45A;
    /* Already clears AA as text on both dark surfaces (issue #354 Phase 2
       audit) — no darkening needed here, unlike the light-mode variant. */
    --vn-amber-text: #E8A33D;
    --vn-coral: #E2724A;
    --vn-coral-light: rgba(226,114,74,0.16);
    --vn-coral-mid: #EE9270;
    --vn-indigo: #8B93F5;
    --vn-indigo-light: rgba(139,147,245,0.16);
    /* Lightened for `color` only, against each hue's own 16% tint over
       --surface-2 (#242424): 5.29:1, 5.43:1 and 5.42:1 respectively, up from
       3.97 / 4.32 / 4.50. Coral's value is --vn-coral-mid, already in the
       palette; the other two are its indigo and purple equivalents. Keep the
       base hues unchanged — they are correct as fills and borders. */
    --vn-coral-text: #EE9270;
    --vn-indigo-text: #A8AEF8;
    --vn-purple-text: #CB9AFD;
    --vn-green: #4ADE80;
    --vn-green-light: rgba(74,222,128,0.14);
    --vn-green-mid: #22c55e;
    --vn-purple: #C084FC;
    --vn-purple-light: rgba(192,132,252,0.16);
    --vn-sky: #38BDF8;
    --vn-sky-light: rgba(56,189,248,0.16);
    /* Already clears AA as text on both dark surfaces — same reasoning as
       --vn-amber-text above. */
    --vn-sky-text: #38BDF8;
    /* --error is unchanged between themes (see the light-mode comment), so
       its text-safe ink needs its own dark value: ~4.85:1 against dark
       --df-panel, lightened the same way --vn-teal/--vn-coral lighten for
       dark mode rather than reusing the light-mode --error-text value. */
    --error-text: #FF3B30;

    --plan-a-accent: #C9925C;
    --plan-a-bg: rgba(201,146,92,0.16);

    --shadow-resting: 0 1px 3px rgba(0,0,0,0.45);
    --shadow-hover:   0 6px 16px rgba(0,0,0,0.55);
  }
}

body, input, select, textarea, button {
  font-family: var(--font-sans);
}

body {
  font-size: var(--fs-md);
  color: var(--text-primary);
  background: var(--surface-1);
}

/* Applied by lockBodyScroll()/unlockBodyScroll() (js/ui.js) while any modal
   is open. position:fixed (not overflow:hidden alone) is what actually
   stops iOS Safari's rubber-band scroll from reaching the document behind
   a fixed-position overlay; `top` is set inline per-lock to the negative
   scroll offset so the page doesn't visibly jump to 0 while locked. */
body.scroll-locked {
  position: fixed;
  left: 0;
  right: 0;
  width: 100%;
}

/* ==================== Landing Page ==================== */
/* .landing-screen is #auth-screen's structural base — flex-centers a column
   in its auth-gate state, block-fills for the marketing page's own layout.
   Everything about how it actually PAINTS (ground, ink, type, the .ap-*
   auth-gate components) now lives in css/landing.css: the auth gate was
   migrated onto the same "Departure Fever" system as the marketing page it
   sits behind (DESIGN.md — normative everywhere, no exceptions; this was
   the one surface still on the retired Field Journal look). See the
   "Auth gate" section at the top of css/landing.css. */
.landing-screen {
  min-height: 100vh;
}

.landing-buttons {
  display: flex;
  gap: var(--space-xl);
  flex-direction: column;
  width: 100%;
  max-width: 300px;
}

.btn-large {
  padding: var(--space-2xl) var(--space-4xl);
  min-height: var(--tap-min);
  font-size: var(--fs-lg);
  font-weight: 500;
  border: none;
  border-radius: var(--radius-sm);
  cursor: pointer;
  font-family: inherit;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-lg);
  transition: all 0.3s ease;
}

/* .btn-create/.btn-join are only used on the auth gate's main CTAs plus the
   retry button — their actual paint now lives in css/landing.css alongside
   the rest of the #auth-panel "Departure Fever" component set, so a plain
   .btn-large.btn-create with no #auth-panel ancestor still gets a sane
   (if unthemed) fallback rather than nothing. */
.btn-create {
  background: var(--vn-teal);
  color: var(--surface-2);
  border: none;
}

.btn-join {
  background: var(--surface-1);
  color: var(--text-primary);
  border: 1.5px solid var(--border-strong);
}

/* ==================== Modal ==================== */
.modal {
  display: none;
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.5);
  z-index: 1000;
  align-items: center;
  justify-content: center;
  padding: 1rem;
  overflow-y: auto;
}

.modal.active {
  display: flex;
  animation: modal-backdrop-in 160ms ease both;
}

/* Entrance only, deliberately. closeModal() drops .active synchronously —
   several suites assert exactly that on the next line — so there is no state
   left to animate out of, and the alternative (deferring the class removal
   behind an animationend) would trade a real behavioural contract for a
   dismissal nobody watches. */
@keyframes modal-backdrop-in {
  from { opacity: 0; }
}

/* The sheet travels further on phones because that is where it is a bottom
   sheet (see the ≤680px block below) — a sheet that barely moves does not
   read as having come up from the edge. Kept as a var rather than a second
   @keyframes so there is one definition of the motion and one of the
   distance. */
@keyframes modal-sheet-in {
  from { opacity: 0; transform: translateY(var(--sheet-y, 12px)); }
}

.modal.active .modal-content {
  animation: modal-sheet-in var(--dur-view) var(--ease-out) both;
}

.modal-content {
  background: var(--surface-2);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-hover);
  padding: 2rem;
  max-width: 500px;
  width: 100%;
  max-height: 90vh; /* fallback for browsers without dvh support */
  max-height: 90dvh; /* respects mobile browser chrome collapsing/expanding, see css/discover.css's screen shells */
  overflow-y: auto;
  overscroll-behavior: contain; /* stop scroll-chaining from this panel into the locked body behind it */
}

/* Below the app's single-column breakpoint, render every modal as a bottom
   sheet instead of a centered dialog — a centered fixed panel doesn't
   reflow when the soft keyboard opens, so a focused field (or the Save
   button) can end up hidden behind it. Anchoring to the bottom edge keeps
   the panel's top free to ride above the keyboard instead. */
@media (max-width: 680px) {
  .modal {
    align-items: flex-end;
    padding: 0;
  }

  .modal-content {
    max-width: 100%;
    border-radius: var(--radius-md) var(--radius-md) 0 0;
    --sheet-y: 26px;
  }
}

.modal-header {
  display: flex;
  align-items: center;
  gap: var(--space-lg);
  margin-bottom: 1.5rem;
  /* title stays left, close button pushed to the far right */
}

/* Add Expense can run long (receipt scan, itemized split, etc.) — keep the
   close button reachable without scrolling back to the top. Bleeds out to
   the modal's full padded width via negative margins, then re-adds that
   padding, so it reads as a proper header bar rather than a floating button. */
#addExpenseModal .modal-header {
  position: sticky;
  top: -2rem;
  z-index: 5;
  margin: -2rem -2rem 1.5rem;
  padding: 2rem 2rem 1rem;
  background: var(--df-panel);
  border-radius: var(--df-radius-card) var(--df-radius-card) 0 0;
}

.modal-header h2 {
  font-size: var(--fs-xl);
  font-weight: 700;
  flex: 1;          /* grow to fill, pushes close btn to the edge */
}

/* Issue: hardcoded #ffffff/#000000 with !important had no dark-mode
   override anywhere — every modal's close button ignored the theme
   entirely. Tokenized, plus the .stop-menu-btn::before tap-target
   expansion pattern (visible glyph stays 32px, hit region reaches
   --tap-min) since the visual size is a deliberate part of the modal
   header's proportions. */
.modal-close {
  flex-shrink: 0;
  margin-left: auto;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  background: var(--surface-2);
  border: 1.5px solid var(--border-strong);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--fs-lg);
  cursor: pointer;
  color: var(--text-primary);
  transition: background var(--dur-feedback) var(--ease-out), color var(--dur-feedback) var(--ease-out), border-color var(--dur-feedback) var(--ease-out);
  line-height: 1;
  box-shadow: var(--shadow-resting);
  position: relative;
}

.modal-close::before {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: max(var(--tap-min), 100%);
  height: max(var(--tap-min), 100%);
  transform: translate(-50%, -50%);
}

.modal-close i {
  color: var(--text-primary);
}

.modal-close:hover {
  background: var(--vn-coral-light);
  border-color: var(--vn-coral-mid);
  color: var(--vn-coral);
}

.modal-close:hover i {
  color: var(--vn-coral);
}

.form-group {
  margin-bottom: 1rem;
}

.form-label {
  display: block;
  font-size: var(--fs-base);
  font-weight: 500;
  color: var(--text-secondary);
  margin-bottom: var(--space-sm);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

/* An inline aside inside a .form-label ("(optional — ...)") otherwise
   inherits the label's uppercase/tracked treatment — fine for a one-word
   "(optional)" but a full explanatory clause set in tracked caps reads as
   a wall of shouted text and is measurably slower to scan than the same
   words in sentence case. Drops to the Label-tier micro-text size instead
   of the form-label's own size so the aside stays visibly subordinate to
   the label it's attached to, not just a different color. */
.form-hint {
  text-transform: none;
  letter-spacing: normal;
  font-size: var(--fs-xs);
  font-weight: 400;
  color: var(--text-muted);
}

/* Issue #710 — one flex input(s)+button row, extracted from three near-
   identical inline-styled copies: the decision board's "Suggest a
   destination"/"Suggest dates" rows and the AI planner's destination-editor
   add row. Wraps on narrow screens rather than a fixed row of fixed-width
   inputs; a call site with extra spacing needs (e.g. margin-top under a
   list above it) still adds that inline, same as any other utility class. */
.inline-add-row {
  display: flex;
  gap: var(--space-sm);
  flex-wrap: wrap;
}

/* Groups the Add Expense modal's ~19 controls into three labelled sections
   (What was it / How much / Who's splitting it) instead of one undivided
   list — a real <fieldset>/<legend> pair, not just a visual heading, so a
   screen reader announces the grouping too. Reset to blend with the
   existing .form-group/.form-full rhythm rather than a browser-default
   fieldset border-box. */
.exp-form-section {
  border: none;
  margin: 0 0 var(--space-4xl);
  padding: 0;
}
.exp-form-section + .exp-form-section {
  border-top: 0.5px solid var(--border);
  padding-top: var(--space-4xl);
}
.exp-form-section-legend {
  padding: 0;
  margin-bottom: var(--space-lg);
  font-size: var(--fs-sm);
  font-weight: 700;
  color: var(--text-primary);
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

/* ── Create Trip wizard: dates + destination-search UI (issues #382, #419) ──
   .wizard-date-row-line/.wizard-date-labels are Q2a's exact-date pair
   layout (js/auth.js's wizardWhenDatesScreenHtml()); .trip-destination-
   input-wrap/.trip-destination-suggestions* are Q1a's destination-search
   sub-screen (wizardWhereDestinationScreenHtml()) — issue #710 split these
   apart from a single shared block once it turned out the two screens no
   longer actually reuse the same rules, despite the "trip-city-*" name
   they'd both inherited from when they did. */
.wizard-date-row-line {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-sm);
  align-items: center;
}

/* "Start date"/"End date" column headers directly above the date
   row-line — a relative "min" hint alone isn't visible until the picker is
   actually opened. */
.wizard-date-labels {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-sm);
  padding: 0 var(--space-2xs);
  font-family: var(--df-mono);
  font-size: var(--fs-xs);
  font-weight: 700;
  color: var(--df-ink-soft);
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

.trip-destination-input-wrap { position: relative; }

/* Hard offset, zero-blur — the one soft/blurred shadow left in this dropdown
   was a leftover from the retired Field Journal system; DESIGN.md's Hard
   Shadow Rule reserves blur for the instant print alone. */
.trip-destination-suggestions {
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  right: 0;
  background: var(--df-panel);
  border: 2px solid var(--df-ink);
  border-radius: var(--df-radius-stub);
  box-shadow: var(--df-press);
  z-index: 10;
  overflow: hidden;
}

.trip-destination-suggestion {
  padding: var(--space-md) var(--space-lg);
  min-height: var(--tap-min);
  font-size: var(--fs-base);
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: var(--space-sm);
}

.trip-destination-suggestion:hover { background: var(--vn-teal-light); }

.trip-destination-suggestion-region {
  font-size: var(--fs-xs);
  color: var(--df-ink-soft);
}

input[type="text"],
input[type="email"],
input[type="password"],
input[type="date"],
input[type="datetime-local"],
input[type="time"],
input[type="number"],
input[type="tel"],
input[type="file"],
select,
textarea {
  width: 100%;
  height: 40px;
  padding: 0 var(--space-xl);
  font-size: var(--fs-md);
  font-family: inherit;
  border: 0.5px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--text-primary);
  background: var(--surface-1);
  box-sizing: border-box;
}

/* iOS Safari auto-zooms the whole page on focus of any control under 16px,
   then leaves the viewport zoomed after blur — this is the one shared rule
   every form control in the app inherits from (see CLAUDE.md's Date inputs
   note), so it's the one place to fix it rather than a per-form override.
   Issue #483 — height: var(--tap-min) added here too, same reasoning: fixed
   once at the shared rule, never per-control (#15/#446). Scoped to this
   mobile breakpoint only, not the base rule above — --tap-min is a touch-
   target floor, and desktop density (Add Stop's 11 controls, Create Trip's
   confirm step) doesn't need it for a mouse. */
@media (max-width: 680px) {
  input[type="text"],
  input[type="email"],
  input[type="password"],
  input[type="date"],
  input[type="datetime-local"],
  input[type="time"],
  input[type="number"],
  input[type="tel"],
  input[type="file"],
  select,
  textarea {
    font-size: var(--fs-lg);
    height: var(--tap-min);
  }
}

textarea {
  height: auto;
  min-height: 80px;
  padding: var(--space-lg) var(--space-xl);
}

/* A native checkbox/radio glyph is far under --tap-min, and most of this
   app's checkbox rows are a plain <label style="display:flex..."> wrapping
   one — one shared rule rather than a per-modal fix, same reasoning as the
   input-font-size rule above. Only sets min-height: the many inline
   display:flex styles already on these labels keep winning for layout, this
   just guarantees the label itself is a full-height tap target around the
   13px glyph. */
label:has(> input[type="checkbox"]),
label:has(> input[type="radio"]) {
  min-height: var(--tap-min);
}

/* Ghost-filled value inherited from a linked itinerary stop (Place/Description
   in Add Expense) — muted until the user focuses the field, which clears it. */
.field-ghost {
  color: var(--text-muted) !important;
}

input[type="file"] {
  height: auto;
  padding: var(--space-lg) var(--space-xl);
}

input:focus,
select:focus,
textarea:focus {
  outline: none;
  border-color: var(--vn-teal);
  box-shadow: 0 0 0 3px var(--vn-teal-light);
}

/* The keyboard focus ring for everything that isn't a form control.
   Until this existed the stylesheet defined exactly two :focus-visible rules
   (both on .stop-connector, both colour-only), so every button, link and chip
   in the app fell back to the UA default — and any future `outline: none`
   would have removed focus visibility silently. Uses the same Trail Green +
   Trail Green Mist glow DESIGN.md documents for inputs, so the whole system
   has one focus language.
   :focus-visible, not :focus — a mouse click on a button must not leave a
   ring behind, which is why the input rule above is deliberately untouched. */
a:focus-visible,
button:focus-visible,
summary:focus-visible,
[role="button"]:focus-visible,
[tabindex]:not([tabindex="-1"]):focus-visible {
  outline: 2px solid var(--vn-teal);
  outline-offset: 2px;
  box-shadow: 0 0 0 5px var(--vn-teal-light);
  /* Deliberately no border-radius here: `outline` already follows the
     element's own radius, so setting one would square off the pill-shaped
     chips and the segmented view toggle the instant they took focus. */
}

.btn-primary {
  width: 100%;
  padding: var(--space-xl);
  min-height: var(--tap-min);
  background: var(--vn-teal);
  color: var(--surface-2);
  border: none;
  border-radius: var(--radius-sm);
  font-size: var(--fs-md);
  font-weight: 500;
  cursor: pointer;
  font-family: inherit;
  margin-top: 1rem;
}

.btn-primary:hover {
  background: var(--vn-teal-mid);
}

.btn-secondary {
  width: 100%;
  padding: var(--space-lg);
  min-height: var(--tap-min);
  background: var(--surface-1);
  color: var(--text-primary);
  border: 0.5px solid var(--border);
  border-radius: var(--radius-sm);
  font-size: var(--fs-md);
  cursor: pointer;
  font-family: inherit;
}

.btn-secondary:hover {
  background: var(--border);
}

/* Issue #317 follow-up: withButtonGuard() (js/ui.js) disables a one-shot
   creation button for the duration of its request. Without this, a custom-
   colored button's disabled state is visually identical to enabled (the UA
   default dimming gets overridden by the explicit background-color above),
   so a disabled click would silently do nothing with no feedback. */
.btn-primary:disabled,
.btn-secondary:disabled,
.btn-small:disabled {
  opacity: 0.55;
  cursor: default;
}
.btn-primary:disabled:hover {
  background: var(--vn-teal);
}
.btn-secondary:disabled:hover {
  background: var(--surface-1);
}

/* Shared by the Itinerary toolbar and the Hotel Stays action row: on mobile
   the buttons flex:1 to fill the narrow column; on desktop (see the
   Desktop layout override below) they shrink back to their natural width
   instead of stretching into oversized bars now that the column is wide. */
.itin-toolbar {
  display: flex;
  gap: var(--space-md);
  margin-bottom: var(--space-lg);
  flex-wrap: wrap;
}

.itin-action-btn {
  flex: 1;
  min-width: 140px;
}

/* Issue #159 — List/Calendar segmented toggle, revealed alongside "Ask AI to
   Adjust" once stops exist (see renderItinToolbar() in js/itinerary.js). */
.itin-view-toggle {
  display: flex;
  border: 1.5px solid var(--border);
  border-radius: var(--radius-pill);
  overflow: hidden;
  flex-shrink: 0;
}
/* Issue #545 follow-up (design critique) — #itin-view-toggle is this shared
   class's itinerary-tab instance, scoped to the id rather than the class so
   any other .itin-view-toggle instance stays on its own footing. The
   List/Calendar toggle is the most-touched control on the tab, and
   DESIGN.md's Shapes rule reserves hairlines for the retired system.
   (The Create Trip wizard, once the other cited counterexample here, was
   converted to Departure Fever in a later audit along with the rest of its
   modal chrome — see the wizard block and .df-modal-content below.)
   #members-subtab-toggle (js/members.js/index.html) shares the same 2px
   treatment as of the People-tab design critique 2026-08-22 — it's this
   panel's most-used control and had been quietly left on the retired
   1.5px hairline while its itinerary sibling got the real keyline.
   #bookings-view-toggle (Upcoming/All/By kind) was the same gap a third
   time — the Bookings tab's own most-used control, caught in a 2026-08-24
   audit. */
#itin-view-toggle,
#members-subtab-toggle,
#bookings-view-toggle {
  border: 2px solid var(--df-ink);
}
#itin-view-toggle .itin-view-toggle-btn + .itin-view-toggle-btn,
#members-subtab-toggle .itin-view-toggle-btn + .itin-view-toggle-btn,
#bookings-view-toggle .itin-view-toggle-btn + .itin-view-toggle-btn {
  border-left: 2px solid var(--df-ink);
}
.itin-view-toggle-btn {
  display: flex;
  align-items: center;
  /* Centres the label now that #482 gave the control a min-height taller
     than its own line box. */
  justify-content: center;
  gap: var(--space-sm);
  padding: var(--space-md) var(--space-xl);
  min-height: var(--tap-min);
  font-size: var(--fs-sm);
  font-weight: 500;
  font-family: inherit;
  border: none;
  background: var(--surface-2);
  color: var(--text-secondary);
  cursor: pointer;
  transition: background 0.15s, color 0.15s;
}
.itin-view-toggle-btn + .itin-view-toggle-btn {
  border-left: 1.5px solid var(--border);
}
.itin-view-toggle-btn.active {
  background: var(--vn-teal);
  color: var(--surface-2);
}

/* Complete People-tab layout redesign — #itin-view-toggle sits inline in a
   toolbar next to other controls (renderItinToolbar()), so its two buttons
   staying at their natural content width is correct there. #members-subtab-
   toggle is now the lone header of a full-width roster card (its own row,
   nothing beside it) — at that width "Travel Companions" and "Join
   Requests" sizing themselves to their own text left a large blank gap of
   card background inside the pill's right side. Scoped to the id so
   #itin-view-toggle's sizing is untouched. #bookings-view-toggle is the
   same situation a second time — its own full-width row in #panel-bookings,
   not inline in a toolbar — caught alongside the same 2px-keyline audit
   that made the stretch obvious. */
#members-subtab-toggle .itin-view-toggle-btn,
#bookings-view-toggle .itin-view-toggle-btn {
  flex: 1;
}

/* Issue #419 — Create Trip guided wizard v3, converted to Departure Fever in
   a later audit alongside the rest of #createTripModal's/#joinTripModal's
   .df-modal-content chrome (index.html). The palette doesn't change here —
   --vn-teal/-light/-mid already alias the same Trail Green values
   .df-modal-content itself resolves to — only shape (2px df-ink keylines,
   --df-radius-stub), shadow (hard offset --df-press*, no blur) and type
   (--df-mono labels) move. */
.wizard-progress-bar { margin-bottom: 0.75rem; }
.wizard-progress-segments {
  display: flex;
  gap: var(--space-sm);
  margin-bottom: var(--space-xs);
}
/* One segment per spine question — always 5, never more (wizardProgress(),
   js/auth.js) — filled solid once answered, half-filled while sitting on
   one of that question's sub-screens. Tapping an answered segment jumps
   back to it, same effect as Back (wizardJumpToSegment()). */
.wizard-progress-segment {
  position: relative;
  flex: 1;
  padding: 0;
  border: none;
  background: none;
  cursor: pointer;
}
/* A question past the current one isn't jumpable yet (wizardJumpToSegment()
   is a no-op on it), so it renders `disabled` above; this is the only
   affordance difference from an answered segment. */
.wizard-progress-segment:disabled {
  cursor: default;
}
/* Expands the tappable area to --tap-min without inflating the visible
   track — the track itself stays a thin bar (an IG-story-style progress
   read), while the button underneath remains a real touch target, same
   ::before-expansion convention used for every other visually-small
   control in this file. */
.wizard-progress-segment::before {
  content: '';
  position: absolute;
  inset: 50% 0 auto 0;
  height: max(var(--tap-min), 100%);
  transform: translateY(-50%);
}
.wizard-progress-segment-track {
  display: block;
  height: 6px;
  border-radius: var(--radius-pill);
  background: var(--df-hair);
  overflow: hidden;
}
/* Same width→transform swap as .pack-progress-fill above; the fraction
   arrives as --fill from the wizard's own render (js/auth.js). */
.wizard-progress-segment-fill {
  display: block;
  height: 100%;
  width: 100%;
  border-radius: var(--radius-pill);
  background: var(--vn-teal);
  transform: scaleX(var(--fill, 0));
  transform-origin: left center;
  transition: transform var(--dur-state) var(--ease-out);
}
.wizard-progress-segment.active .wizard-progress-segment-fill { background: var(--vn-teal-mid); }
.wizard-progress-label {
  font-family: var(--df-mono);
  font-size: var(--fs-xs);
  font-weight: 700;
  color: var(--df-ink-soft);
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

/* Options before inputs (issue #419 §2) — every chip question renders as a
   vertical stack of full-width tappable rows, never a grid of squares,
   so a long translated label never truncates. */
.wizard-chip-grid {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  margin: var(--space-sm) 0 var(--space-lg);
}
.wizard-chip {
  display: flex;
  align-items: center;
  width: 100%;
  min-height: var(--tap-min);
  padding: var(--space-lg) var(--space-xl);
  font-size: var(--fs-md);
  font-family: inherit;
  text-align: left;
  color: var(--df-ink);
  background: var(--df-panel);
  border: 2px solid var(--df-ink);
  border-radius: var(--df-radius-stub);
  box-shadow: var(--df-press);
  cursor: pointer;
  transition: transform var(--dur-feedback) var(--df-ease), box-shadow var(--dur-feedback) var(--df-ease), background var(--dur-feedback) var(--df-ease), border-color var(--dur-feedback) var(--df-ease), color var(--dur-feedback) var(--df-ease);
}
.wizard-chip:hover { background: var(--vn-teal-light); border-color: var(--vn-teal); transform: translate(2px, 2px); box-shadow: var(--df-press-hover); }
/* Auto-advance on tap (issue #419 §4) means a chip is only ever :active for
   the brief window before its onclick navigates away — this IS the "shows
   the selected state for ~150ms" feedback the PRD calls for, no separate
   timer needed. */
.wizard-chip:active { background: var(--vn-teal-light); border-color: var(--vn-teal); color: var(--vn-teal); transform: translate(4px, 4px); box-shadow: var(--df-press-active); }
/* Q5's name chips are the one exception (js/auth.js's wizardNameScreenHtml())
   — picking one patches state and re-renders in place rather than
   navigating away, so it needs a look that PERSISTS past the tap, not the
   transient :active feedback every auto-advancing chip uses above. Stays at
   the resting --df-press offset (not pressed-in) — selected is a state, not
   a press. */
.wizard-chip.selected {
  background: var(--vn-teal-light);
  border-color: var(--vn-teal);
  color: var(--vn-teal);
  font-weight: 600;
}

.wizard-nights-readout {
  margin-top: var(--space-sm);
  font-size: var(--fs-sm);
  color: var(--df-ink-soft);
  min-height: 1em;
}

/* Recap card (issue #419 §5) — one tappable row per answered question,
   jumping back via wizardJumpToSegment() (renderCreateTripRecapCard(),
   js/auth.js). Replaces the old plain-text review lines. The dashed rule
   between rows borrows the stub's own torn-ticket internal
   rule (DESIGN.md Shapes) — apt for a review card reached right before the
   trip gets "stamped" into existence. */
.wizard-recap-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  width: 100%;
  min-height: var(--tap-min);
  padding: var(--space-md) var(--space-xl);
  font-family: inherit;
  font-size: var(--fs-base);
  text-align: left;
  color: var(--df-ink-soft);
  background: none;
  border: none;
  border-bottom: 2px dashed var(--df-hair);
  cursor: pointer;
  transition: background var(--dur-feedback) var(--ease-out);
}
.wizard-recap-row:last-child { border-bottom: none; }
.wizard-recap-row:hover { background: var(--vn-teal-light); }
.wizard-recap-label {
  font-family: var(--df-mono);
  font-weight: 700;
  color: var(--df-ink-soft);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  flex-shrink: 0;
}
.wizard-recap-value {
  color: var(--df-ink);
  text-align: right;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Back/Continue standardized to identical size/color/font (same .btn-small
   class on both, index.html) — no Skip button any more (issue #419 §7),
   and no primary-vs-secondary visual hierarchy across the row. */
.wizard-nav {
  display: flex;
  gap: var(--space-md);
  margin-bottom: 0.75rem;
}
.wizard-nav button {
  flex: 1;
}

/* The quiet "Create trip now" escape hatch (issue #419 §7) — visible from
   Q2 onward (wizardNavState(), js/auth.js). Deliberately not styled like a
   button: a full-strength CTA here would compete with "Create Trip" itself
   on the last screen. */
.wizard-escape-hatch {
  text-align: center;
  margin-bottom: var(--space-sm);
}
.wizard-quiet-link {
  min-height: var(--tap-min);
  padding: var(--space-sm) var(--space-lg);
  background: none;
  border: none;
  font-family: inherit;
  font-size: var(--fs-sm);
  color: var(--df-ink-soft);
  text-decoration: underline;
  cursor: pointer;
}
.wizard-quiet-link:hover { color: var(--df-ink); }

/* Issue #382 — budget step's currency picker next to the amount input.
   Defaults to the resolved home currency (openCreateTripModal(), js/ui.js)
   but is a real editable <select>, not a read-only badge — Trip.budgetCurrency
   exists specifically so a deliberately different choice (e.g. budgeting a
   Japan trip in JPY with a MYR home currency) has somewhere to be recorded
   instead of silently mismatching later budget-vs-spend comparisons. */
.create-trip-budget-row {
  display: flex;
  gap: var(--space-sm);
  align-items: center;
}
/* 30/70 split — the currency only ever needs to show a 3-letter code
   (below), so it doesn't need anywhere near half the row's width; Amount is
   the field actually being typed into.
   No fixed height here on purpose: the select is a normal in-flow child, so
   the wrap auto-sizes to whatever height the shared input/select rule gives
   it — including the @media(max-width:680px) bump to var(--tap-min). A
   fixed height here previously pinned this one control to 40px and silently
   beat that shared mobile rule, shrinking it under the 44px touch-target
   floor — the exact per-control-override class of bug #483 fixed at the
   shared rule (itself following #15's date-input precedent), just missed
   here because this control's own height was never part of that sweep. The
   badge below still overlays it exactly via inset:0. */
.create-trip-budget-currency-wrap {
  position: relative;
  flex: 0 0 30%;
}
.create-trip-budget-currency-sel {
  width: 100%;
  color: transparent;
}
.create-trip-budget-currency-sel option {
  color: var(--df-ink);
}
.create-trip-budget-currency-badge {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
  font-size: var(--fs-base);
  font-weight: 600;
  color: var(--df-ink);
}
.create-trip-budget-row input {
  flex: 1 1 70%;
  min-width: 0;
}

/* ==================== App Layout ==================== */
/* .app is now just the show/hide shell — .home-topbar (top bar) and .tabs
   (sidebar/bottom bar) both need to span/sit outside the mobile-width
   column, so the column properties that used to live here now live on
   .app-main, the wrapper around the actual panels. Mirrors the
   .home-screen / .home-main split on the My Trips screen. */
.app {
  display: none;
}

/* Departure Fever ground for the trip view — same halftone/wash treatment as
   .home-screen (My Trips), but the trip view has no viewport-height wrapper
   of its own (the whole page scrolls, see .app-main below), so the ground
   paints on .app.active directly rather than on a flex-column shell.
   min-height, not just background: <body>'s own background is still the
   retired --surface-1 grey (shared with unconverted screens, so it stays
   as-is), and .app sizes to its content by default — a short itinerary (or
   one with no stops yet) left that grey visible below the halftone ground,
   a seam breaking the printed-ground illusion on any viewport taller than
   the content. */
.app.active {
  display: block;
  min-height: 100vh;
  min-height: 100dvh;
  background-color: var(--df-ground);
  background-image: var(--df-halftone), var(--df-wash);
  background-size: 6px 6px, auto, auto;
  background-attachment: fixed;
  color: var(--df-ink);
  font-family: var(--df-sans);
}

/* Same page-wide focus ring as .home-screen's — scoped here rather than
   raising the app-wide 2px rule, which still belongs to the surfaces that
   haven't converted yet. */
.app a:focus-visible,
.app button:focus-visible,
.app [tabindex]:not([tabindex="-1"]):focus-visible {
  outline: 3px solid var(--df-brand);
  outline-offset: 3px;
}

/* Departure Fever skins for the shared .btn-primary/.btn-secondary/.btn-small
   controls — scoped to .app rather than editing those base rules directly,
   since they're also used by still-unconverted screens (Discover, and
   several account/social modals reachable from both My Trips and the trip
   view: profile, friends, create/join trip). Editing the base rule would
   silently start converting fragments of screens this PR doesn't own —
   exactly the half-conversion DESIGN.md's whole-screen rule forbids. Same
   pattern as .home-error button's own scoped override on the My Trips
   screen. Color declarations are untouched (already correct --vn-* tokens);
   only shape, keyline and shadow change. */
.app .btn-primary {
  border-radius: var(--radius-pill);
  box-shadow: var(--df-press);
  transition: transform var(--dur-feedback) var(--df-ease), box-shadow var(--dur-feedback) var(--df-ease), background var(--dur-feedback) var(--df-ease);
}
.app .btn-primary:hover {
  transform: translate(2px, 2px);
  box-shadow: var(--df-press-hover);
}
.app .btn-primary:active {
  transform: translate(4px, 4px);
  box-shadow: var(--df-press-active);
}
.app .btn-primary:disabled {
  transform: none;
  box-shadow: var(--df-press);
}

.app .btn-secondary {
  border: 2px solid var(--df-ink);
  border-radius: var(--radius-pill);
  background: var(--df-panel);
  color: var(--df-ink);
  box-shadow: var(--df-press);
  transition: transform var(--dur-feedback) var(--df-ease), box-shadow var(--dur-feedback) var(--df-ease);
}
.app .btn-secondary:hover {
  background: var(--df-panel);
  transform: translate(2px, 2px);
  box-shadow: var(--df-press-hover);
}
.app .btn-secondary:active {
  transform: translate(4px, 4px);
  box-shadow: var(--df-press-active);
}
.app .btn-secondary:disabled {
  transform: none;
  box-shadow: var(--df-press);
}

.app .btn-small {
  border: 2px solid var(--df-ink);
  border-radius: var(--radius-pill);
  background: var(--df-panel);
  color: var(--df-ink);
}
.app .btn-small:hover {
  background: var(--df-hair);
}

/* Departure Fever skin for trip-owned modals — everything from the
   itinerary builder/decision-board/proposals/booking modals down to Add
   Expense/packing/sharing, plus create/join trip (added in a Create Trip
   wizard theming audit). Marked with
   the .df-modal-content class on each modal's .modal-content div
   (index.html) rather than reached via .app descendant selectors, because
   several of these render as DOM siblings of #app, not children of it
   (decisionBoardModal, itinProposalsModal, itin-builder-modal and others sit
   earlier in the document; createTripModal/joinTripModal earlier still).
   Create/join trip were converted together specifically because they sit as
   siblings in index.html and open from the same My Trips screen — doing
   only one would have traded the old half-converted-screen seam for a
   half-converted-modal-pair one. The rest of the account/social/pre-trip
   cluster (profile, friends) deliberately still does NOT carry this class —
   those are shared with My Trips, which this doesn't convert (My Trips
   itself still opens Field-Journal profileModal/friendsModal today without
   that being a violation — same precedent).

   discoverDetailModal used to be the exception in that cluster and carried
   it — retired by #659 in favor of the /discover/:code page (renderDiscoverDetail()
   styled by css/discover.css's "Trip details" block regardless), so that
   exception no longer applies to anything currently in the DOM. */
.df-modal-content {
  background: var(--df-panel);
  border: 2px solid var(--df-ink);
  border-radius: var(--df-radius-card);
  box-shadow: var(--df-press-large);
  font-family: var(--df-sans);
  color: var(--df-ink);
}

.df-modal-content .modal-header h2 {
  font-family: var(--df-display);
  font-weight: 800;
  letter-spacing: -0.02em;
}

.df-modal-content .modal-close {
  background: var(--df-panel);
  border: 2px solid var(--df-ink);
  color: var(--df-ink);
  box-shadow: var(--df-press);
  transition: transform var(--dur-feedback) var(--df-ease), box-shadow var(--dur-feedback) var(--df-ease);
}
.df-modal-content .modal-close i { color: var(--df-ink); }
.df-modal-content .modal-close:hover {
  background: var(--vn-coral-light);
  border-color: var(--vn-coral-mid);
  color: var(--vn-coral);
  transform: translate(2px, 2px);
  box-shadow: var(--df-press-hover);
}
.df-modal-content .modal-close:hover i { color: var(--vn-coral); }

.df-modal-content .form-label {
  font-family: var(--df-mono);
  color: var(--df-ink-soft);
  letter-spacing: 0.08em;
}
.df-modal-content .form-hint { color: var(--df-ink-soft); }

/* :not([type=radio]):not([type=checkbox]) — settingsSheetModal (added
   2026-08-21) is the first df-modal-content consumer to contain radio/
   checkbox inputs (.share-radio-group, .set-toggle-row). Both already carry
   their own on-brand styling (accent-color: var(--vn-teal), i.e. Trail
   Green) and stay native-rendered by design; without this exclusion they'd
   also pick up this rule's border/background/radius, drawing a bordered
   stub box around a small native circle. */
.df-modal-content input:not([type="radio"]):not([type="checkbox"]),
.df-modal-content select,
.df-modal-content textarea {
  border: 2px solid var(--df-ink);
  border-radius: var(--df-radius-stub);
  background: var(--df-panel);
  color: var(--df-ink);
}

.df-modal-content .btn-primary {
  border-radius: var(--radius-pill);
  box-shadow: var(--df-press);
  transition: transform var(--dur-feedback) var(--df-ease), box-shadow var(--dur-feedback) var(--df-ease), background var(--dur-feedback) var(--df-ease);
}
.df-modal-content .btn-primary:hover { transform: translate(2px, 2px); box-shadow: var(--df-press-hover); }
.df-modal-content .btn-primary:active { transform: translate(4px, 4px); box-shadow: var(--df-press-active); }
.df-modal-content .btn-primary:disabled { transform: none; box-shadow: var(--df-press); }

/* confirmAction()'s destructive variant — Alert Red, never Stamp Coral
   (DESIGN.md: coral is branded and must not read as danger). */
.df-modal-content .btn-primary.btn-danger-solid {
  background: var(--error);
  color: #fff;
}
.df-modal-content .btn-primary.btn-danger-solid:hover { background: var(--error); filter: brightness(0.9); }

.df-modal-content .btn-secondary {
  border: 2px solid var(--df-ink);
  border-radius: var(--radius-pill);
  background: var(--df-panel);
  color: var(--df-ink);
  box-shadow: var(--df-press);
  transition: transform var(--dur-feedback) var(--df-ease), box-shadow var(--dur-feedback) var(--df-ease);
}
.df-modal-content .btn-secondary:hover { background: var(--df-panel); transform: translate(2px, 2px); box-shadow: var(--df-press-hover); }
.df-modal-content .btn-secondary:active { transform: translate(4px, 4px); box-shadow: var(--df-press-active); }
.df-modal-content .btn-secondary:disabled { transform: none; box-shadow: var(--df-press); }

.df-modal-content .btn-small {
  border: 2px solid var(--df-ink);
  border-radius: var(--radius-pill);
  background: var(--df-panel);
  color: var(--df-ink);
}
.df-modal-content .btn-small:hover { background: var(--df-hair); }

/* Design canvas gap-close — Add/Edit Stop modal footer: Delete (edit mode
   only, toggled via [hidden] in js/itinerary.js) + Cancel + Save, replacing
   a lone Save button so the form that already knows which stop it's editing
   is also where deleting it lives. */
.stop-modal-footer {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  margin-top: var(--space-lg);
}
.stop-modal-footer .btn-primary { flex: 1; }

/* Icon-only, outlined in Alert Red rather than filled — this sits beside a
   filled Cancel/Save pair and a second solid-red pill would out-shout them
   both; DESIGN.md's coral is brand, not danger, so red is still the only
   correct hue (same reasoning as .btn-danger-solid above). */
.stop-modal-delete-btn {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--tap-min);
  height: var(--tap-min);
  padding: 0;
  border: 2px solid var(--error);
  border-radius: 50%;
  background: var(--df-panel);
  color: var(--error);
  box-shadow: var(--df-press);
  font-size: 16px;
  transition: transform var(--dur-feedback) var(--df-ease), box-shadow var(--dur-feedback) var(--df-ease), background var(--dur-feedback) var(--df-ease);
}
.stop-modal-delete-btn:hover { background: var(--error-light); transform: translate(2px, 2px); box-shadow: var(--df-press-hover); }
.stop-modal-delete-btn:active { transform: translate(4px, 4px); box-shadow: var(--df-press-active); }

@media (max-width: 680px) {
  .df-modal-content {
    border-radius: var(--df-radius-card) var(--df-radius-card) 0 0;
  }
}

.app-main {
  max-width: 680px;
  margin: 0 auto;
  padding: calc(65px + 1rem) 0 calc(80px + env(safe-area-inset-bottom)); /* top clears the fixed .home-topbar, bottom clears the ~25%-larger .tabs bar */
  /* Contains the incoming panel's sideways travel (see .panel-enter below) so
     a tab switch can never flash a horizontal scrollbar. `clip`, not
     `hidden`, is load-bearing: `hidden` would make this a scroll container
     and break #itin-ideas-sidebar's position:sticky at the desktop
     breakpoint, and would force overflow-y to auto as well. Nothing that
     needs to escape this box is nested inside it — the stop "⋯" popover is
     appended to <body> on purpose (see toggleStopMenu(), js/itinerary.js). */
  overflow-x: clip;
}

/* Unlike the My Trips screen (whose topbar never scrolls because
   .home-screen clips to viewport height and only .home-body scrolls inside
   it — see the Home / My Trips Screen section below), the in-trip app has
   no such wrapper: the whole page scrolls, so without this the topbar
   scrolled away with everything else. Scoped to .app (not the base
   .home-topbar rule) so the My Trips screen's copy is untouched. */
.app .home-topbar {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
}

.header-logo {
  width: 36px;
  height: 36px;
  border-radius: var(--df-radius-stub);
  background: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--topbar-accent);
  font-size: var(--fs-xl);
  flex-shrink: 0; /* never let a long trip name squash the logo badge */
}

.header-sub {
  font-size: var(--fs-sm);
  color: var(--topbar-fg-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* --surface-2, not a literal #fff: the badge sits on the pinned Field Green
   top bar, so its ground does not flip with it — a hardcoded white kept an
   amber foreground that lightens after dark, landing the pair at 2.16:1.
   The token follows the theme and puts --vn-amber-text back on a surface it
   was actually darkened for. */
.trip-badge {
  font-size: var(--fs-xs);
  background: var(--surface-2);
  color: var(--vn-amber-text);
  border-radius: var(--radius-pill);
  padding: var(--space-xs) var(--space-lg);
  font-weight: 500;
  border: 2px solid var(--vn-amber-mid);
}

/* ── Destination photo backdrop (Pexels, best-effort — see
   applyDestinationPhoto in js/utils.js). A fixed, full-viewport layer behind
   the entire trip view (nav bar, tabs, and every panel all paint above it —
   z-index:-1 puts it behind everything else in the page's stacking order),
   not squeezed into the nav bar strip (too cramped there for a photo to read
   as anything but noise). Only ever shows through the actual gaps around
   content — the desktop column's side margins, the sliver above/between day
   cards — since every panel/card in front of it has its own opaque
   background. mask-image is the actual "opacity 1 top-right → 0
   bottom-left" fade the image itself carries; the plain opacity transition
   below is a separate, one-time fade-IN once the photo finishes loading.
   pointer-events:none so it can never intercept a click meant for the real
   content sitting in front of it. */
.app-backdrop {
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.4s ease;
}
.app-backdrop.app-backdrop-photo {
  /* Sized to 75% width × 75% height, pinned to the top-right corner, rather
     than covering the full viewport — no-repeat so it doesn't tile into the
     remaining area. */
  background-image: var(--content-photo);
  background-size: 75% 75%;
  background-position: top right;
  background-repeat: no-repeat;
  /* mask-size/repeat must be set explicitly — the default (auto / repeat)
     tiles the gradient in many small repetitions across an element this
     large instead of spanning it once, which averages out to the mask
     looking almost entirely transparent instead of fading smoothly.
     Radial, centered at the top-right corner (same corner the photo itself
     is pinned to). */
  -webkit-mask-image: radial-gradient(circle at top right, #000 0%, transparent 25%);
  mask-image: radial-gradient(circle at top right, #000 0%, transparent 25%);
  -webkit-mask-size: 100% 100%;
  mask-size: 100% 100%;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
}
.app-backdrop.app-backdrop-photo.app-backdrop-photo-visible {
  opacity: 1;
}

.tabs {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  z-index: 40;
  display: flex;
  gap: 0;
  max-width: 680px;
  margin: 0 auto;
  background: var(--df-panel);
  border-top: 2px solid var(--df-ink); /* the keyline alone carries the separation — no ambient shadow in this system */
  padding: var(--space-md) max(var(--space-lg), env(safe-area-inset-left)) calc(var(--space-md) + env(safe-area-inset-bottom)) max(var(--space-lg), env(safe-area-inset-right)); /* ~25% larger than before, easier to see/tap on mobile */
}

/* The active-tab marker, as one element that travels rather than five that
   blink on and off. Which tab you're on and which one you just left are the
   same fact, and only a marker that moves between them says both.
   Positioned from --tab-ind-x/y, set by positionTabIndicator() (js/ui.js)
   from the active button's own offset box; --tab-ind-op stays 0 until that
   has run at least once, so it can never flash at the far left before it
   knows where to sit (and stays invisible under jsdom, which reports every
   offset as 0). Absolutely positioned against .tabs' padding box — .tabs is
   already position:fixed, so it is its own containing block. */
.tabs::after {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  width: 22px;
  height: 3px;
  border-radius: 0 0 3px 3px;
  background: var(--vn-teal);
  opacity: var(--tab-ind-op, 0);
  transform: translateX(var(--tab-ind-x, 0px));
  transition: transform var(--dur-state) var(--ease-out);
  pointer-events: none;
}

/* Set for exactly one frame while the marker is placed without a preceding
   position (first paint, a resize, a tab being hidden/shown by
   applyObserverTabRestrictions()) — it must appear where it belongs, not
   slide there from wherever it happened to be. */
.tabs.tabs-no-anim::after { transition: none; }

/* min-width, not just flex:1. `flex: 1` sets flex-basis:0, so growth is
   shared equally — but a flex item still cannot shrink below its min-content
   width, and with seven tabs the row's min-content sum exceeds a phone
   viewport. Every tab then sits at its own min-content, which made the
   icon-only Settings tab (no label to set a floor) the narrowest at 29px
   while its six labelled siblings kept theirs. The token is the floor for
   all of them; overflow:hidden means an unusually narrow viewport clips a
   label rather than pushing the bar wider than the screen. */
.tab {
  flex: 1;
  min-width: var(--tap-min);
  min-height: var(--tap-min);
  overflow: hidden;
  flex-direction: column;
  padding: var(--space-md) var(--space-sm);
  font-size: var(--fs-base);
  font-weight: 400;
  color: var(--df-ink-soft);
  cursor: pointer;
  border: none;
  background: none;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-xs);
  transition: color 0.15s;
}

.tab.active {
  color: var(--vn-teal);
  font-weight: 500;
}

.tab i {
  font-size: var(--fs-2xl);
  /* The icon carries the tap acknowledgement, so the bar still confirms a
     press on its own if the travelling marker above never gets positioned. */
  transition: transform var(--dur-state) var(--ease-out);
}

.tab.active i {
  transform: translateY(-1px) scale(1.06);
}

.tab-icon-only i {
  font-size: var(--fs-2xl);
}

.tab-icon-only span {
  display: none; /* label only shown in the desktop sidebar layout, see @media below */
}

/* Seven labelled destinations do not fit a phone at --fs-base. Measured
   label vs available tab width: at 390px "Bookings" is 53px in a 53px tab
   (the labels touch), and at 360px it is 53px in a 49px tab (clipped). One
   step down to --fs-sm buys 4px of air at 390 and stops the clipping at
   360 — strictly better at every width in the range, and it is the label
   under an icon rather than a form control, so the 16px iOS-zoom floor
   (which applies to input/select/textarea) is not in play here.
   The real answer is fewer top-level destinations — both iOS and Material
   put the ceiling at five — but that is a product decision, not a fix. */
@media (max-width: 430px) {
  .tab {
    font-size: var(--fs-sm);
  }
}

.panel {
  display: none;
  padding: 1rem;
}

.panel.active {
  display: block;
}

/* Tab switching. The tabs are an ordered row (Itinerary → Expenses → Rating →
   Packing → Settings) and the sidebar at ≥1024px keeps that same order
   vertically, so the incoming panel enters from whichever side it sits on
   relative to the one leaving — the bar's own order is the spatial model, and
   this is the only thing that makes it legible. --panel-dir is set by
   showTab() (js/ui.js): +1 moving down the list, -1 moving back up.
   10px, not a full-width slide: at this distance it reads as "a different
   thing arrived", which is the whole message, without the panel ever
   travelling far enough to feel like a page transition on a tab press. */
@keyframes panel-enter {
  from { opacity: 0; transform: translateX(calc(var(--panel-dir, 1) * 10px)); }
}

.panel.panel-enter {
  animation: panel-enter var(--dur-view) var(--ease-out) both;
}

/* A single row in a list (friend requests, friends list, blocked users,
   search results, trip invitations, shared-trip history) — same shape
   repeated inline across js/friends.js until this was pulled out. */
.list-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--space-md) 0;
  border-bottom: 1px solid var(--border);
}

.toast-msg {
  position: fixed;
  bottom: calc(76px + env(safe-area-inset-bottom)); /* clears the bottom tab bar */
  left: 50%;
  transform: translateX(-50%);
  background: #1a1a1a;
  color: #fff;
  padding: var(--space-lg) var(--space-4xl);
  border-radius: 20px;
  font-size: var(--fs-base);
  font-weight: 500;
  z-index: 9999;
  pointer-events: none;
  /* #609 F1 — this used to live only on .toast-msg.has-action, so every
     plain-message toast (which is every error toast: none of them pass an
     `action`) rendered as one unwrapped, uncapped-width line. A ~80-char
     express-validator message laid out ~508px wide on a 390px viewport,
     bleeding off both edges with the first/last words cut clean off. */
  white-space: normal;
  max-width: min(92vw, 420px);
  /* Rises off the tab bar it sits above rather than materialising in place —
     the toast reports something that just happened down there. .toast-in is
     added by toast() (js/utils.js) alongside the opacity it already set. */
  transform: translateX(-50%) translateY(8px);
  transition: opacity var(--dur-state), transform var(--dur-state) var(--ease-out), background var(--dur-state);
}

/* #609 F3/F4 — the only visual/AA distinction a failure toast gets: passing
   `{ kind: 'error' }` to toast() (js/utils.js) swaps the background and adds
   a role="alert"/aria-live="assertive" pair plus a tappable close button
   (below). Text stays white — var(--error) is already used at this exact
   contrast for .btn-danger-solid, see css/style.css:1398. */
.toast-msg.toast-error {
  background: var(--error);
}

.toast-msg.toast-in {
  transform: translateX(-50%) translateY(0);
}

/* Leaving is not an event worth watching — a dismissal that takes as long as
   the arrival reads as the toast being reluctant to go. */
.toast-msg:not(.toast-in) {
  transition-duration: 140ms;
}

/* Only set while the toast is offering a trailing button — toast()'s
   `action` option (an Undo affordance) or an error's close button (below):
   the base toast stays pointer-events:none so it can never swallow a tap
   aimed at the UI behind it. */
.toast-msg.has-action {
  pointer-events: auto;
  display: flex;
  align-items: center;
  gap: var(--space-xl);
}

.toast-action {
  flex-shrink: 0;
  min-height: var(--tap-min);
  padding: 0 var(--space-lg);
  background: none;
  border: none;
  /* Literal, not --vn-teal: the toast is pinned to a #1a1a1a background in
     BOTH themes, so a theme-following token would resolve to the light-mode
     forest green (#10702A) against near-black. This is the same signal mint
     dark mode uses, for the same contrast reason. */
  color: #54ff82;
  font-family: inherit;
  font-size: var(--fs-base);
  font-weight: 700;
  cursor: pointer;
}

.toast-action:hover { text-decoration: underline; }

/* #609 F3 — the close control an error toast gets that a plain one never
   had ("no dismiss control" — F3). Icon-only, so sized to --tap-min rather
   than reusing .toast-action's text padding. */
.toast-close {
  flex-shrink: 0;
  min-height: var(--tap-min);
  min-width: var(--tap-min);
  display: flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: none;
  color: #fff;
  cursor: pointer;
}

.toast-close i { font-size: var(--fs-lg); }

/* Runtime update detection (issue #191 Phase 3) - same dark pill look as
   .toast-msg, but persistent (no auto-dismiss/opacity transition) and with
   clickable controls, so unlike .toast-msg it needs pointer-events:auto and
   an explicit width cap so long copy wraps instead of overflowing. */
.version-update-banner {
  position: fixed;
  bottom: calc(76px + env(safe-area-inset-bottom)); /* clears the bottom tab bar */
  left: 50%;
  transform: translateX(-50%);
  background: #1a1a1a;
  color: #fff;
  padding: var(--space-lg) var(--space-xl) var(--space-lg) var(--space-3xl);
  border-radius: 20px;
  font-size: var(--fs-base);
  font-weight: 500;
  z-index: 9999;
  display: flex;
  align-items: center;
  gap: var(--space-lg);
  max-width: calc(100vw - 32px);
  box-shadow: 0 4px 16px rgba(0,0,0,0.25);
}
.version-update-refresh {
  background: var(--vn-green-mid);
  color: #fff;
  border: none;
  border-radius: 14px;
  padding: var(--space-sm) var(--space-2xl);
  font-size: var(--fs-sm);
  font-weight: 600;
  cursor: pointer;
  white-space: nowrap;
}
.version-update-dismiss {
  background: none;
  border: none;
  color: #fff;
  opacity: 1;
  font-size: var(--fs-xl);
  line-height: 1;
  cursor: pointer;
  padding: 0 var(--space-2xs);
}
/* Dimmed-until-hover is only a real affordance on a device that can hover —
   on touch, :hover never fires, so gating the dim behind (hover: hover)
   keeps it full-opacity by default there instead of permanently muted. */
@media (hover: hover) {
  .version-update-dismiss {
    opacity: 0.7;
  }
  .version-update-dismiss:hover {
    opacity: 1;
  }
}

/* ---- Desktop layout: sidebar nav instead of a bottom tab bar, and a wider
   main content column instead of a centered 680px mobile-style strip.
   max-width explicitly cleared (not just omitted) since the base .app-main
   rule sets 680px and a media query only overrides properties it actually
   declares — the column fills whatever space is left next to the 220px
   sidebar rather than capping out and leaving a dead strip on wide
   monitors. ---- */
@media (min-width: 1024px) {
  .app-main {
    max-width: none;
    margin: 0 0 0 220px;
    padding: calc(65px + 1.5rem) 2.5rem 1.5rem;
  }

  .tabs {
    position: fixed;
    top: 65px; /* sits below the now-full-width .home-topbar instead of under it */
    left: 0;
    right: auto;
    bottom: 0;
    width: 220px;
    max-width: none;
    margin: 0;
    flex-direction: column;
    align-items: stretch;
    gap: var(--space-xs);
    border-top: none;
    border-right: 2px solid var(--df-ink); /* the keyline alone carries the separation — no ambient shadow in this system */
    padding: var(--space-4xl) var(--space-lg);
  }

  .tab {
    flex: none;
    flex-direction: row;
    justify-content: flex-start;
    gap: var(--space-lg);
    padding: var(--space-lg) var(--space-xl);
    font-size: var(--fs-md);
    border-radius: var(--df-radius-stub);
  }

  .tab:hover {
    background: var(--df-hair);
  }

  /* Issue #680B — pins Settings to the foot of the sidebar, separated from
     the six content tabs above it, rather than reading as a seventh
     destination. #tab-settings is already last in DOM order (index.html), so
     this is CSS-only — margin-top:auto on a column flex item consumes the
     rail's remaining vertical space, no reorder needed (decision 7).
     Deliberately scoped inside this ≥1024px block only: below 1024px .tabs
     is a horizontal bottom bar, where margin-top:auto acts on the CROSS axis
     and would drop the Settings icon out of alignment with its six siblings. */
  #tab-settings {
    margin-top: auto;
    border-top: 1px solid var(--df-hair);
    padding-top: calc(var(--space-lg) + var(--space-md));
  }

  /* Same marker, re-aimed at the axis the bar actually runs on here: a
     vertical bar on the sidebar's outer edge, travelling on Y. Issue #680B —
     --dur-view (280ms), not --dur-state (200ms): the marker can now travel
     the full height of the rail (Packing to the pinned Settings), a
     sidebar-length trip rather than a hop between adjacent tabs.
     prefers-reduced-motion already answers this transition (see the
     .tabs::after rule near the end of this file) so the marker still lands
     correctly with motion off, just without the travel. */
  .tabs::after {
    width: 3px;
    height: 20px;
    border-radius: 0 3px 3px 0;
    transform: translateY(var(--tab-ind-y, 0px));
    transition: transform var(--dur-view) var(--ease-out);
  }

  .tab i,
  .tab-icon-only i {
    font-size: 19px;
  }

  .tab-icon-only span {
    display: inline;
  }

  .toast-msg,
  .version-update-banner {
    bottom: 24px; /* no bottom tab bar to clear at this width */
  }

  /* Itinerary/Hotel action buttons: stop flex-growing to fill the row now
     that the row itself is much wider — shrink to natural content width
     and stay left-aligned instead of stretching into oversized bars. */
  .itin-action-btn {
    flex: 0 1 auto;
    width: auto;
    min-width: 0;
  }
}

/* ==================== Summary Cards ==================== */
.stat-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
  gap: var(--space-lg);
  margin-bottom: 1rem;
}

.stat-card {
  background: var(--df-panel);
  border: 2px solid var(--df-ink);
  border-radius: var(--df-radius-card);
  box-shadow: var(--df-press);
  padding: var(--space-xl) var(--space-3xl);
}

/* Shared Fund's inline selects/amount inputs (renderPotSection() et al) —
   these only need a WIDTH override for their flex-row layout; height, font-
   size, font-family, border and border-radius all come from the shared
   input/select rule (css/style.css:585) same as every other field in the
   app. Before this class existed they duplicated all of the above by hand
   with raw px values (28-36px tall, 11-13px text) — smaller than 16px on
   mobile, which re-triggers the iOS Safari auto-zoom-on-focus bug DESIGN.md
   already had to fix once (issue #15). */
.pot-inline-select,
.pot-inline-input {
  width: auto;
  min-width: 110px;
}

.pot-inline-select-flex {
  flex: 1;
  min-width: 120px;
  /* A member picker holding one name; without a ceiling it stretched to
     whatever the panel column happened to be (730px at a desktop width). */
  max-width: 320px;
}

.pot-inline-input {
  min-width: 90px;
  max-width: 140px;
}

.pot-inline-input-narrow {
  min-width: 80px;
  max-width: 100px;
}

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

.stat-value {
  font-family: var(--df-display);
  font-size: var(--fs-xl);
  font-weight: 800;
  letter-spacing: -0.02em;
  color: var(--df-ink);
  line-height: 1;
}

/* The trip total is the one thing this screen exists to answer, so it is the
   screen's single loud element (DESIGN.md's One Shout Rule) — a role the
   full-bleed green Add Expense slab used to hold by being the largest and
   most saturated object on the page. Every other figure here stays quiet. */
.stat-card-primary .stat-value {
  font-size: var(--fs-3xl);
}

/* A multi-currency trip stacks one figure per currency in the same card —
   they are peers, so none of them takes the primary size. */
.stat-value-stacked {
  font-size: var(--fs-lg);
}
.stat-card-primary .stat-value-stacked {
  font-size: var(--fs-xl);
}
.stat-value-stacked + .stat-value-stacked {
  margin-top: var(--space-xs);
}

.stat-sub {
  font-size: var(--fs-xs);
  color: var(--df-ink-soft);
  margin-top: var(--space-xs);
}

/* Budget tab category itemization (issue #410) — a plain label/value row
   per category, reusing .cat-pill for the label half rather than inventing
   a second category-tagging visual. */
.budget-cat-breakdown { margin-top: var(--space-md); }
.budget-cat-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  padding: var(--space-xs) 0;
  font-size: var(--fs-sm);
}
.budget-cat-row input[type="number"] { width: 100px; }
.budget-cat-unallocated {
  font-weight: 600;
  color: var(--df-ink);
  border-top: 2px solid var(--df-hair);
  margin-top: var(--space-xs);
  padding-top: var(--space-sm);
}

/* Small conclusion line under the Spending by Category bars — absorbs what
   used to be a standalone "Expenses" stat tile. */
.cat-breakdown-count {
  font-size: var(--fs-xs);
  color: var(--df-ink-soft);
  text-align: right;
  margin-top: var(--space-2xs);
}

/* Issue #242 — per-member transport booking status (Transport section). */
.transport-member-status-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
}
.transport-member-status-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
}
.transport-member-status-name {
  font-size: var(--fs-base);
  color: var(--df-ink-soft);
}
.transport-member-status-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  border: 2px solid var(--df-ink);
  background: var(--df-panel);
  color: var(--df-ink-soft);
  border-radius: var(--radius-pill);
  padding: var(--space-2xs) var(--space-lg);
  font-size: var(--fs-sm);
  font-family: inherit;
  cursor: pointer;
}
button.transport-member-status-chip:hover {
  border-color: var(--vn-teal);
}
.transport-member-status-chip.booked {
  border-color: var(--vn-teal);
  color: var(--vn-teal);
  background: var(--vn-teal-light);
  cursor: default;
}

/* ==================== Section Headings ==================== */
.section-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin: 1.25rem 0 0.6rem;
}

.section-head h3 {
  font-family: var(--df-mono);
  font-size: var(--fs-base);
  font-weight: 700;
  color: var(--df-ink-soft);
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

/* Issue: on the Expenses panel, this heading (13px/500/secondary) organizes
   content that's LARGER and DARKER than it — .expense-name is 14px/500/
   primary, so seven section labels in a row (All entries, Summary, Spending
   by category, Per person, Settlement, ...) read as fainter than what they
   introduce, and nothing on the screen claims visual priority. Scoped to
   this panel only — .section-head is a shared component used across the
   app and this isn't a redesign of it, just restoring hierarchy on the one
   screen where it inverted. */
#panel-expenses .section-head h3 {
  font-family: var(--df-display);
  font-size: var(--fs-lg);
  font-weight: 800;
  letter-spacing: -0.02em;
  text-transform: none;
  color: var(--df-ink);
}

.section-sub {
  font-size: var(--fs-xs);
  color: var(--df-ink-soft);
  margin: -0.3rem 0 0.6rem;
  line-height: 1.4;
}

/* ==================== Forms ==================== */
.form-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-lg);
  margin-bottom: var(--space-lg);
}

.form-full {
  margin-bottom: var(--space-lg);
}

/* Consistent input/select height across the expenses panel */
#panel-expenses input:not([type=file]),
#panel-expenses select,
#edit-expense-modal input:not([type=file]),
#edit-expense-modal select {
  height: 40px;
  box-sizing: border-box;
}

/* Issue #630 follow-up — this rule's ID-scoped selectors outrank the shared
   input/select/textarea rule's own @media (max-width: 680px) --tap-min
   override (verified via scripts/verify-mobile.mjs: every input/select
   inside these two containers measured a hard 40px at 390px regardless of
   the shared rule's mobile block), silently defeating the 44px tap floor
   for the entire Expenses panel and Edit Expense modal. Same mobile
   breakpoint, same fix, scoped to match. */
@media (max-width: 680px) {
  #panel-expenses input:not([type=file]),
  #panel-expenses select,
  #edit-expense-modal input:not([type=file]),
  #edit-expense-modal select {
    height: var(--tap-min);
  }
}

#panel-expenses textarea,
#edit-expense-modal textarea {
  height: auto;
  min-height: 60px;
  box-sizing: border-box;
}

/* Currency + amount wrapper */
.exp-amount-wrap {
  display: flex;
  gap: 0;
  align-items: stretch;
}

/* Code-only in both the closed select and the open dropdown list (issue
   #630) — no overlay, nothing to clip, nothing to fall out of sync. The
   full currency name lives in the conversion line below (#myr-convert)
   instead, where a name actually helps. Previously this was a
   color:transparent select with a badge painted on top to fake a
   code-only closed state while showing "CODE (Full Name)" open; Blink
   paints a closed select's text from the selected <option>'s computed
   color rather than the select's own, so the hidden text bled through
   under the badge on every Chromium browser. */
.exp-currency-picker {
  position: relative;
  min-width: 60px;
  flex-shrink: 0;
}

.exp-currency-sel {
  width: 100%;
  height: 100%;
  border-top-right-radius: 0 !important;
  border-bottom-right-radius: 0 !important;
  border-right: none !important;
}

.exp-amount-inp {
  flex: 1;
  min-width: 0;
  border-top-left-radius: 0 !important;
  border-bottom-left-radius: 0 !important;
}

/* Issue #630 follow-up — the "Include tax" checkbox measured a native
   13x13 hit box (well under --tap-min) in scripts/verify-mobile.mjs. The
   wrapping <label> already extends the clickable area in practice, but its
   own row height isn't guaranteed >=44px either, so this uses the same
   ::before-expansion idiom as .stop-menu-btn/.member-menu-btn/.set-info-btn
   — glyph stays its native small size, tappable area doesn't. */
#exp-tax-toggle, #edit-exp-tax-toggle {
  position: relative;
  flex-shrink: 0;
}
#exp-tax-toggle::before, #edit-exp-tax-toggle::before {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: max(var(--tap-min), 100%);
  height: max(var(--tap-min), 100%);
  transform: translate(-50%, -50%);
}

.pax-toggle {
  display: flex;
  gap: var(--space-sm);
  flex-wrap: wrap;
}

/* Issue #630 — the single person-selection primitive across the Add/Edit
   Expense form's Paid by, Equal split and itemized rows (those rows moved
   off .assignee-btn onto this; .assignee-btn itself lives on for
   js/packing.js/js/accommodations.js — see the comment above
   .item-assignees). Full display names are no longer truncated to a first
   word (two members sharing one could never be told apart) — max-width +
   ellipsis here, title="" at the call site carries the full name for
   anyone who needs it. */
.pax-btn {
  min-height: var(--tap-min);
  padding: var(--space-sm) var(--space-xl);
  font-size: var(--fs-sm);
  border: 2px solid var(--df-ink);
  border-radius: var(--radius-pill);
  cursor: pointer;
  background: var(--df-panel);
  color: var(--df-ink-soft);
  font-family: inherit;
  max-width: 160px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  transition: background var(--dur-feedback) var(--ease-out), color var(--dur-feedback) var(--ease-out), border-color var(--dur-feedback) var(--ease-out);
}

.pax-btn.sel {
  background: var(--vn-teal-light);
  border-color: var(--vn-teal);
  color: var(--vn-teal);
}

/* Itemized rows are dense — tighten via gap (.item-assignees above) and
   horizontal padding only, never by dropping under --tap-min again (that
   was the old .assignee-btn's mistake at ~19px tall). */
.pax-btn-compact {
  padding: var(--space-xs) var(--space-md);
  max-width: 120px;
}

/* "All"/"None" for an itemized row's assignees — a text toggle, not a
   third person-chip variant, so it can't be mistaken for a member named
   "All" the way .assignee-btn.all-btn used to read. Visible box stays
   small and dense; ::before expands the real tappable zone to --tap-min,
   same trick as .stop-edit-btn (this pair has enough breathing room above
   its own chip row that the expanded zones don't overlap a neighbor). */
.item-all-none-toggle {
  display: flex;
  gap: var(--space-md);
  margin-bottom: var(--space-xs);
}
.item-all-none-btn {
  position: relative;
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  font-size: var(--fs-xs);
  font-weight: 600;
  color: var(--vn-teal);
  text-decoration: underline;
  cursor: pointer;
}
.item-all-none-btn::before {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: max(var(--tap-min), 100%);
  height: max(var(--tap-min), 100%);
  transform: translate(-50%, -50%);
}


/* ==================== Categories ==================== */
.cat-pill {
  display: inline-flex;
  align-items: center;
  /* Issue #451 — the pill leads the stop row now and carries a label, so it
     must not be the thing that collapses when a long place name competes for
     width; .cat-pill-label truncates instead. */
  flex-shrink: 0;
  gap: var(--space-xs);
  font-size: var(--fs-xs);
  padding: var(--space-2xs) var(--space-md);
  border-radius: var(--radius-pill);
  font-weight: 500;
}

.cat-food {
  background: var(--vn-amber-light);
  color: var(--vn-amber-text);
}

.cat-transport {
  background: var(--vn-sky-light);
  color: var(--vn-sky-text);
}

.cat-shopping {
  background: var(--vn-coral-light);
  color: var(--vn-coral-text);
}

.cat-hotel {
  background: var(--vn-indigo-light);
  color: var(--vn-indigo-text);
}

.cat-activity {
  background: var(--vn-green-light);
  color: var(--vn-green);
}

.cat-other {
  background: var(--df-panel);
  color: var(--df-ink-soft);
}

/* Issue #630 — Add/Edit Expense's category picker, icon chips reusing the
   same Tabler glyph + hue taxonomy as the Expenses subtabs (issue #451) and
   .cat-food/etc above, instead of a native emoji <select>. */
.exp-cat-chip-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
}

.exp-cat-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  min-height: var(--tap-min);
  padding: var(--space-sm) var(--space-lg);
  font-size: var(--fs-sm);
  border: 2px solid var(--df-ink);
  border-radius: var(--radius-pill);
  cursor: pointer;
  background: var(--df-panel);
  color: var(--df-ink-soft);
  font-family: inherit;
  transition: background var(--dur-feedback) var(--ease-out), color var(--dur-feedback) var(--ease-out), border-color var(--dur-feedback) var(--ease-out);
}

.exp-cat-chip.sel {
  border-color: var(--df-ink);
  color: var(--df-ink);
}

.exp-cat-chip.sel.cat-food { background: var(--vn-amber-light); border-color: var(--vn-amber); color: var(--vn-amber-text); }
.exp-cat-chip.sel.cat-transport { background: var(--vn-sky-light); border-color: var(--vn-sky); color: var(--vn-sky-text); }
.exp-cat-chip.sel.cat-shopping { background: var(--vn-coral-light); border-color: var(--vn-coral); color: var(--vn-coral-text); }
.exp-cat-chip.sel.cat-hotel { background: var(--vn-indigo-light); border-color: var(--vn-indigo); color: var(--vn-indigo-text); }
.exp-cat-chip.sel.cat-activity { background: var(--vn-green-light); border-color: var(--vn-green); color: var(--vn-green); }

/* Design canvas gap-close — Add/Edit Stop and Quick Idea's Category/Type
   picker, same icon-chip idiom as .exp-cat-chip-row above but for the
   itinerary's own 7 PLACE_TYPES (js/state.js) rather than the 5 expense
   categories, so it needs its own attraction/other hues .exp-cat-chip never
   had to cover. */
.stop-cat-chip-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
}

.stop-cat-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  min-height: var(--tap-min);
  padding: var(--space-sm) var(--space-lg);
  font-size: var(--fs-sm);
  border: 2px solid var(--df-ink);
  border-radius: var(--radius-pill);
  cursor: pointer;
  font-family: inherit;
  transition: background var(--dur-feedback) var(--ease-out), color var(--dur-feedback) var(--ease-out), border-color var(--dur-feedback) var(--ease-out);
}
/* :not(.sel), not a bare base rule — every chip also carries a bare .cat-food/
   .cat-attraction/etc marker class (needed so .sel.cat-* below can target it),
   and those same classes are also the unscoped ".Categories" pill rules
   (.cat-food { background: var(--vn-amber-light); ... } etc., same file,
   no .cat-pill scope) — same specificity as a bare .stop-cat-chip rule, so an
   UNselected chip would win or lose that tie purely on file order. :not(.sel)
   is one compound selector heavier and beats them outright regardless of
   where either rule sits in the file. */
.stop-cat-chip:not(.sel) {
  background: var(--df-panel);
  color: var(--df-ink-soft);
}

.stop-cat-chip.sel {
  border-color: var(--df-ink);
  color: var(--df-ink);
}

.stop-cat-chip.sel.cat-food { background: var(--vn-amber-light); border-color: var(--vn-amber); color: var(--vn-amber-text); }
.stop-cat-chip.sel.cat-transport { background: var(--vn-sky-light); border-color: var(--vn-sky); color: var(--vn-sky-text); }
.stop-cat-chip.sel.cat-shopping { background: var(--vn-coral-light); border-color: var(--vn-coral); color: var(--vn-coral-text); }
.stop-cat-chip.sel.cat-hotel { background: var(--vn-indigo-light); border-color: var(--vn-indigo); color: var(--vn-indigo-text); }
.stop-cat-chip.sel.cat-activity { background: var(--vn-green-light); border-color: var(--vn-green); color: var(--vn-green); }
.stop-cat-chip.sel.cat-attraction { background: var(--vn-purple-light); border-color: var(--vn-purple); color: var(--vn-purple-text); }
/* "Other" carries no hue in the Category Compass (same rule as the .stop-cat-*
   rail variables, css/style.css .itin-stop-card.stop-cat-other) — selected
   just goes to full ink instead of borrowing one. */
.stop-cat-chip.sel.cat-other { background: var(--df-chip-fill); border-color: var(--df-ink); color: var(--df-ink); }

/* Category breakdown bars (renderCategoryBars()) reuse a category's .cat-*
   class for its Compass hue, same as the .cat-pill badge above it — but a
   bar needs the bold, saturated color, not the pill's light tint
   background, or it reads as washed-out instead of a real fill. */
.cat-bar-row {
  margin-bottom: var(--space-lg);
}
.cat-bar-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-md);
  margin-bottom: var(--space-xs);
}
.cat-bar-value {
  font-size: var(--fs-sm);
  color: var(--df-ink);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.cat-bar-pct {
  color: var(--df-ink-soft);
}
.cat-bar-track {
  height: 5px;
  background: var(--df-hair);
  border-radius: var(--radius-xs);
  overflow: hidden;
}

.cat-bar-fill {
  height: 100%;
  border-radius: var(--radius-xs);
}
.cat-bar-fill.cat-food { background: var(--vn-amber); }
.cat-bar-fill.cat-transport { background: var(--vn-sky); }
.cat-bar-fill.cat-shopping { background: var(--vn-coral); }
.cat-bar-fill.cat-hotel { background: var(--vn-indigo); }
.cat-bar-fill.cat-activity { background: var(--vn-green); }
.cat-bar-fill.cat-other { background: var(--df-ink-soft); }

.cat-attraction {
  background: var(--vn-purple-light);
  color: var(--vn-purple-text);
}

/* ==================== Expenses panel layout ====================
   The panel is three zones — standing (totals + settlement), the ledger
   (filters + entries + shared fund), and the breakdown (by category, per
   person) — laid out as one column on a phone and as a wide ledger plus a
   quiet rail from 1024px.

   What this replaced: one flat column of seven equally-weighted section
   heads, every container stretching to whatever width .app-main happened to
   give it. Above the 1024px breakpoint .app-main deliberately drops its
   680px cap so an itinerary stop row can use the width (see the Desktop
   layout block above .stat-grid) — but nothing on this panel has a second
   axis to spend it on, so an expense row measured 1588px at a 1920px
   viewport to carry a place name at one end and an amount at the other. The
   member's own balance and the settle action sat ~2000px down the page,
   below twelve entry cards, in that same stretched shape. */
.exp-panel {
  max-width: 1180px;
}

/* One spacing ladder, three rungs: zone > block > heading. Before this the
   whole panel sat on a single 1.25rem interval, so seven sections read as
   seven peers with nothing grouping any of them. */
.exp-zone {
  margin-bottom: clamp(40px, 6vw, 64px);
}
.exp-zone:last-child {
  margin-bottom: 0;
}

.exp-block + .exp-block {
  margin-top: var(--space-5xl);
}

/* The shared .section-head carries its own top margin for the flat-column
   case; opening a block, the rung above it already separates it. */
.exp-panel .exp-zone-first-head {
  margin-top: 0;
}

.exp-count {
  font-size: var(--fs-sm);
  color: var(--df-ink-soft);
}

/* Heading, count and the Add control share one row: the count belongs to the
   heading (not to the far edge, which is where .section-head's own
   space-between would throw it), and the button takes the edge. */
.exp-ledger-head {
  justify-content: flex-start;
  gap: var(--space-lg);
  flex-wrap: wrap;
}
.exp-add-btn {
  width: auto;
  margin-top: 0;
  margin-left: auto;
  padding-left: var(--space-4xl);
  padding-right: var(--space-4xl);
  display: inline-flex;
  align-items: center;
  gap: var(--space-sm);
}

/* One column left for the Add button to keep its own line at natural width,
   so it spans instead — the shared .btn-primary's original full-bleed shape,
   kept only where the row genuinely can't hold it. Above this it is a
   control sitting beside a heading, not the loudest object on the screen. */
@media (max-width: 560px) {
  .exp-add-btn {
    flex-basis: 100%;
    margin-left: 0;
    justify-content: center;
  }

  /* Two 174px columns can't hold a trip total — "JPY 152,380.00" at the
     primary size broke across two lines, which is the one figure on this
     screen that must read in a single glance. */
  .exp-zone-standing .stat-grid {
    grid-template-columns: 1fr;
  }
}

.exp-list-more {
  margin-top: var(--space-md);
}

.exp-pot-section:not(:empty) {
  margin-top: var(--space-5xl);
}

@media (min-width: 1024px) {
  /* The long content — the entry ledger, then the breakdown under it — takes
     the wide column; the standing zone becomes a narrow rail that sticks to
     the top of the scroll. Standing is deliberately the SHORT column: it is
     the one thing worth keeping on screen while the ledger is scanned, and a
     rail can only stick if it has room to. (An earlier arrangement stacked
     standing AND breakdown into the rail; the rail then outran the ledger and
     left half a screen of empty ground beside it.) */
  /* .active is in the selector because .panel.active's own `display: block`
     is more specific than a bare .exp-panel — and the panel must stay
     display:none while another tab is showing. */
  .exp-panel.active {
    display: grid;
    grid-template-columns: minmax(0, 1fr) clamp(288px, 24vw, 348px);
    column-gap: var(--space-5xl);
    align-items: start;
  }
  .exp-zone-ledger {
    grid-column: 1;
    grid-row: 1;
  }
  .exp-zone-breakdown {
    /* Spans both columns: the standing rail stops at the end of the ledger
       row, and the breakdown is the one zone with two peer halves to spend
       the full width on. */
    grid-column: 1 / -1;
    grid-row: 2;
    margin-bottom: 0;
    /* Side by side rather than stacked: neither block wants the full column
       (a 5px category bar does not get better at 714px), and the two answer
       the same question from opposite ends — by what, and by whom. */
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    column-gap: var(--space-5xl);
    align-items: start;
  }
  .exp-zone-breakdown .exp-block + .exp-block {
    margin-top: 0;
  }
  .exp-zone-standing {
    grid-column: 2;
    grid-row: 1 / -1;
    position: sticky;
    /* Clears the fixed 65px .app .home-topbar, plus the same breathing room
       .app-main's own padding-top gives the column start. */
    top: calc(65px + var(--space-3xl));
    margin-bottom: 0;
  }
  /* The rail is narrower than .stat-grid's own auto-fit minimum wants to
     admit — two 130px columns fit, but "JPY 152,380.00" at --fs-xl does not
     fit inside one of them. Stacked, the trip total gets the full rail
     width and becomes the screen's one loud element. */
  .exp-zone-standing .stat-grid {
    grid-template-columns: 1fr;
    gap: var(--space-md);
  }
}

/* ==================== Expense List ==================== */
.expense-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
}

.expense-item {
  background: var(--df-panel);
  border: 2px solid var(--df-ink);
  border-radius: var(--df-radius-card);
  box-shadow: var(--df-press);
  overflow: hidden;
  transition: box-shadow var(--dur-feedback) var(--df-ease), transform var(--dur-feedback) var(--df-ease);
  padding: 0;
}

.expense-item:hover {
  box-shadow: var(--df-press-hover);
  transform: translate(2px, 2px);
}

.exp-card-header {
  display: flex;
  align-items: stretch;
}

/* The card's open/close control — was the whole .expense-item <div>'s
   onclick; now a real <button> so it's keyboard-reachable and its
   aria-expanded state is announced. Edit sits as a sibling, never nested
   inside it (see renderExpenses()'s comment). */
.exp-card-toggle {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: var(--space-lg);
  padding: var(--space-lg) var(--space-2xl);
  background: none;
  border: none;
  text-align: left;
  cursor: pointer;
  font: inherit;
  color: inherit;
}

.exp-card-toggle:hover {
  background: var(--df-hair);
}

.exp-header-edit-btn {
  align-self: center;
  flex-shrink: 0;
  margin-right: var(--space-2xl);
}

.exp-card-left {
  flex: 1;
  min-width: 0;
}

.exp-card-right {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  flex-shrink: 0;
  text-align: right;
}

/* Category, pot flag and day sit on one metadata line above the name rather
   than bracketing it top and bottom. Same three fields, two lines instead of
   three, and the day badge is no longer read as a caption belonging to the
   name it sat under. */
.exp-card-meta {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-sm);
  margin-bottom: var(--space-xs);
}

.exp-cat-badge {
  display: inline-block;
}

.exp-day-badge {
  font-size: var(--fs-xs);
  color: var(--df-ink-soft);
  background: var(--df-ground);
  border-radius: var(--radius-xs);
  padding: 1px var(--space-sm);
  display: inline-block;
}

.exp-chevron {
  font-size: var(--fs-md);
  color: var(--df-ink-soft);
  transition: transform var(--dur-state) var(--ease-out);
  flex-shrink: 0;
}

.exp-chevron.rotated {
  transform: rotate(180deg);
}

/* Issue: card expand/collapse was an instant style.display flip — the most-
   used interaction on the screen was the only one with no motion at all,
   and every card below it jumped. A grid-template-rows 0fr<->1fr transition
   animates the height without needing to know the content's real height in
   JS. The inner wrapper carries the visual chrome (border/background/
   padding) and min-height:0 so it can actually collapse to zero instead of
   enforcing its own content height on the 0fr track. */
.exp-card-details {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows var(--dur-view) var(--ease-out);
}

.exp-card-details.open {
  grid-template-rows: 1fr;
}

/* ⚠️ The clipper must carry no padding of its own. `overflow: hidden` clips
   at the PADDING box, not the content box — so with the 0fr track holding
   the content box at zero height, the 8px + 12px of vertical padding this
   rule used to declare stayed as a 20px window that a collapsed card's first
   detail row ("Paid by …") was plainly readable through. Every collapsed
   card on the tab showed a clipped strip of its own hidden content. The
   chrome (keyline, ground, padding) lives on the body wrapper inside. */
.exp-card-details-inner {
  overflow: hidden;
  min-height: 0;
}

.exp-card-details-body {
  border-top: 2px solid var(--df-hair);
  background: var(--df-ground);
  padding: var(--space-md) var(--space-2xl) var(--space-xl);
}

/* Issue #473 PR7a — progressive field disclosure inside a form (the Add
   Transport modal is the first user of this; a fresh, form-scoped class
   rather than reusing .exp-card-details, which is specifically an expense
   card's own display chrome). Same animated grid-template-rows 0fr<->1fr
   collapse as .exp-card-details above — see that block's comment for why. */
.form-more-toggle {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  width: 100%;
  min-height: var(--tap-min);
  background: none;
  border: none;
  padding: var(--space-md) 0;
  margin-bottom: var(--space-sm);
  font: inherit;
  font-weight: 600;
  color: var(--vn-teal);
  cursor: pointer;
  text-align: left;
}
.form-more-chevron {
  transition: transform var(--dur-state) var(--ease-out);
}
.form-more-toggle[aria-expanded="true"] .form-more-chevron {
  transform: rotate(180deg);
}
.form-more-details {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows var(--dur-view) var(--ease-out);
}
.form-more-details.open {
  grid-template-rows: 1fr;
}
.form-more-details-inner {
  overflow: hidden;
  min-height: 0;
}

.exp-detail-row {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  font-size: var(--fs-sm);
  padding: var(--space-sm) 0;
  border-bottom: 2px solid var(--df-hair);
  gap: var(--space-xl);
}

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

.exp-detail-row > span:first-child {
  color: var(--df-ink-soft);
  flex-shrink: 0;
}

.exp-detail-val {
  text-align: right;
  color: var(--df-ink-soft);
}

/* Issue: Delete used to sit in the always-visible card header, 8px from
   Edit, inside a card whose entire body is itself a click target — a
   mis-tap destroyed an expense behind nothing but a native confirm(). Moved
   into the expanded detail region as a clearly labeled (not icon-only)
   action, reachable only once a card is deliberately opened. */
.exp-detail-delete-row {
  justify-content: flex-end;
  border-bottom: none;
  padding-top: var(--space-lg);
}
.exp-delete-link {
  color: var(--vn-coral);
  border-color: var(--vn-coral-mid);
}
.exp-delete-link:hover {
  background: var(--vn-coral-light);
}

/* Category Compass — every category owns the same hue here as its .cat-pill
   badge and its subtabs filter chip below, extended to all six (previously
   only food/transport/shopping had a stripe at all, and transport's used
   the wrong hue — .expense-item.transport keyed off `type`, a 3-bucket
   field that never actually holds 'transport'; the class was dead). */
.expense-item.cmp-food {
  border-left: 4px solid var(--vn-amber);
}

.expense-item.cmp-transport {
  border-left: 4px solid var(--vn-sky);
}

.expense-item.cmp-shopping {
  border-left: 4px solid var(--vn-coral);
}

.expense-item.cmp-hotel {
  border-left: 4px solid var(--vn-indigo);
}

.expense-item.cmp-activity {
  border-left: 4px solid var(--vn-green);
}

.expense-item.cmp-other {
  border-left: 4px solid var(--df-ink-soft);
}

.expense-name {
  font-size: var(--fs-md);
  font-weight: 500;
  color: var(--df-ink);
}

.expense-meta {
  font-size: var(--fs-sm);
  color: var(--df-ink-soft);
  margin-top: var(--space-2xs);
  display: flex;
  align-items: center;
  gap: var(--space-md);
  flex-wrap: wrap;
}

.expense-amount {
  font-size: var(--fs-lg);
  font-weight: 500;
  color: var(--df-ink);
  text-align: right;
}

.expense-per {
  font-size: var(--fs-xs);
  color: var(--df-ink-soft);
  text-align: right;
}

.expense-pax {
  font-size: var(--fs-xs);
  color: var(--df-ink-soft);
}

/* ==================== Balance ==================== */
.balance-card {
  background: var(--df-panel);
  border: 2px solid var(--df-ink);
  border-radius: var(--df-radius-card);
  box-shadow: var(--df-press);
  padding: var(--space-2xl) var(--space-3xl);
  margin-bottom: var(--space-md);
}

.balance-name {
  font-size: var(--fs-base);
  font-weight: 500;
  color: var(--df-ink);
  margin-bottom: var(--space-sm);
}

.top-payer-badge {
  font-size: var(--fs-xs);
  font-weight: 500;
  color: var(--vn-amber-text);
  white-space: nowrap;
}

.balance-figures {
  display: flex;
  gap: var(--space-3xl);
  font-size: var(--fs-sm);
  color: var(--df-ink-soft);
  margin-bottom: var(--space-sm);
}
.balance-figures strong {
  color: var(--df-ink);
  font-weight: 600;
}

.balance-cat-pills {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
}

.settle-row {
  background: var(--df-panel);
  border: 2px solid var(--df-ink);
  border-radius: var(--df-radius-card);
  box-shadow: var(--df-press);
  padding: var(--space-xl) var(--space-2xl);
  margin-bottom: var(--space-md);
}

.settle-row-top {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  font-size: var(--fs-base);
  margin-bottom: var(--space-md);
}

/* Single-column settlement amount + sentence wording — replaces the old
   rigid one-line layout (names + arrow + two currencies) that wrapped
   awkwardly on narrow screens. */
.settle-sentence {
  font-size: var(--fs-base);
  color: var(--df-ink);
  line-height: 1.4;
}
.settle-amount {
  font-family: var(--df-display);
  font-size: var(--fs-2xl);
  font-weight: 800;
  letter-spacing: -0.02em;
  color: var(--df-ink);
  margin: var(--space-2xs) 0 var(--space-md);
}
/* Issue: this used to be a flat, unconditional --vn-coral (the "warning"
   hue) for every row — being owed money read identically to owing it. Now
   colored by the viewer's own direction, matching the .owes-pos/.owes-neg
   convention already used in the Per-person cards above. A third-party row
   (neither .settle-amount-owe nor -owed) stays neutral ink. */
.settle-amount-owe {
  color: var(--vn-coral);
}
.settle-amount-owed {
  color: var(--vn-teal);
}

.settle-currency-toggle {
  display: inline-flex;
  align-items: center;
  gap: var(--space-sm);
  min-height: var(--tap-min);
  background: var(--df-panel);
  border: 2px solid var(--df-ink);
  border-radius: var(--radius-pill);
  padding: var(--space-xs) var(--space-lg);
  font-size: var(--fs-xs);
  font-weight: 500;
  color: var(--df-ink-soft);
  cursor: pointer;
  font-family: inherit;
  transition: background var(--dur-feedback) var(--ease-out), color var(--dur-feedback) var(--ease-out), border-color var(--dur-feedback) var(--ease-out);
}
.settle-currency-toggle:hover {
  background: var(--vn-teal-light);
  border-color: var(--vn-teal);
  color: var(--vn-teal);
}

/* ==================== Packing List ==================== */
.pack-progress-bar {
  height: 6px;
  background: var(--df-panel);
  border-radius: 3px;
  overflow: hidden;
  margin-bottom: var(--space-xs);
}
/* transform, not width. Animating width makes the browser lay the element
   out again on every frame; a scaleX from the left is composited. The fill
   spans the track and JS publishes the fraction as --fill (renderPacking(),
   js/packing.js) instead of writing a width percentage. --dur-state, since
   this is a state change rather than a tap acknowledgement. */
.pack-progress-fill {
  height: 100%;
  width: 100%;
  background: var(--vn-teal);
  border-radius: 3px;
  transform: scaleX(var(--fill, 0));
  transform-origin: left center;
  transition: transform var(--dur-state) var(--ease-out);
}
.pack-progress-label {
  font-size: var(--fs-xs);
  color: var(--df-ink-soft);
}

.pack-item-row {
  background: var(--df-panel);
  border: 2px solid var(--df-ink);
  border-radius: var(--df-radius-card);
  box-shadow: var(--df-press);
  padding: var(--space-lg) var(--space-2xl);
  margin-bottom: var(--space-md);
}
.pack-item-top {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  flex-wrap: wrap;
}
.pack-item-name {
  font-size: var(--fs-base);
  font-weight: 500;
  color: var(--df-ink);
}
.pack-item-chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
  margin-top: var(--space-md);
}
.pack-item-actions {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  margin-left: auto;
}
.pack-assignee-tag {
  font-size: var(--fs-xs);
  color: var(--df-ink-soft);
}

/* Issue #70 — per-item X/Y progress badge and lightweight reassign picker */
.pack-item-progress {
  font-size: var(--fs-xs);
  font-weight: 600;
  color: var(--vn-teal);
  background: var(--df-ground);
  padding: 1px var(--space-sm);
  border-radius: var(--df-radius-stub);
}
.pack-reassign-select {
  /* No own font-size (issue #446) — inherits the shared input/select/
     textarea rule so the @media (max-width: 680px) 16px bump isn't beaten,
     which was re-triggering the iOS Safari auto-zoom-on-focus bug. */
  height: 26px;
  padding: 0 var(--space-xs);
  border: 2px solid var(--df-ink);
  border-radius: var(--df-radius-stub);
  background: var(--df-panel);
  color: var(--df-ink);
}

.pack-suggestion-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  background: var(--df-panel);
  border: 2px dashed var(--df-hair);
  border-radius: var(--df-radius-card);
  padding: var(--space-lg) var(--space-2xl);
  margin-bottom: var(--space-md);
  flex-wrap: wrap;
}
.pack-suggested-by {
  font-size: var(--fs-xs);
  color: var(--df-ink-soft);
  margin-left: var(--space-md);
}

.settle-row-actions {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  flex-wrap: wrap;
}

.settle-action-btn {
  display: inline-flex;
  align-items: center;
  gap: var(--space-sm);
  min-height: var(--tap-min);
  background: var(--vn-teal);
  color: var(--surface-2);
  border: 2px solid var(--df-ink);
  border-radius: var(--radius-pill);
  padding: var(--space-md) var(--space-xl);
  font-size: var(--fs-sm);
  font-weight: 600;
  cursor: pointer;
  font-family: inherit;
  box-shadow: var(--df-press);
  transition: transform var(--dur-feedback) var(--df-ease), box-shadow var(--dur-feedback) var(--df-ease);
}
.settle-action-btn:hover { transform: translate(2px, 2px); box-shadow: var(--df-press-hover); }
.settle-action-btn:active { transform: translate(4px, 4px); box-shadow: var(--df-press-active); }

.settle-confirm-btn {
  background: var(--vn-teal-mid);
}

.settle-link-btn {
  background: none;
  border: 2px solid var(--df-ink);
  color: var(--df-ink-soft);
  border-radius: var(--radius-pill);
  padding: var(--space-sm) var(--space-xl);
  min-height: var(--tap-min);
  font-size: var(--fs-sm);
  cursor: pointer;
  font-family: inherit;
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
}

.settle-receipt-link {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  font-size: var(--fs-sm);
  color: var(--vn-teal);
  text-decoration: none;
}

.settle-status {
  font-size: var(--fs-sm);
  font-weight: 600;
  padding: var(--space-xs) var(--space-lg);
  border-radius: var(--radius-pill);
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
}

.settle-status-pending {
  background: var(--df-panel);
  /* Was --text-muted (4.9:1 on --surface-1) — technically AA but the
     lowest-contrast text on the screen, on a status indicator that should
     read at a glance; --text-secondary gives real headroom. */
  color: var(--df-ink-soft);
}

.settle-status-verifying {
  background: var(--vn-amber-light);
  color: var(--vn-amber-text);
}

.settle-status-confirmed {
  background: var(--vn-teal-light);
  color: var(--vn-teal);
}

.settle-status-note {
  font-size: var(--fs-sm);
  color: var(--df-ink-soft);
}

.settle-history {
  font-size: var(--fs-xs);
  color: var(--df-ink-soft);
  margin-top: var(--space-sm);
  padding-top: var(--space-sm);
  border-top: 2px dashed var(--df-hair);
}

/* .balance-bar-bg / .balance-bar-fill were removed here: nothing in js/,
   index.html or admin.html has referenced either class for the whole of this
   repo's history, so the rules only cost bytes. They also carried the third
   of the three layout-property transitions the detector flags. */

.balance-row {
  display: flex;
  justify-content: space-between;
  font-size: var(--fs-sm);
  color: var(--df-ink-soft);
}

.owes-badge {
  font-size: var(--fs-sm);
  padding: var(--space-xs) var(--space-lg);
  border-radius: var(--radius-pill);
  font-weight: 500;
}

.owes-pos {
  background: var(--vn-teal-light);
  color: var(--vn-teal);
}

.owes-neg {
  background: var(--vn-coral-light);
  color: var(--vn-coral);
}

.owes-zero {
  background: var(--df-panel);
  color: var(--df-ink-soft);
}

/* ==================== Settings Tab ==================== */
/* .settings-section / its h4 outlive the Settings panel's own restructure:
   the profile modal's account danger zone (index.html) still uses them, and
   that modal is a different, still-unconverted surface. The panel's own
   layout is the "Settings panel layout" block below. */
.settings-section {
  margin-bottom: 1.5rem;
}

.settings-section h4 {
  font-family: var(--df-mono);
  font-size: var(--fs-base);
  font-weight: 700;
  color: var(--df-ink-soft);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  margin-bottom: 0.75rem;
}

/* ==================== Settings panel layout ====================
   Issue #680 split the panel into sub-tabs — General, AI, Currency &
   Expenses, Sharing, Data (AI split out of General as a #680 follow-up) —
   with only one visible at a time (showSettingsSubtab(), js/settings.js),
   instead of the four zones that
   used to render as one long stacked scroll (see docs/architecture-notes.md
   for the full history). The stub (this trip's own identity + code) now
   renders inline at the top of the General sub-panel; the two-column
   "settings column beside a sticky stub rail" layout that used to live here
   at 1024px+ retired along with it — a rail can't also be one of four
   sub-tabs.

   Before the zones (which this in turn replaced): one flat column of three
   group titles over eleven equally-weighted sections, every container
   stretching to whatever width .app-main happened to give it. Above 1024px
   .app-main deliberately drops its 680px cap so an itinerary stop row can
   use the width (see the Desktop layout block above .stat-grid) — but
   nothing on this panel had a second axis to spend it on, so a three-option
   radio group measured 1140px at a 1440px viewport and 1620px at 1920px.
   The trip code, the one value here anybody ever transcribes to another
   person, was the fourth unstyled line of the first card; Delete Trip, the
   most destructive control in the app, was the largest and most saturated
   object on the screen at a full-bleed 1140px. */
.set-panel {
  max-width: 1180px;
}

/* Sub-panels share the sub-tab bar's own bottom margin for the gap above
   them; only one is ever visible (display:none on the rest), so there is no
   sibling-to-sibling zone spacing to manage the way .set-zone used to. */
#settings-subtabs {
  margin-bottom: var(--space-3xl);
}

/* Blocks pair up wherever two fit. auto-fit rather than a fixed column count
   because this same grid runs at four very different widths — the full phone
   column, the 680px cap, the ~740px column left beside the stub at 1440px,
   and ~1220px at 1920px — and the answer at each is just "as many 280px
   columns as fit". */
.set-blocks {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: var(--space-5xl);
  align-items: start;
}

/* A list of people, or a row of per-member actions, is the one shape here
   that genuinely wants the whole column. */
.set-block-wide {
  grid-column: 1 / -1;
}

/* Quiet, because it labels rather than announces — the tier above it does
   the announcing. Matches .settings-section-title exactly, which is what the
   shared join-approval and visibility controls emit as their own heading
   (js/members.js, js/settings.js), so a block heading reads the same however
   it was authored. */
.set-block-title,
.set-block .settings-section-title {
  font-family: var(--df-mono);
  font-size: var(--fs-sm);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--df-ink-soft);
  margin: 0 0 var(--space-sm);
}

/* Marks the seam between the People tab's roster/permissions zone and its
   Ratings zone (design critique 2026-08-22 — layout: the two used to meet
   as a heading with more top margin, which read as one panel accidentally
   bordering another rather than a considered move to a different
   register). --df-hair, not the loud 2px ink keyline every card already
   carries — a full band border here would compete with the member cards
   sitting right above it. */
.set-panel-divider {
  border: none;
  border-top: 2px solid var(--df-hair);
  margin: var(--space-5xl) 0 0;
}

.set-block-hint {
  font-size: var(--fs-base);
  line-height: 1.5;
  color: var(--df-ink-soft);
  margin: 0 0 var(--space-xl);
  max-width: 58ch;
}

/* Martian Mono at label tier — the ticket-printing voice (DESIGN.md), used
   here for the stub's field names and nothing else. */
.set-label {
  display: block;
  font-family: var(--df-mono);
  font-size: var(--fs-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--df-ink-soft);
  margin-bottom: var(--space-sm);
}

/* A control and the value it resolves to, on one line — the home-currency
   picker and the code it currently means. */
.set-field-row {
  display: flex;
  align-items: center;
  gap: var(--space-lg);
  flex-wrap: wrap;
}
.set-field-row select,
.set-field-row input {
  flex: 1 1 180px;
  min-width: 0;
}
.set-field-suffix {
  font-family: var(--df-mono);
  font-size: var(--fs-base);
  font-weight: 700;
  letter-spacing: 0.06em;
  color: var(--df-ink);
  white-space: nowrap;
}

/* Actions sit at their natural width. Every button on this panel used to
   carry an inline width:100%, which at 1140px turned a two-word label into a
   bar — and made the destructive ones the loudest things on the screen. */
.set-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-lg);
  margin-top: var(--space-md);
}
.set-actions .btn-secondary,
.set-actions .btn-primary {
  width: auto;
  margin-top: 0;
  padding-left: var(--space-4xl);
  padding-right: var(--space-4xl);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-sm);
}

/* One column narrow enough that two labels can't share a row: they span
   instead, which is the shared .btn-secondary's original full-bleed shape,
   kept only where the row genuinely cannot hold them side by side. */
@media (max-width: 480px) {
  .set-actions .btn-secondary,
  .set-actions .btn-primary {
    flex: 1 1 100%;
  }
}

/* ── Destinations badge list (issue #610) ─────────────────────────
   A row of chips instead of a single value, since tripData.destinations is
   the normal multi-city case, not an edge case. Wraps rather than scrolls —
   a five-city trip has to fit at 390px with no horizontal page scroll
   (npm run verify-mobile is what actually catches that; jsdom has no
   layout engine). */
.set-dest-badges {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
  margin-bottom: var(--space-lg);
}

.set-dest-empty {
  font-size: var(--fs-base);
  color: var(--df-ink-soft);
  margin: 0 0 var(--space-lg);
}

.set-dest-chip {
  display: inline-flex;
  align-items: center;
  /* --space-md, not the tighter --space-sm most chips use: three
     ::before-expanded icon buttons sit side by side below, and their
     horizontal hit-zones need this much real gap between glyphs or two
     buttons touch and the topmost-in-DOM one (paint order, no z-index)
     silently steals clicks meant for its neighbor — caught by a click on
     "move earlier" landing on the disabled "move later" button instead. */
  gap: var(--space-md);
  padding: var(--space-xs) var(--space-sm) var(--space-xs) var(--space-xs);
  background: var(--df-hair);
  border-radius: var(--radius-pill);
  font-size: var(--fs-sm);
  max-width: 100%;
}

.set-dest-chip-num {
  flex-shrink: 0;
  width: 22px;
  height: 22px;
  border-radius: var(--radius-pill);
  background: var(--vn-teal-light);
  color: var(--vn-teal);
  font-size: var(--fs-xs);
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: center;
}

.set-dest-chip-label {
  padding: 0 var(--space-xs);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Visually a small in-chip glyph, but the hit region still reaches
   --tap-min via the same ::before-expansion convention as .modal-close. */
.set-dest-chip-btn {
  flex-shrink: 0;
  position: relative;
  width: 20px;
  height: 20px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: none;
  color: var(--df-ink-soft);
  cursor: pointer;
  font-family: inherit;
  font-size: var(--fs-xs);
}

/* Full --tap-min vertically (nothing above/below competes for it), but
   only 28px horizontally: three of these sit in a row with just
   --space-md between glyphs, and a full 44px width here would overlap
   the neighboring button's own zone — see the .set-dest-chip gap comment.
   28px + the --space-md gap on each side meet exactly at the midpoint
   between two button centers, so a click still resolves to whichever
   button it's actually nearer. */
.set-dest-chip-btn::before {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: max(28px, 100%);
  height: max(var(--tap-min), 100%);
  transform: translate(-50%, -50%);
}

.set-dest-chip-btn:hover:not(:disabled) {
  color: var(--df-ink);
}

.set-dest-chip-btn:disabled {
  opacity: 0.3;
  cursor: not-allowed;
}

.set-dest-chip-remove:hover:not(:disabled) {
  color: var(--vn-coral-text);
}

.set-dest-add {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  flex-wrap: wrap;
}
.set-dest-add input,
.set-dest-add select {
  flex: 1 1 160px;
  min-width: 0;
}
.set-dest-add .btn-small {
  flex-shrink: 0;
  width: auto;
  margin-top: 0;
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
}

/* ── The stub ──────────────────────────────────────────────────
   The trip's own ticket. Everything above the tear says what the trip is;
   everything below it is the half you hand to someone else. It is the one
   object on this panel that lifts off the ground (Press Large), because it
   is a thing rather than a panel — every other block is drawn straight onto
   the page and separated by rhythm, not by a container. */
.set-stub {
  background: var(--df-panel);
  border: 2px solid var(--df-ink);
  border-radius: var(--df-radius-stub);
  box-shadow: var(--df-press-large);
  margin-bottom: clamp(40px, 6vw, 64px);
  overflow: hidden;
}

.set-stub-body {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: var(--space-4xl);
  padding: var(--space-4xl) var(--space-4xl) var(--space-3xl);
}

/* Issue #636 — the destinations field carries an add-row (text input +
   country select + button), not just a value, so it can't share a ~190px
   auto-fit column with Trip name the way the plain value fields do; it
   always takes the stub's full width. No-op once .set-stub-body collapses
   to a single 1fr column at 1024px+ (see below). */
.set-stub-field-wide {
  grid-column: 1 / -1;
}

.set-stub-value {
  font-size: var(--fs-md);
  font-weight: 500;
  color: var(--df-ink);
  line-height: 1.35;
}

/* The perforation. 2px dashed is the stub's own rule in this system — the
   line a ticket is torn along — and it is the only dashed edge on the panel
   for exactly that reason. */
.set-stub-tear {
  border-top: 2px dashed var(--df-ink);
  padding: var(--space-3xl) var(--space-4xl) var(--space-4xl);
}

/* The screen's one shout. Nine characters of Martian Mono at display scale:
   the only value on this panel that gets read aloud or typed into another
   device, and the whole reason most people open Settings at all. It is also
   the copy control — see index.html on why the value is the button. */
.set-stub-code {
  display: flex;
  align-items: center;
  gap: var(--space-lg);
  width: 100%;
  min-height: var(--tap-min);
  padding: var(--space-md) var(--space-lg);
  margin: 0 0 var(--space-4xl) calc(var(--space-lg) * -1);
  background: none;
  border: 2px solid transparent;
  border-radius: var(--df-radius-stub);
  cursor: pointer;
  font-family: inherit;
  text-align: left;
  transition: background var(--dur-feedback) var(--df-ease), border-color var(--dur-feedback) var(--df-ease);
}
.set-stub-code:hover {
  background: var(--df-chip-fill);
  border-color: var(--df-ink);
}

.set-stub-code-value {
  font-family: var(--df-mono);
  font-size: var(--fs-3xl);
  font-weight: 700;
  letter-spacing: 0.06em;
  line-height: 1;
  color: var(--df-ink);
  overflow-wrap: anywhere;
}

.set-stub-code-icon {
  font-size: var(--fs-xl);
  color: var(--df-ink-soft);
  margin-left: auto;
  flex-shrink: 0;
}
.set-stub-code:hover .set-stub-code-icon,
.set-stub-code.set-stub-code-copied .set-stub-code-icon {
  color: var(--vn-teal);
}

.set-stub-invite {
  width: 100%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-sm);
}

/* ── Danger ────────────────────────────────────────────────────
   Fenced rather than enlarged. Alert Red on the keyline and the heading —
   never Stamp Coral, which is branded and must not read as danger
   (DESIGN.md); these two buttons were --vn-coral before, one of them a
   full-bleed coral slab. */
.set-danger {
  border: 2px solid var(--error);
  border-radius: var(--df-radius-card);
  background: var(--df-panel);
  padding: var(--space-4xl);
}
.set-danger-title {
  color: var(--error-text);
}
.set-danger-notice {
  font-size: var(--fs-sm);
  color: var(--df-ink-soft);
  border: 2px solid var(--df-hair);
  border-radius: var(--df-radius-stub);
  padding: var(--space-lg) var(--space-xl);
  margin-bottom: var(--space-xl);
}
/* --error-text, not --error: this is a label, and --error itself measures
   ~3.98:1 as text. The keyline keeps --error, which clears the 3:1
   non-text floor. */
.set-danger-btn {
  color: var(--error-text);
  border-color: var(--error);
}

@media (prefers-reduced-motion: reduce) {
  .set-stub-code {
    transition: none;
  }
}

/* Fields on this panel take the converted skin — 2px ink keyline, stub
   radius, Cut Stock fill — the same way .df-modal-content skins the fields
   inside a converted modal. The shared base rule is still the retired
   system's 0.5px hairline and is left alone: it's what every unconverted
   screen still renders with, and editing it would half-convert all of them
   (DESIGN.md's whole-screen rule). Height is NOT set here — the shared rule
   owns it, including the 44px mobile floor. */
.set-panel input:not([type=checkbox]):not([type=radio]),
.set-panel select,
.set-panel textarea {
  border: 2px solid var(--df-ink);
  border-radius: var(--df-radius-stub);
  background: var(--df-panel);
  color: var(--df-ink);
}

.set-field-prefix {
  font-size: var(--fs-base);
  color: var(--df-ink);
  white-space: nowrap;
}

/* A note that follows the control it describes, rather than introducing it. */
.set-block-note {
  font-size: var(--fs-xs);
  color: var(--df-ink-soft);
  margin: var(--space-md) 0 0;
}

/* Complete People-tab layout redesign (design canvas, 2026-08-23/24) —
   the roster and Access & Permissions used to stack as two full-width
   .set-block-wide blocks, leaving the desktop ~1180px panel mostly empty
   air beside a list of member cards. A viewport media query, not
   #member-budget-list's own container-query pattern above — that pattern
   needs a wrapper distinct from the element being queried (a container
   can't condition its own layout on its own inline size, only a
   descendant's), and there's no such wrapper here worth adding. Matching
   the ≥1024px sidebar breakpoint exactly (not an earlier value) is what
   keeps this correct: app-main is capped at 680px below that breakpoint
   regardless of window width, so this grid never actually reaches 700px+
   until the same moment the sidebar appears and the panel gets its real
   1180px ceiling — a lower breakpoint would flip the columns while the
   panel itself was still the narrow mobile column. */
.people-top-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-5xl);
  align-items: start;
}
@media (min-width: 1024px) {
  .people-top-grid {
    grid-template-columns: 1fr 320px;
  }
}

/* Quiet counts under the panel title — "6 companions · 2 join requests" —
   so the roster's size is legible before scanning it. Set by
   renderMemberList() (js/members.js). */
.people-stat-line {
  font-family: var(--df-mono);
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--df-ink-soft);
  margin: var(--space-lg) 0 var(--space-3xl);
}

/* Design critique 2026-08-23 (layout) — the People tab's Access &
   Permissions block was the one un-carded thing on a panel that otherwise
   cards everything (the roster rows above, the trophy tiles and rating
   cards below): a bare heading and two flush-left rows floating on the
   ground, reading as unfinished rather than as a considered flat moment.
   .set-block itself stays bare everywhere else (Settings' own sub-tabs
   rely on that flat look — see .set-summary-row's own comment), so this is
   its own class rather than the shared one — shared now by the roster
   wrapper beside it too, so the pair reads as two cards of equal weight
   rather than a card next to a bare list. Same recipe as
   .member-item/.rating-card/.trophy-card on this same panel: df-panel
   fill, 2px ink keyline, card radius, Press shadow. */
.people-card {
  background: var(--df-panel);
  border: 2px solid var(--df-ink);
  border-radius: var(--df-radius-card);
  box-shadow: var(--df-press);
  padding: 0.9rem 1rem;
}

/* The People tab's own two headings ("People", "Ratings (Be Honest! 😏)")
   were both riding .section-head h3's quiet default (13px mono uppercase,
   a label voice, not a heading one) — the same inversion #panel-expenses
   was fixed for below. Elevated to match, now that the panel underneath
   them carries real card weight. */
#panel-people .section-head h3 {
  font-family: var(--df-display);
  font-size: var(--fs-2xl);
  font-weight: 800;
  letter-spacing: -0.02em;
  text-transform: none;
  color: var(--df-ink);
}

/* ── Access & Permissions summary rows (issue #642 P2) ──────────────────
   Setting name · current value · chevron. A non-editable row is a plain
   div (no button semantics, nothing to activate); an editable one is a
   real <button> spanning the full row — native keyboard/Enter/Space
   activation and a single --tap-min-sized target, not a separate icon. */
.set-summary-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-lg);
  width: 100%;
  min-height: var(--tap-min);
  padding: var(--space-lg) 0;
  border-bottom: 2px solid var(--df-hair);
  font-family: inherit;
  font-size: var(--fs-base);
  color: var(--df-ink);
  background: none;
  border-left: none;
  border-right: none;
  border-top: none;
  text-align: left;
}
.set-summary-row:last-child {
  border-bottom: none;
  padding-bottom: 0;
}
.set-summary-row-editable {
  cursor: pointer;
}
.set-summary-row-editable:hover {
  color: var(--vn-teal);
}
.set-summary-row-label {
  font-weight: 600;
  flex-shrink: 0;
}
.set-summary-row-value {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  color: var(--df-ink-soft);
  font-size: var(--fs-sm);
  text-align: right;
  overflow-wrap: anywhere;
}
.set-summary-row-value i {
  font-size: var(--fs-md);
  flex-shrink: 0;
}

/* A second thought inside a block — the Discover toggle under trip
   visibility. A dashed rule rather than a solid one: it divides within an
   idea, where the zone rules divide between them. */
.set-subblock {
  margin-top: var(--space-4xl);
  padding-top: var(--space-4xl);
  border-top: 2px dashed var(--df-hair);
}

.set-toggle-row {
  display: flex;
  align-items: center;
  gap: var(--space-lg);
}
.set-toggle-row input[type=checkbox] {
  width: 16px;
  height: 16px;
  flex-shrink: 0;
  accent-color: var(--vn-teal);
}

/* ── Per-member budget allocation (issue #104, relocated by #642 P1) ──
   Who / what they're allocated. One row per member, two cells, wrapping to a
   stack when the column can't hold them side by side. Role/removal actions
   used to live in a third cell here too, before #642 P1 folded them into
   each Travel Companions row's own "⋯" menu instead (js/members.js). */
/* A container query, not a viewport one: this block renders at four very
   different widths (the phone column, the 680px cap, the ~700px column left
   beside the stub, and the full 1180px), and what decides whether the two
   cells fit on one line is the block's own width, not the window's. */
.set-block-wide {
  container-type: inline-size;
}

.set-role-row {
  display: grid;
  grid-template-columns: 1fr;
  align-items: center;
  gap: var(--space-lg);
  padding: var(--space-xl) 0;
  border-bottom: 2px solid var(--df-hair);
}
.set-role-row:last-child {
  border-bottom: none;
  padding-bottom: 0;
}

/* The two columns are shared across every row via subgrid rather than
   recomputed per row — a per-row `auto` sized each member's budget cell to
   that member's own input, so the field landed at a different x on every
   line and the list stopped reading as columns. Where subgrid is
   unsupported the rows keep the single-column stack above, which is the
   phone layout and still correct. */
@container (min-width: 620px) {
  #member-budget-list {
    display: grid;
    grid-template-columns: minmax(200px, 1fr) auto;
  }
  .set-role-row {
    grid-column: 1 / -1;
    grid-template-columns: subgrid;
  }
}

.set-role-who {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-sm);
  min-width: 0;
}
.set-role-name {
  font-size: var(--fs-md);
  font-weight: 600;
  color: var(--df-ink);
  overflow-wrap: anywhere;
}

.set-role-budget {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  flex-shrink: 0;
}
.set-role-budget input {
  width: 88px;
}

/* Pending join requests sit under the companions list they belong to, in
   their own stable container (#pending-requests) — see js/members.js on why
   that container has to exist rather than being appended into the list. */
.set-panel .pending-section {
  margin-top: var(--space-4xl);
}

.member-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
}

.member-item {
  background: var(--df-panel);
  border: 2px solid var(--df-ink);
  border-radius: var(--df-radius-card);
  box-shadow: var(--df-press);
  padding: var(--space-xl);
  display: flex;
  align-items: center;
  justify-content: space-between;
}

/* Issue #628 §A2 — a join-request-status row shown in Travel Companions
   reads as not-yet-real, distinct from a genuine member row. */
.member-item-pending {
  opacity: 0.6;
}

.member-name {
  font-size: var(--fs-base);
  font-weight: 500;
  color: var(--df-ink);
}

/* min-width: 0 lets this flex child actually shrink below its content size —
   without it a long nickname/username (e.g. a real display name with no
   spaces) pushes the row wider instead of wrapping, overflowing the card and
   crowding out the "⋯"/add-friend buttons at narrow widths. overflow-wrap
   below is the same fix .set-role-name already uses for this exact problem
   on the Budget tab's member rows — wrap the text, never hide it. */
.member-info { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.member-nick { font-size: var(--fs-md); font-weight: 600; color: var(--df-ink); overflow-wrap: anywhere; }
.member-username { font-size: var(--fs-xs); color: var(--df-ink-soft); overflow-wrap: anywhere; }
/* Complete People-tab layout redesign — was plain colored text; a pill
   (DESIGN.md's kicker-pill: outline only, transparent fill, Martian Mono
   label, currentColor keyline) reads as a real role badge instead of a
   caption. align-self: flex-start keeps it from stretching to the column's
   width under .member-info's flex-column default. */
.member-role {
  display: inline-flex;
  align-items: center;
  align-self: flex-start;
  margin-top: 1px;
  font-family: var(--df-mono);
  font-size: var(--fs-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  border: 1.6px solid currentColor;
  border-radius: var(--radius-pill);
  padding: 1px 8px;
}

.member-you-tag {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2xs);
  font-size: var(--fs-xs);
  font-weight: 600;
  color: var(--vn-teal);
  vertical-align: middle;
}

/* ── Approval stamp (design critique 2026-08-22 — delight) ───────────────
   The People tab had zero Departure Fever signature objects — every other
   accent on it is keylines/shadows/pills, the abstract half of the system
   only. Approving a join request is this app's own arrival-stamp moment
   (the same material the landing hero's .lp-stampword and the /discover
   stub's .dtl-stamp use for "this destination, struck"), scaled down from
   a hero line to a list row. playApproveStamp() (js/members.js) swaps a
   pending row's Approve/Reject buttons for this element and adds
   .is-struck before the list re-renders without that row. The resting
   transform lives in the base rule, not the keyframes, so an interrupted
   or reduced-motion render still lands correctly placed. */
.member-approve-stamp {
  display: inline-block;
  padding: 0.08em 0.36em 0.16em;
  border: 0.08em solid currentColor;
  border-radius: 0.22em;
  box-shadow: inset 0 0 0 0.032em currentColor;
  font-family: var(--df-mono);
  font-size: var(--fs-sm);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--vn-coral);
  transform: rotate(-2.6deg);
  transform-origin: 40% 60%;
}
@media (prefers-reduced-motion: no-preference) {
  .member-approve-stamp.is-struck { animation: member-approve-strike 420ms cubic-bezier(0.34, 1.56, 0.64, 1) both; }
}
@keyframes member-approve-strike {
  0%   { opacity: 0; transform: rotate(-11deg) scale(1.85); }
  55%  { opacity: 1; transform: rotate(-2.6deg) scale(0.95); }
  78%  { transform: rotate(-2.6deg) scale(1.025); }
  100% { transform: rotate(-2.6deg) scale(1); }
}

/* 28px visible circle, same as before — but the tappable area is expanded
   to --tap-min with an invisible ::before rather than growing the box
   itself, the same idiom .member-menu-btn/.stop-menu-btn already use
   (design critique 2026-08-22: this was 28x28px real hit area, under the
   44px floor, on both People-tab call sites). */
.btn-icon-sm {
  position: relative;
  width: 28px; height: 28px; border-radius: var(--df-radius-stub); border: 2px solid var(--df-ink);
  background: var(--df-panel); display: flex; align-items: center; justify-content: center;
  cursor: pointer; color: var(--df-ink-soft); font-size: var(--fs-base); flex-shrink: 0;
}
.btn-icon-sm::before {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: max(var(--tap-min), 100%);
  height: max(var(--tap-min), 100%);
  transform: translate(-50%, -50%);
}
.btn-icon-sm:hover { border-color: var(--vn-teal); color: var(--vn-teal); }
.btn-icon-sm.sent { background: var(--vn-teal); border-color: var(--vn-teal); color: var(--surface-2); }
.btn-icon-sm.sent:hover { border-color: var(--vn-coral); background: var(--vn-coral); }

.pending-section { margin-top: var(--space-xl); }
.pending-item { display: flex; align-items: center; justify-content: space-between; gap: var(--space-lg); flex-wrap: wrap; padding: var(--space-md) 0; border-bottom: 2px solid var(--df-hair); }
.pending-item:last-child { border-bottom: none; }

.settings-section-title { font-family: var(--df-mono); font-size: var(--fs-sm); font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: var(--df-ink-soft); margin-bottom: var(--space-lg); }

/* ── Standard "X" close/delete button — coral tint at rest so it's clearly
   visible (and reads as "delete") against any card background, in both
   light and dark mode, without needing a hover to notice it. Previously a
   hardcoded white circle, which was nearly invisible against the white
   (--surface-2) card background used in light mode.
   Use with .x-close-btn-corner to pin to the top-right of a card/box. ── */
.x-close-btn {
  background: var(--vn-coral-light);
  border: 2px solid var(--vn-coral-mid);
  color: var(--vn-coral);
  width: 26px;
  height: 26px;
  flex-shrink: 0;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--fs-md);
  line-height: 1;
  cursor: pointer;
  box-shadow: var(--df-press);
  transition: background var(--dur-feedback) var(--df-ease), color var(--dur-feedback) var(--df-ease), border-color var(--dur-feedback) var(--df-ease), box-shadow var(--dur-feedback) var(--df-ease), transform var(--dur-feedback) var(--df-ease);
  position: relative;
}
.x-close-btn:hover { transform: translate(2px, 2px); box-shadow: var(--df-press-hover); }
.x-close-btn:active { transform: translate(4px, 4px); box-shadow: var(--df-press-active); }
/* Same expanded-hit-zone trick as .stop-menu-btn — the visible circle stays
   26px (it sits inline next to other controls at that size throughout the
   app), the tappable region reaches --tap-min. */
.x-close-btn::before {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: max(var(--tap-min), 100%);
  height: max(var(--tap-min), 100%);
  transform: translate(-50%, -50%);
}
.x-close-btn i { color: var(--vn-coral); }
.x-close-btn:hover {
  background: var(--vn-coral);
  border-color: var(--vn-coral);
  /* Issue #432 — a literal white foreground on a --vn-coral fill is
     1.31:1 in dark mode (--vn-coral itself flips to a lighter clay
     there); var(--surface-2) is the theme-correct foreground on a
     saturated fill in both themes (see the note above `@media
     (prefers-color-scheme: dark)`'s --vn-teal definition). */
  color: var(--surface-2);
}
.x-close-btn:hover i { color: var(--surface-2); }
/* Centers the button exactly on the box's top-right border (half in, half
   out) rather than inset inside it - the standard corner-delete position
   used throughout the app. */
.x-close-btn-corner {
  position: absolute;
  top: 0;
  right: 0;
  transform: translate(50%, -50%);
  z-index: 2;
}


.itin-empty {
  padding: 2rem 1rem;
  text-align: center;
  color: var(--df-ink-soft);
  font-size: var(--fs-base);
}

/* ==================== Rating Section ==================== */
.rating-card {
  background: var(--df-panel);
  border: 2px solid var(--df-ink);
  border-radius: var(--df-radius-card);
  box-shadow: var(--df-press);
  padding: 0.9rem 1rem;
  margin-bottom: 0.75rem;
}

/* Issue #71 — gamification trophies */
.trophy-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  gap: var(--space-md);
  margin-bottom: 1rem;
}
/* box-shadow added (design critique 2026-08-23 — layout) — every other card
   on this panel (.member-item, .rating-card, and now
   #people-access-permissions-card) carries the Press shadow; this was the
   one flat exception, reading as a lower tier of object rather than the
   fifth item in the row above it. */
.trophy-card {
  background: var(--df-panel);
  border: 2px solid var(--df-ink);
  border-radius: var(--df-radius-card);
  box-shadow: var(--df-press);
  padding: 0.6rem;
  text-align: center;
  cursor: default;
}
.trophy-emoji { display: block; font-size: var(--fs-2xl); }
.trophy-label { display: block; font-size: var(--fs-xs); font-weight: 600; color: var(--df-ink); margin-top: var(--space-2xs); }
.trophy-winner { display: block; font-size: var(--fs-xs); color: var(--vn-teal); margin-top: 1px; }

.rating-card-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  margin-bottom: 0.6rem;
}

.rating-card-name {
  font-size: var(--fs-md);
  font-weight: 600;
  color: var(--df-ink);
}

.rating-you-tag {
  font-size: var(--fs-xs);
  font-weight: 500;
  color: var(--df-ink-soft);
}

.rating-avg-badge {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--vn-amber-text);
  background: var(--vn-amber-light);
  padding: var(--space-2xs) var(--space-md);
  border-radius: var(--radius-pill);
  white-space: nowrap;
}

.rating-avg-count {
  font-weight: 400;
  color: var(--df-ink-soft);
}

.rating-my-input {
  padding-bottom: 0.75rem;
  margin-bottom: 0.75rem;
  border-bottom: 2px dashed var(--df-hair);
}

.rating-my-input-actions {
  display: flex;
  align-items: center;
  gap: var(--space-md);
}

.rating-my-summary {
  padding-bottom: 0.6rem;
  margin-bottom: 0.6rem;
  border-bottom: 2px dashed var(--df-hair);
}

.rating-my-summary-row {
  display: flex;
  align-items: center;
  gap: var(--space-md);
}

.rating-my-summary-label {
  font-size: var(--fs-sm);
  font-weight: 500;
  color: var(--df-ink-soft);
  flex: 1;
}

.rating-stars {
  display: flex;
  gap: var(--space-xs);
  margin-bottom: 0.5rem;
}

.rating-star {
  font-size: var(--fs-2xl);
  cursor: pointer;
  color: var(--df-ink-soft);
  transition: all 0.15s;
}

.rating-star:hover,
.rating-star.active {
  color: var(--vn-amber-text);
  transform: scale(1.15);
}

.rating-comment {
  width: 100%;
  padding: var(--space-md);
  font-size: var(--fs-sm);
  border: 2px solid var(--df-ink);
  border-radius: var(--df-radius-stub);
  resize: none;
  height: 52px;
  margin-bottom: 0.5rem;
  font-family: inherit;
}

.rating-anon-check {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  font-size: var(--fs-sm);
  color: var(--df-ink-soft);
  margin-bottom: 0.6rem;
  cursor: pointer;
}

.rating-others-list {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.rating-other-item {
  background: var(--df-panel);
  border-radius: var(--df-radius-stub);
  padding: var(--space-md) var(--space-lg);
}

.rating-other-top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
}

.rating-other-name {
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--df-ink);
}

.rating-other-stars {
  font-size: var(--fs-base);
  color: var(--vn-amber-text);
  letter-spacing: 1px;
}

.rating-other-comment {
  font-size: var(--fs-sm);
  color: var(--df-ink-soft);
  font-style: italic;
  margin-top: var(--space-xs);
}

.rating-other-date {
  font-size: var(--fs-xs);
  color: var(--df-ink-soft);
  margin-top: var(--space-xs);
}

.rating-others-empty {
  font-size: var(--fs-sm);
  color: var(--df-ink-soft);
  padding: var(--space-2xs) 0;
}

/* ==================== Receipt Scan Area ==================== */
.scan-area {
  border: 2px dashed var(--df-hair);
  border-radius: var(--df-radius-card);
  padding: var(--space-4xl) var(--space-3xl);
  text-align: center;
  margin-bottom: var(--space-xl);
  background: var(--df-panel);
  transition: border-color var(--dur-state) var(--df-ease);
}

.scan-area > i {
  font-size: var(--fs-3xl);
  color: var(--df-ink-soft);
}

.scan-area p {
  font-size: var(--fs-base);
  color: var(--df-ink-soft);
  margin: var(--space-xs) 0 var(--space-lg);
}

.scan-btns {
  display: flex;
  gap: var(--space-md);
  justify-content: center;
  flex-wrap: wrap;
}

/* Issue #630 — receipt scan moves from a hero block above every field
   (.scan-area, still used elsewhere) to a quiet row inside the "More
   details" disclosure — it's the entry path for a minority of expenses,
   not the majority default.
   Issue #675 — Take Photo/Upload sit on their own line below the heading
   (was one wrapping flex row) — a column layout instead of a row. */
.exp-scan-row {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  padding: var(--space-md) 0;
  margin-bottom: var(--space-md);
  border-bottom: 1px solid var(--df-hair);
  color: var(--df-ink-soft);
  font-size: var(--fs-sm);
}
.exp-scan-row-header {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-md);
}
.exp-scan-row-header > i {
  font-size: var(--fs-lg);
  flex-shrink: 0;
}
.exp-scan-row .scan-btns {
  gap: var(--space-sm);
}

/* Issue #675 — whether Take Photo/Upload runs AI item-extraction or just
   attaches the photo (see toggleAiScanReceipt(), js/expenses.js). Reuses
   .settle-currency-toggle's pill-with-icon shape; pushed to the row's far
   right via margin-left:auto, wrapping to its own line first on a narrow
   viewport rather than overlapping the heading text. */
.exp-ai-scan-toggle {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  min-height: var(--tap-min);
  margin-left: auto;
  background: var(--df-panel);
  border: 2px solid var(--df-ink);
  border-radius: var(--radius-pill);
  padding: var(--space-xs) var(--space-lg);
  font-size: var(--fs-xs);
  font-weight: 500;
  color: var(--df-ink-soft);
  cursor: pointer;
  font-family: inherit;
  flex-shrink: 0;
  transition: background var(--dur-feedback) var(--ease-out), color var(--dur-feedback) var(--ease-out), border-color var(--dur-feedback) var(--ease-out);
}
.exp-ai-scan-toggle.on {
  background: var(--vn-teal-light);
  border-color: var(--vn-teal);
  color: var(--vn-teal);
}

/* Issue #630 — the Day field, compact instead of full-width: a native
   <select> already opens its own list on click, so this is a sizing/
   pill-shape change only, not a new interaction. */
.exp-day-chip-wrap {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  margin-bottom: var(--space-lg);
}
.exp-day-chip-sel {
  width: auto;
  min-width: 100px;
  height: var(--tap-min);
  border-radius: var(--radius-pill);
  padding: var(--space-xs) var(--space-lg);
  border: 2px solid var(--df-ink);
  background: var(--df-panel);
  font-size: var(--fs-sm);
  font-family: inherit;
}
/* Issue #630 follow-up — var(--fs-sm) reads below 16px, iOS auto-zoom risk
   on focus. Kept as the compact desktop chip size; mobile gets its own
   override same as the shared input/select/textarea rule does, rather than
   raising the desktop size unconditionally. */
@media (max-width: 680px) {
  .exp-day-chip-sel {
    font-size: var(--fs-lg);
  }
}

/* Issue #630 — the primary action stays reachable without scrolling past
   the "More details" disclosure on a short viewport; a no-op on desktop
   where the modal doesn't scroll. */
@media (max-width: 680px) {
  .exp-modal-sticky-footer {
    position: sticky;
    bottom: 0;
    background: var(--df-panel-2, var(--surface-2));
    padding: var(--space-md) 0;
    margin: var(--space-md) calc(-1 * var(--space-xl)) calc(-1 * var(--space-xl));
    padding-left: var(--space-xl);
    padding-right: var(--space-xl);
    border-top: 1px solid var(--df-hair);
  }
}

/* ==================== Split Mode Toggle ==================== */
.split-toggle {
  display: flex;
  gap: 0;
  border: 2px solid var(--df-ink);
  border-radius: var(--df-radius-stub);
  overflow: hidden;
  width: 100%;
}

.split-btn {
  flex: 1;
  min-height: var(--tap-min);
  padding: var(--space-md) var(--space-xl);
  border: none;
  background: var(--df-panel);
  color: var(--df-ink-soft);
  font-size: var(--fs-base);
  font-family: inherit;
  cursor: pointer;
  transition: background var(--dur-feedback) var(--df-ease), color var(--dur-feedback) var(--df-ease);
}

.split-btn:first-child {
  border-right: 2px solid var(--df-ink);
}

/* Issue #675 — the Equal-split panel (All/Select-members toggle + the
   member roster) sits flush under the outer Equal/Itemized .split-toggle,
   sharing its border and only rounding the bottom corners, no gap between
   them — one continuous box instead of two stacked, visually unrelated
   toggle bars. .split-toggle-top squares off that outer bar's own bottom
   corners to match, or its pill rounding would poke out past the panel's
   flat top edge right where they're supposed to look joined. */
.split-toggle-top {
  border-radius: var(--df-radius-stub) var(--df-radius-stub) 0 0;
}
.split-equal-panel {
  border: 2px solid var(--df-ink);
  border-top: none;
  border-radius: 0 0 var(--df-radius-stub) var(--df-radius-stub);
  padding: var(--space-lg);
  background: var(--df-panel);
}

/* Issue #670 — the All/Select-members toggle nests inside .split-equal-panel
   at otherwise identical weight to the outer Equal/Itemized toggle; this
   modifier reads it as clearly subordinate (smaller type, tighter padding)
   without dropping under --tap-min. */
.split-toggle-sub .split-btn {
  font-size: var(--fs-sm);
  padding: var(--space-sm) var(--space-lg);
}

.split-btn.active {
  background: var(--vn-teal);
  color: var(--surface-2);
  font-weight: 600;
}

.scan-btn {
  display: inline-flex;
  align-items: center;
  gap: var(--space-sm);
  min-height: var(--tap-min);
  padding: var(--space-md) var(--space-3xl);
  border-radius: var(--radius-pill);
  border: 2px solid var(--df-ink);
  background: var(--df-panel);
  color: var(--df-ink);
  font-size: var(--fs-base);
  font-family: inherit;
  cursor: pointer;
  box-shadow: var(--df-press);
  transition: background var(--dur-feedback) var(--df-ease), border-color var(--dur-feedback) var(--df-ease), box-shadow var(--dur-feedback) var(--df-ease), transform var(--dur-feedback) var(--df-ease);
}

.scan-btn:hover {
  border-color: var(--vn-teal);
  color: var(--vn-teal);
  transform: translate(2px, 2px);
  box-shadow: var(--df-press-hover);
}
.scan-btn:active {
  transform: translate(4px, 4px);
  box-shadow: var(--df-press-active);
}

.scan-btn i {
  font-size: var(--fs-md);
}

.vnd-note {
  font-size: var(--fs-xs);
  color: var(--df-ink-soft);
  margin-top: var(--space-xs);
}

/* ==================== Expense Items ==================== */
/* gap/margin-top are large enough that a .x-close-btn-corner button (which
   protrudes ~13px above its row's top border) never overlaps the row above
   it or the header row. */
.items-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-4xl);
  margin-top: var(--space-2xl);
  margin-bottom: var(--space-md);
}

/* Header row above .items-list — columns match .item-row-top's grid exactly
   (7% No. / 68% Item / 25% Price) so the two stay visually aligned. */
.items-header-row {
  display: grid;
  grid-template-columns: 7% 68% 25%;
  gap: var(--space-xs);
  padding: 0 var(--space-3xl) var(--space-sm);
  font-family: var(--df-mono);
  font-size: var(--fs-xs);
  font-weight: 700;
  color: var(--df-ink-soft);
  text-transform: uppercase;
  letter-spacing: 0.08em;
}
.items-col-price { text-align: right; }

.item-row {
  position: relative;
  background: var(--df-panel);
  border: 2px solid var(--df-ink);
  border-radius: var(--df-radius-card);
  padding: var(--space-lg) var(--space-3xl);
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}

.item-row-top {
  display: grid;
  grid-template-columns: 7% 68% 25%;
  gap: var(--space-xs);
  align-items: center;
}

.item-no {
  font-size: var(--fs-base);
  font-weight: 600;
  color: var(--df-ink-soft);
}

.item-name-input {
  /* No own font-size (issue #446) — see .pack-reassign-select above. */
  padding: var(--space-xs) var(--space-md);
  border: 2px solid var(--df-ink);
  border-radius: var(--df-radius-stub);
  background: var(--df-panel);
  color: var(--df-ink);
  font-family: inherit;
  min-width: 0;
}

.item-amount-input {
  width: 100%;
  padding: var(--space-xs) var(--space-md);
  border: 2px solid var(--df-ink);
  border-radius: var(--df-radius-stub);
  background: var(--df-panel);
  color: var(--df-ink);
  font-family: inherit;
  min-width: 0;
}

.item-name-static {
  font-size: var(--fs-base);
  font-weight: 500;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.item-amount-static {
  font-size: var(--fs-base);
  color: var(--df-ink-soft);
  white-space: nowrap;
  text-align: right;
}

/* Issue #630 — the Add/Edit Expense form's itemized rows stopped using
   .assignee-btn: .pax-btn (below, "Split mode") is now the single person-
   selection primitive across Paid by, Equal split AND itemized rows — one
   height, one type scale, one selected treatment, so the two states can't
   invert each other the way .pax-btn's teal-light "selected" and
   .assignee-btn's solid-teal "selected" used to. A dense itemized row
   tightens via gap/horizontal padding only (.pax-btn-compact below) —
   never by shrinking under --tap-min again, which is what made these
   ~19px tall in the first place.
   .assignee-btn itself is NOT deleted — js/packing.js (check-off chips,
   scope/assignee pickers) and js/accommodations.js (hotel-vote thumbs-up)
   still use it for an unrelated feature; only .all-btn (the expense form's
   own "All" chip, now a text toggle, .item-all-none-toggle below) had no
   other caller and was safe to remove. */
.item-assignees {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-xs);
}

/* Issue: these were ~19px tall — the core interaction of the itemized-split
   feature, performed one-handed. A literal --tap-min (44px) box would
   balloon a dense wrapped row of these into something absurd and, worse,
   each chip's invisible hit-zone would overlap its neighbors (the same
   problem .itin-cal-plan-badge's own comment above describes) — so like
   that badge, this caps at the WCAG AA floor (24px) rather than the fuller
   --tap-min, with the row's gap widened from --space-xs to --space-lg so
   adjacent chips don't crowd each other even at the larger size. Still
   used by js/packing.js and js/accommodations.js (see comment above) —
   only the Add/Edit Expense form itself moved off this class. */
.assignee-btn {
  min-height: 24px;
  font-size: var(--fs-xs);
  padding: var(--space-xs) var(--space-lg);
  border-radius: var(--radius-pill);
  border: 2px solid var(--df-ink);
  background: var(--df-panel);
  color: var(--df-ink-soft);
  cursor: pointer;
  font-family: inherit;
  transition: background var(--dur-feedback) var(--ease-out), color var(--dur-feedback) var(--ease-out), border-color var(--dur-feedback) var(--ease-out);
}

.assignee-btn:disabled {
  cursor: not-allowed;
  opacity: 0.5;
}

.assignee-btn.sel {
  background: var(--vn-teal);
  border-color: var(--vn-teal);
  color: var(--surface-2);
}

.item-share-note {
  font-size: var(--fs-xs);
  color: var(--df-ink-soft);
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
}

.items-member-totals {
  background: var(--df-panel);
  border-radius: var(--df-radius-stub);
  padding: var(--space-lg) var(--space-xl);
  margin-top: var(--space-xl);
}

.member-total-row {
  display: flex;
  justify-content: space-between;
  font-size: var(--fs-base);
  padding: var(--space-xs) 0;
}

.btn-small {
  min-height: var(--tap-min);
  font-size: var(--fs-sm);
  padding: var(--space-sm) var(--space-xl);
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: var(--surface-2);
  color: var(--text-primary);
  cursor: pointer;
  font-family: inherit;
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  transition: background var(--dur-feedback) var(--ease-out);
}

.btn-small:hover {
  background: var(--surface-1);
}

.btn-small.sent { background: var(--vn-teal); border-color: var(--vn-teal); color: var(--surface-2); }
.btn-small.sent:hover { background: var(--vn-coral); border-color: var(--vn-coral); }

/* "Belanja" treat-expense (issue #72) */
.belanja-btn { background: var(--vn-teal); border-color: var(--vn-teal); color: var(--surface-2); }
.belanja-btn:hover { background: var(--vn-coral); border-color: var(--vn-coral); }

/* One-off purple accent, used only for the Transfer Host action —
   not part of the three-hue brand palette, so it's a dedicated class
   with its own dark-mode override rather than a new --vn-* token. */
.btn-transfer-host { color: #7c3aed; border-color: #c4b5fd; }
@media (prefers-color-scheme: dark) {
  .btn-transfer-host { color: #a78bfa; border-color: #5b3fa0; }
}

/* ==================== Empty State ==================== */
.empty {
  text-align: center;
  padding: 2rem 1rem;
  color: var(--df-ink-soft);
  font-size: var(--fs-base);
}

.empty i {
  font-size: var(--fs-4xl);
  display: block;
  margin-bottom: var(--space-md);
  opacity: 0.4;
}

/* ==================== Tabs Subtabs ==================== */
/* Expenses' subtab row (All/Food/Transport/Shopping/Hotel/Activity/Other/
   Shared Fund) has no fallback for not fitting — genuinely overflows the
   viewport below ~390px, which forces horizontal scroll since nothing here
   previously clipped or scrolled it. Horizontal scroll matches this app's
   existing convention for a crowded control row that can't wrap
   (.itin-calendar, .lp-discover-row). The wrapping ".subtabs-scroll" adds a
   fade-to-edge cue plus keyboard-visible scroll so "there's more" is never
   silent — the raw OS scrollbar with no cue was the actual bug, not the
   horizontal scroll itself. */
.subtabs-scroll {
  position: relative;
  margin-bottom: 1rem;
}

.subtabs-scroll::after {
  content: '';
  position: absolute;
  top: 0;
  right: 0;
  bottom: 2px;
  width: 28px;
  background: linear-gradient(to right, transparent, var(--df-ground) 85%);
  pointer-events: none;
  /* Hidden once the row has scrolled to (or never had) an overflowing
     right edge — toggled by a tiny scroll listener in expenses.js. */
  opacity: 1;
  transition: opacity var(--dur-feedback) var(--ease-out);
}

.subtabs-scroll.at-end::after {
  opacity: 0;
}

.subtabs {
  display: flex;
  gap: var(--space-xs);
  border-bottom: 2px solid var(--df-hair);
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: thin;
}

.subtab {
  flex-shrink: 0;
  white-space: nowrap;
  min-height: var(--tap-min);
  padding: var(--space-sm) var(--space-xl);
  font-size: var(--fs-sm);
  background: none;
  border: none;
  cursor: pointer;
  color: var(--df-ink-soft);
  border-bottom: 2px solid transparent;
  font-weight: 400;
  transition: color var(--dur-feedback) var(--df-ease), border-color var(--dur-feedback) var(--df-ease);
}

/* Category Compass — the filter row was the one surface where every
   category's own hue (already used on its .cat-pill badge and its
   .expense-item border stripe) didn't appear at all; every tab rendered
   the same trail-green active state regardless of which category it
   filtered to. */
.subtab.active {
  color: var(--vn-teal);
  border-bottom-color: var(--vn-teal);
  font-weight: 500;
}

.subtab.cmp-food.active {
  color: var(--vn-amber-text);
  border-bottom-color: var(--vn-amber);
}

.subtab.cmp-transport.active {
  color: var(--vn-sky-text);
  border-bottom-color: var(--vn-sky);
}

.subtab.cmp-shopping.active {
  color: var(--vn-coral);
  border-bottom-color: var(--vn-coral);
}

.subtab.cmp-hotel.active {
  color: var(--vn-indigo);
  border-bottom-color: var(--vn-indigo);
}

.subtab.cmp-activity.active {
  color: var(--vn-green);
  border-bottom-color: var(--vn-green);
}

/* Settings sub-tab bar (#680) — /impeccable critique 2026-08-21, P1: this was
   the one piece of chrome on an otherwise-converted screen carrying none of
   the product's type voice, reusing Expenses' category-filter .subtab idiom
   verbatim (system-sans, no tracking, a translucent --df-hair divider
   instead of a real keyline). Scoped to #settings-subtabs specifically,
   not the shared .subtab base — Expenses' own filter tabs are a separate,
   unreviewed design decision and stay exactly as they were. Matches
   .set-block-title's treatment (Martian Mono, 700, uppercase, 0.08em) so the
   bar reads as belonging to the same screen as the content it introduces. */
#settings-subtabs {
  border-bottom: 2px solid var(--df-ink);
}

#settings-subtabs .subtab {
  font-family: var(--df-mono);
  font-size: var(--fs-sm);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--df-ink-soft);
}

#settings-subtabs .subtab.active {
  color: var(--vn-teal);
  border-bottom-color: var(--vn-teal);
}

/* ==================== Responsive ==================== */
@media (max-width: 480px) {
  .stat-grid {
    grid-template-columns: 1fr 1fr;
  }

  .modal-content {
    padding: 1.5rem;
  }

  #addExpenseModal .modal-header {
    top: -1.5rem;
    margin: -1.5rem -1.5rem 1.5rem;
    padding: 1.5rem 1.5rem 1rem;
  }

  .form-row {
    grid-template-columns: 1fr;
  }

  /* .home-topbar has three things competing for space here (trip name,
     member-count badge, account pill) — the badge is the least essential
     (member details are still in Settings), so it's the one that gives. */
  .trip-badge {
    display: none;
  }

  /* .notification-dropdown's default right:0 anchors it to the bell
     trigger, not the viewport — and the bell isn't flush against the
     screen edge (.home-profile sits to its right), so a 300px/85vw-wide
     dropdown genuinely runs off the left edge of the screen below ~400px.
     Anchoring to the viewport directly instead of the trigger sidesteps
     the problem outright rather than trying to clamp a width/position
     that depends on exactly how much room the trigger has around it. */
  .notification-dropdown {
    position: fixed;
    top: 65px; /* matches .app-main's own topbar-clearance convention */
    right: 8px;
    left: 8px;
    width: auto;
    max-width: none;
  }

  /* Dropped entirely, not just capped at 90px. On the trip view the trip is
     the subject of the page and its name was truncating to "Japan…" while
     the signed-in username — the one thing the user already knows — kept a
     pill roughly three times wider. The avatar still identifies the account
     and the caret still says the pill opens a menu; the name is back in the
     dropdown the moment it opens. Frees ~90px for the title beside it. */
  .home-profile-name {
    display: none;
  }
}

#my-trips-screen {
  display: none;
}

#my-trips-screen .btn-large {
  font-size: var(--fs-md);
  padding: var(--space-2xl) var(--space-3xl);
}

/* No max-height/overflow here on purpose — .home-body (below) is the one
   scroll region for this screen. This element used to duplicate that with
   its own inner `overflow-y: auto` sized off `100vh` (ignoring mobile browser
   chrome, unlike .home-screen's deliberate `dvh`), producing a scroller
   nested inside a scroller that trapped touch scrolling. */
#my-trips-list {
  padding-top: var(--space-2xl);
}


/* ==================== Receipt Progress Bar ==================== */
.progress-track {
  width: 100%;
  height: 4px;
  background: var(--df-hair);
  border-radius: 2px;
  overflow: hidden;
}

.progress-fill {
  height: 100%;
  width: 40%;
  background: var(--vn-teal);
  border-radius: 2px;
  animation: progress-slide 1.2s ease-in-out infinite;
}

@keyframes progress-slide {
  0%   { transform: translateX(-150%); }
  50%  { transform: translateX(100%); }
  100% { transform: translateX(250%); }
}

/* Destination summary row — default (fresh-mode) card treatment. Moved out
   of index.html's inline style so .itin-generate-destination-compact below
   can override it without !important. */
#itin-generate-destination-summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-sm);
  padding: var(--space-sm) var(--space-md);
  background: var(--df-hair);
  border-radius: var(--df-radius-stub);
}
#itin-generate-destination-text {
  font-size: var(--fs-base);
  min-width: 0;
  overflow-wrap: anywhere;
}

/* Issue #545 harden pass — in adjust mode this block is edited in only a
   minority of opens (a host/co-host overriding the destination for one
   generation), yet it rendered as the same bordered form-group card fresh
   mode needs, adding a decision surface to a flow that's supposed to be
   "type what you want changed." openGenerateModal() toggles this class for
   mode === 'adjust' only; fresh mode is unaffected. Demotes to a plain
   single line — label, value and edit icon inline, no card fill — while
   leaving toggleAgentDestinationEditor()'s expand-on-tap behavior untouched. */
.itin-generate-destination-compact {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2xs) var(--space-sm);
}
.itin-generate-destination-compact > .form-label {
  display: inline;
  margin-bottom: 0;
  font-size: var(--fs-xs);
  flex-shrink: 0;
}
.itin-generate-destination-compact #itin-generate-destination-summary {
  background: none;
  padding: 0;
  border-radius: 0;
  flex: 1 1 auto;
  min-width: 0;
}
.itin-generate-destination-compact #itin-generate-destination-text {
  color: var(--df-ink-soft);
  font-size: var(--fs-sm);
}
.itin-generate-destination-compact #itin-generate-destination-editor {
  flex-basis: 100%;
}

/* ==================== AI Generate / Ask AI to Adjust modal ====================
   Issue #545 harden pass. The modal chrome (header, inputs, buttons) already
   inherits .df-modal-content; this covers the pieces unique to this flow that
   had never been styled at all — the Generate/History/Saved tabs, the
   revision diff list, and the sticky action footer the HTML comment above
   #agent-modal-actions already claimed existed. */

/* Generate/History/Saved — same segmented-pill construction as #itin-view-toggle
   (2px keyline container, inner dividers), so the two tab bars a member sees
   in the same tab read as one system. */
.agent-tabs {
  display: flex;
  border: 2px solid var(--df-ink);
  border-radius: var(--radius-pill);
  overflow: hidden;
  margin-bottom: var(--space-2xl);
}
.agent-tab {
  flex: 1;
  min-height: var(--tap-min);
  padding: var(--space-sm) var(--space-md);
  font-size: var(--fs-sm);
  font-weight: 600;
  font-family: inherit;
  border: none;
  background: var(--df-panel);
  color: var(--df-ink-soft);
  cursor: pointer;
  transition: background var(--dur-feedback) var(--ease-out), color var(--dur-feedback) var(--ease-out);
}
.agent-tab + .agent-tab {
  border-left: 2px solid var(--df-ink);
}
.agent-tab.active {
  background: var(--vn-teal);
  color: var(--surface-2);
}

/* Generate flow status line — plain by default, switches to the Alert Red
   treatment (never Stamp Coral, which is branded and must not read as
   danger) once generateItinerary() marks a failed attempt. role="status"
   aria-live="polite" (index.html) means the switch is announced, not just
   painted. */
#itin-generate-status {
  font-size: var(--fs-base);
  color: var(--df-ink-soft);
  padding: var(--space-md) 0;
}
#itin-generate-status.status-error {
  color: var(--error);
  font-weight: 600;
}

/* The revision diff list — Added/Edited/Removed. Text-first: a Martian Mono
   badge names the state so the meaning survives colour-blindness; the
   badge's outline ink is reinforcement only (kicker-pill construction: 2px
   currentColor keyline, transparent fill — DESIGN.md's Chips section). */
.agent-diff-row {
  padding: var(--space-md) 0;
  border-bottom: 2px solid var(--df-hair);
  overflow-wrap: anywhere;
}
.agent-diff-row:last-child { border-bottom: none; }

.agent-diff-meta {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-sm);
  font-family: var(--df-mono);
  font-size: var(--fs-xs);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--df-ink-soft);
  margin-bottom: var(--space-xs);
}
.agent-diff-time { color: var(--df-ink-soft); }

.agent-diff-badge {
  font-family: var(--df-mono);
  font-size: 0.625rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 1px var(--space-sm);
  border: 2px solid currentColor;
  border-radius: var(--radius-pill);
}
.agent-diff-added .agent-diff-badge { color: var(--vn-teal); }
.agent-diff-changed .agent-diff-badge { color: var(--df-ink-soft); }
/* --error-text, not --error — this renders as text (`currentColor` on a
   `border`, but also directly as the badge label), and --error itself is
   sub-AA as text (issue #545 harden pass). */
.agent-diff-removed .agent-diff-badge { color: var(--error-text); }

.agent-diff-dest {
  font-weight: 700;
  color: var(--df-ink);
}
.agent-diff-removed .agent-diff-dest {
  text-decoration: line-through;
  color: var(--df-ink-soft);
}

.agent-diff-note {
  font-size: var(--fs-xs);
  color: var(--df-ink-soft);
  margin-top: var(--space-2xs);
}

.agent-diff-reference {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  margin-top: var(--space-sm);
}
.agent-diff-photo {
  width: 32px;
  height: 32px;
  object-fit: cover;
  border-radius: var(--df-radius-print);
  border: 2px solid var(--df-ink);
  flex-shrink: 0;
}
.agent-diff-link {
  font-size: var(--fs-xs);
  color: var(--vn-teal);
}

/* A revision that came back identical to the current plan — nothing added,
   removed or changed. Renders instead of a silent, unmarked repeat of the
   same list, which used to be indistinguishable from a bug. Same
   amber-light/amber-text notice pairing used for pending-status callouts
   throughout this file. */
.agent-diff-empty {
  display: flex;
  align-items: flex-start;
  gap: var(--space-sm);
  padding: var(--space-lg) var(--space-md);
  margin-bottom: var(--space-lg);
  background: var(--vn-amber-light);
  color: var(--vn-amber-text);
  border-radius: var(--df-radius-stub);
  font-size: var(--fs-sm);
}
.agent-diff-empty i { flex-shrink: 0; margin-top: 2px; }

/* The sticky footer the HTML's own comment already describes — never
   actually implemented (there was no CSS for #agent-modal-actions at all).
   On a long revision (many days/stops) the Accept/Regenerate buttons would
   scroll out of reach, exactly the "no confirmation button" bug this modal
   was built to fix (issue #135 Phase 1.5). Same negative-margin trick
   #addExpenseModal's header uses, mirrored to the bottom edge. */
#agent-modal-actions {
  position: sticky;
  bottom: -2rem;
  z-index: 5;
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  margin: var(--space-2xl) -2rem -2rem;
  padding: var(--space-lg) 2rem 2rem;
  background: var(--df-panel);
  border-top: 2px solid var(--df-hair);
  border-radius: 0 0 var(--df-radius-card) var(--df-radius-card);
}

@media (max-width: 480px) {
  #agent-modal-actions {
    margin: var(--space-2xl) -1.5rem -1.5rem;
    padding: var(--space-lg) 1.5rem 1.5rem;
  }
}

/* ==================== Itinerary Choice Cards ==================== */
.choice-cards {
  display: flex;
  gap: 1rem;
  flex-wrap: wrap;
}

.choice-card {
  flex: 1;
  min-width: 140px;
  border: 2px solid var(--df-ink);
  border-radius: var(--df-radius-card);
  padding: 1.25rem 1rem;
  text-align: center;
  cursor: pointer;
  transition: border-color 0.15s, background 0.15s;
}

.choice-card:hover {
  border-color: var(--vn-teal);
  background: var(--vn-teal-light);
}

.choice-card-icon {
  font-size: var(--fs-3xl);
  color: var(--vn-teal);
  display: block;
  margin-bottom: var(--space-md);
}

.choice-card-title {
  font-weight: 700;
  font-size: var(--fs-md);
  margin-bottom: var(--space-xs);
}

.choice-card-desc {
  font-size: var(--fs-sm);
  color: var(--df-ink-soft);
  line-height: 1.4;
}

/* Issue #242 — "Browse destination templates"/"Let AI help me plan" render
   disabled with their reason inline (not hidden) until destination + dates
   are confirmed; see renderItinChoiceGating() in js/itinerary.js. */
.choice-card-disabled {
  opacity: 0.5;
  cursor: not-allowed;
  pointer-events: none;
}

/* Issue #242 — the pre-itinerary planning hub, reusing .choice-card for its
   individual cards (same visual language as the "Where are you headed?"
   modal's choice cards). */
.planning-hub {
  border: 2px solid var(--df-ink);
  border-radius: var(--df-radius-card);
  padding: 1rem;
  margin-bottom: 1rem;
  background: var(--df-panel);
}

.planning-hub-header {
  font-family: var(--df-display);
  font-weight: 800;
  font-size: var(--fs-lg);
  letter-spacing: -0.02em;
  margin-bottom: 0.75rem;
}

.planning-hub-cards {
  display: flex;
  gap: 0.75rem;
  flex-wrap: wrap;
}

.planning-hub-cards .choice-card {
  text-align: left;
  min-width: 160px;
}

/* Issue #312 — the "Where to go" hub card, once destinationLocked, lists
   each confirmed city stacked instead of a single description line. */
.planning-hub-destination-line {
  font-weight: 600;
  color: var(--df-ink);
}
.planning-hub-destination-line + .planning-hub-destination-line {
  margin-top: var(--space-2xs);
}

/* Issue #242 — "or start a vote" secondary action inside a hub card, and the
   poll voting UI inside #tripPollModal. */
.planning-hub-vote-link {
  display: block;
  background: none;
  border: none;
  color: var(--vn-teal);
  font-size: var(--fs-xs);
  font-weight: 600;
  cursor: pointer;
  font-family: inherit;
  padding: var(--space-xs) 0 0;
  text-align: left;
}

.planning-hub-poll-option {
  display: flex;
  align-items: center;
  gap: var(--space-lg);
  padding: var(--space-md) 0;
  border-bottom: 2px solid var(--df-hair);
}

.planning-hub-poll-option:last-child {
  border-bottom: none;
}

/* Issue #312 Phase 2 — "Where to go" modal's suggestion list. */
.where-to-go-suggestion {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  padding: var(--space-sm) 0;
  border-bottom: 2px solid var(--df-hair);
  font-size: var(--fs-base);
}

.where-to-go-suggestion:last-child {
  border-bottom: none;
}

.where-to-go-suggested-by {
  color: var(--df-ink-soft);
  font-size: var(--fs-xs);
  flex-shrink: 0;
}

.where-to-go-empty {
  color: var(--df-ink-soft);
  font-size: var(--fs-base);
  text-align: center;
  padding: var(--space-lg) 0;
}

.where-to-go-note {
  color: var(--df-ink-soft);
  font-size: var(--fs-xs);
  margin-bottom: var(--space-sm);
}

/* Issue #312 UX round 2 — section headings inside the Where to go modal
   ("Current Destination" / "Suggested"). */
.where-to-go-section-label {
  font-family: var(--df-mono);
  font-size: var(--fs-xs);
  color: var(--df-ink-soft);
  text-transform: uppercase;
  font-weight: 700;
  letter-spacing: 0.08em;
  margin-bottom: var(--space-sm);
}

/* One row of the ordered "Current Destination" list. */
.where-to-go-destination {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  padding: var(--space-sm) 0;
  border-bottom: 2px solid var(--df-hair);
  font-size: var(--fs-base);
}

.where-to-go-destination:last-child {
  border-bottom: none;
}

.where-to-go-destination-num {
  flex-shrink: 0;
  width: 22px;
  height: 22px;
  border-radius: var(--radius-pill);
  background: var(--vn-teal-light);
  color: var(--vn-teal);
  font-size: var(--fs-xs);
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: center;
}

.where-to-go-destination-label {
  flex: 1;
  min-width: 0;
}

.where-to-go-icon-btn {
  flex-shrink: 0;
  min-width: var(--tap-min);
  min-height: var(--tap-min);
  display: flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: none;
  color: var(--df-ink-soft);
  cursor: pointer;
  font-family: inherit;
  border-radius: var(--df-radius-stub);
}

.where-to-go-icon-btn:hover:not(:disabled) {
  background: var(--df-hair);
  color: var(--df-ink);
}

.where-to-go-icon-btn:disabled {
  opacity: 0.3;
  cursor: not-allowed;
}

.planning-hub-poll-vote-btn {
  flex-shrink: 0;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  border: 2px solid var(--df-ink);
  background: var(--df-panel);
  color: var(--df-ink-soft);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  font-size: var(--fs-lg);
}

.planning-hub-poll-vote-btn.voted {
  border-color: var(--vn-teal);
  color: var(--vn-teal);
  background: var(--vn-teal-light);
}

.planning-hub-poll-vote-btn:disabled {
  cursor: not-allowed;
  opacity: 0.6;
}

.planning-hub-poll-option-body {
  flex: 1;
  min-width: 0;
}

.planning-hub-poll-option-label {
  font-weight: 600;
  font-size: var(--fs-base);
}

.planning-hub-poll-option-votes {
  font-size: var(--fs-xs);
  color: var(--df-ink-soft);
}

/* ==================== Itinerary Builder ==================== */
.builder-day {
  border: 2px solid var(--df-ink);
  border-radius: var(--df-radius-card);
  padding: var(--space-xl) var(--space-2xl);
  margin-bottom: var(--space-xl);
}

.builder-day-header {
  font-weight: 700;
  font-size: var(--fs-md);
  color: var(--vn-teal);
  margin-bottom: var(--space-md);
}

.builder-stop-row {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  font-size: var(--fs-base);
  padding: var(--space-xs) 0;
  border-bottom: 2px solid var(--df-hair);
  flex-wrap: wrap;
}

.builder-stop-time {
  color: var(--df-ink-soft);
  min-width: 48px;
  font-variant-numeric: tabular-nums;
}

.builder-stop-dest {
  font-weight: 600;
  flex: 1;
}

.builder-stop-addr {
  font-size: var(--fs-xs);
  color: var(--df-ink-soft);
  width: 100%;
  padding-left: 56px;
}

.builder-stop-edit {
  background: none;
  border: none;
  cursor: pointer;
  color: var(--df-ink-soft);
  font-size: var(--fs-base);
  padding: 0 var(--space-xs);
  line-height: 1;
}

.builder-stop-del {
  background: var(--df-panel) !important;
  border: 2px solid var(--df-ink);
  box-shadow: var(--df-press);
  color: var(--df-ink) !important;
  cursor: pointer;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: var(--fs-xs);
  line-height: 1;
  flex-shrink: 0;
}

.builder-stop-edit:hover { color: var(--vn-teal); }
.builder-stop-del:hover { background: var(--vn-coral-light) !important; border-color: var(--vn-coral-mid); color: var(--vn-coral) !important; }

.builder-add-form {
  display: flex;
  flex-direction: column;
  margin-top: var(--space-lg);
  padding: var(--space-lg);
  background: var(--df-ground);
  border-radius: var(--df-radius-stub);
}

.builder-add-form input {
  /* No own font-size (issue #446) — see .pack-reassign-select above. */
  font-family: inherit;
  border: 2px solid var(--df-ink);
  border-radius: var(--df-radius-stub);
  padding: var(--space-md) var(--space-lg);
  background: var(--df-panel);
  color: inherit;
}

/* ==================== Home / My Trips Screen ==================== */

/* Outer shell: a full-viewport-height column of [topbar, main] on mobile so
   the three zones inside .home-main always fit exactly; on desktop this
   relaxes into a normal flowing page — see the Desktop layout block above
   .stat-grid for the >=1024px behavior. */
/* ==================== MY TRIPS — Departure Fever ====================
   Converted whole from the retired Field Journal look (soft blurred shadows,
   near-white cards on #f5f5f5, system type, 16px radii). DESIGN.md's
   migration note is why this is one commit covering shape, type and shadow
   together: a panel carrying both a blurred card shadow and a hard ink
   offset reads as a bug, not a transition.

   What is deliberately NOT converted here: .home-topbar, .home-profile* and
   .notification-* above. Those are shared verbatim with the in-trip app
   shell (index.html:1734), so converting them from this screen would convert
   half of another screen — exactly what the whole-screen rule forbids. They
   already sit on --topbar-bg, which IS the system's Field Green, so the
   chrome is on-vocabulary on colour today and the seam is only in the
   dropdown panels' geometry. They convert with the app shell, not here. */
.home-screen {
  display: flex;
  flex-direction: column;
  height: 100vh;
  height: 100dvh; /* respects mobile browser chrome */
  overflow: hidden;
  /* The ground is a shape: a real 6px halftone dot screen over two broad
     radial ink washes. It must stay continuous — see the --df-halftone note
     in :root for why nothing over it may carry a backdrop-filter. */
  background-color: var(--df-ground);
  background-image: var(--df-halftone), var(--df-wash);
  background-size: 6px 6px, auto, auto;
  background-attachment: fixed;
  color: var(--df-ink);
  font-family: var(--df-sans);
}

/* The page-wide focus ring for this system: 3px at 3px offset, no glow
   anywhere. Scoped to the screen rather than raising the app-wide 2px rule,
   which belongs to the unconverted surfaces until they convert too. */
.home-screen a:focus-visible,
.home-screen button:focus-visible,
.home-screen [tabindex]:not([tabindex="-1"]):focus-visible {
  outline: 3px solid var(--df-brand);
  outline-offset: 3px;
}

/* Top bar — spans the full page width (unlike .home-main/.app-main below,
   which stay column-width), holding branding on the left and the account
   menu on the right. Reused as-is by the in-trip app (see .app in the App
   Layout section) so both screens share identical top-bar chrome; on the
   My Trips screen there's nothing to its left/right that could overlap it,
   but the in-trip .tabs sidebar is position:fixed from the top of the
   viewport, so it needs a z-index bump to guarantee it paints above that
   fixed sidebar rather than leaving it to DOM-order luck. */
.home-topbar {
  position: relative;
  z-index: 45;
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.9rem 1.5rem;
  /* Darker than the bar rather than a green tint of it — the tint was picked
     against the old mint background and is invisible on the brand green. */
  border-bottom: 0.5px solid rgba(0,0,0,0.18);
  background: var(--topbar-bg);
}

/* min-height: on the trip page this is the only route back to My Trips (the
   sidebar has no Home item), and it measured 37px. Harmless on the My Trips
   screen, where the same frame is inert branding. */
.home-topbar-brand {
  display: flex;
  align-items: center;
  gap: var(--space-lg);
  min-height: var(--tap-min);
  min-width: 0; /* lets the trip name/subtitle below ellipsis instead of overflowing */
}

/* The trip view's copy of the frame is a real <button> (see index.html) —
   only that instance needs the resets, since the My Trips one is still an
   inert <div>. text-align:left because a button centers its content by
   default, which would un-align the title from the logo beside it. */
.home-topbar-brand-btn {
  border: none;
  background: none;
  padding: 0;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
}

/* Wraps title+subtitle so long trip names truncate instead of pushing the
   account menu off-screen; harmless for the My Trips screen's static short
   "Lancong" text, which never needs to shrink. */
.home-topbar-brand-text {
  min-width: 0;
  overflow: hidden;
}

.home-topbar-title {
  font-weight: 600;
  font-size: var(--fs-lg);
  color: var(--topbar-fg);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Account menu: a profile pill that opens a small dropdown (Create Account
   for guests + Log out) instead of spelling both out as separate buttons. */
.home-profile {
  position: relative;
  min-width: 0; /* lets .home-profile-name actually ellipsis instead of overflowing */
}

.home-profile-trigger {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  min-width: 0;
  max-width: 100%;
  min-height: var(--tap-min);
  background: #fff;
  border: 0.5px solid transparent;
  border-radius: var(--radius-pill);
  padding: var(--space-xs) var(--space-lg) var(--space-xs) var(--space-xs);
  font-size: var(--fs-sm);
  font-weight: 500;
  color: var(--topbar-accent);
  font-family: inherit;
  cursor: pointer;
  transition: border-color 0.15s;
}
/* --topbar-accent, not --vn-teal: this pill is fixed white in both themes,
   so the theme-aware token would draw a mint hairline in dark mode. */
.home-profile-trigger:hover { border-color: var(--topbar-accent); }

.home-profile-avatar {
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: #fff;
  color: var(--topbar-accent);
  font-size: var(--fs-xs);
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}

.home-profile-name {
  max-width: 130px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Sits inside the white .home-profile-trigger pill, not on the bar, so it
   takes the accent rather than --topbar-fg-muted (white on white). */
.home-profile-caret {
  font-size: var(--fs-sm);
  color: var(--topbar-accent);
  flex-shrink: 0;
}

/* Departure Fever geometry (font already inherited Figtree from .home-screen/
   .app — this is the "half-converted" seam the My Trips comment flagged:
   only shape was still Field Journal). A dropdown reads as a small torn-off
   stub of the same stock the page is cut from, so it takes the stub radius
   and the card-scale press shadow rather than the pill/12px controls take. */
.home-profile-dropdown {
  display: none;
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  min-width: 190px;
  background: var(--df-panel);
  border: 2px solid var(--df-ink);
  border-radius: var(--df-radius-card);
  box-shadow: var(--df-press-large);
  padding: var(--space-sm);
  z-index: 50;
}
.home-profile-dropdown.open { display: block; }

.home-profile-dropdown-meta {
  font-size: var(--fs-xs);
  color: var(--df-ink-soft);
  padding: var(--space-sm) var(--space-lg) var(--space-md);
  border-bottom: 2px solid var(--df-hair);
  margin-bottom: var(--space-xs);
}

.home-profile-item {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  width: 100%;
  min-height: var(--tap-min);
  background: none;
  border: none;
  border-radius: var(--df-radius-stub);
  padding: var(--space-md) var(--space-lg);
  font-size: var(--fs-base);
  font-family: inherit;
  color: var(--df-ink);
  cursor: pointer;
  text-align: left;
  text-decoration: none; /* the Admin menu entry (issue #182) is an <a>, not a <button> */
  box-sizing: border-box;
}
.home-profile-item:hover { background: var(--df-hair); }
.home-profile-item-danger { color: var(--vn-coral); }
.home-profile-item-danger:hover { background: var(--vn-coral-light); }

/* Notification bell - same trigger/dropdown/open-class shape as
   .home-profile above, just an icon button instead of an avatar pill. */
.notification-bell { position: relative; }

.notification-bell-trigger {
  position: relative;
  width: var(--tap-min);
  height: var(--tap-min);
  display: flex;
  align-items: center;
  justify-content: center;
  background: #fff;
  border: 0.5px solid transparent;
  border-radius: 50%;
  font-size: var(--fs-lg);
  color: var(--topbar-accent);
  cursor: pointer;
  transition: border-color 0.15s;
  flex-shrink: 0;
}
/* --topbar-accent, not --vn-teal: fixed-white pill in both themes (same
   reason as .home-profile-trigger:hover). */
.notification-bell-trigger:hover { border-color: var(--topbar-accent); color: var(--topbar-accent); }

.notification-bell-badge {
  position: absolute;
  top: -2px;
  right: -2px;
  min-width: 16px;
  height: 16px;
  padding: 0 var(--space-xs);
  border-radius: var(--radius-pill);
  background: var(--vn-coral);
  /* --surface-2, not #fff: --vn-coral lightens to #E2724A after dark, where
     a literal white is 3.12:1. The token flips to #242424 and gives 4.98:1
     (and stays #ffffff / 6.96:1 in light). Issue #432, same rule as teal. */
  color: var(--surface-2);
  font-size: var(--fs-xs);
  font-weight: 700;
  line-height: 16px;
  text-align: center;
}

/* renderNotificationBells() (js/notifications.js) adds this only on the
   transition from no unread notifications to some — a new thing arrived,
   worth a beat of attention — never on a count simply ticking upward while
   already visible, which would nag on every poll. */
@keyframes notification-badge-pop {
  from { transform: scale(0.4); }
}
.notification-bell-badge-pop {
  animation: notification-badge-pop var(--dur-feedback) var(--ease-out);
}

.notification-dropdown {
  display: none;
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  width: 300px;
  max-width: 85vw;
  background: var(--df-panel);
  border: 2px solid var(--df-ink);
  border-radius: var(--df-radius-card);
  box-shadow: var(--df-press-large);
  z-index: 50;
}
.notification-dropdown.open { display: block; }

.notification-dropdown-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--space-lg) var(--space-xl);
  border-bottom: 2px solid var(--df-hair);
  font-size: var(--fs-base);
  font-weight: 600;
}

.notification-mark-all {
  background: none;
  border: none;
  font-family: inherit;
  font-size: var(--fs-xs);
  font-weight: 600;
  color: var(--vn-teal);
  cursor: pointer;
  padding: 0;
}

.notification-list { max-height: 320px; overflow-y: auto; }

.notification-empty {
  padding: 1.5rem var(--space-xl);
  text-align: center;
  font-size: var(--fs-sm);
  color: var(--df-ink-soft);
}

.notification-item {
  padding: var(--space-lg) var(--space-xl);
  border-bottom: 2px solid var(--df-hair);
  cursor: pointer;
}
.notification-item:last-child { border-bottom: none; }
.notification-item:hover { background: var(--df-hair); }
.notification-item.unread { background: var(--vn-teal-light); }
.notification-item.unread:hover { background: var(--vn-teal-light); filter: brightness(0.97); }

.notification-message { font-size: var(--fs-base); color: var(--df-ink); line-height: 1.4; }
.notification-time { font-size: var(--fs-xs); color: var(--df-ink-soft); margin-top: var(--space-xs); }

/*
  .home-main holds the mobile-width three-zone body:
    ② .home-body    — fills remaining space, scrolls internally
    ③ .home-footer  — fixed, never scrolls
  On desktop this becomes a normal flowing column under the (now full-width)
  top bar — see the Desktop layout block above .stat-grid.
*/
.home-main {
  flex: 1;
  min-height: 0; /* lets this flex child shrink & scroll inside .home-screen */
  display: flex;
  flex-direction: column;
  width: 100%;
  /* Matches .app-main's documented 680px in-app cap (DESIGN.md) — this used
     to stop at 480px, leaving the screen a narrow ribbon on a tablet between
     that and the 1024px desktop breakpoint below. */
  max-width: 680px;
  margin: 0 auto;
  overflow: hidden;
}

/* Intermediate step so the 1024px jump to a grid isn't the first time the
   list changes shape — a tablet-width viewport already has the room. */
@media (min-width: 680px) {
  #my-trips-list:has(.home-trip-card) {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    /* Wider than the flat look needed — see the ≥1024px block for why the
       hard offset shadow sets the gutter floor. */
    gap: var(--space-5xl) var(--space-4xl);
  }

  #my-trips-list .home-trip-card {
    margin-bottom: 0;
  }
}

/* ② Scrollable body.
   One rule, not the two stacked declarations this used to carry — the second
   fully overrode the first's `background`, so any edit to the first was
   silently dead.

   The scroll-hint fade is a translucent ink now rather than
   rgba(0,0,0,0.06), which was a black fade on a near-black ground in dark
   mode: the "more content below" cue existed in light mode only. It is also
   only the bottom fade — the old rule painted an opaque --surface-1 band
   across the top, which on this screen's halftone ground would blank the
   dots inside the band and leave them crisp immediately below it, the same
   ghost-band artefact DESIGN.md bans backdrop-filter for. */
.home-body {
  flex: 1;
  min-height: 0;          /* critical: lets flex child shrink & scroll */
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  padding: var(--space-4xl) var(--space-4xl) var(--space-xl);
  background: linear-gradient(rgba(16, 26, 20, 0), var(--df-hair))
              bottom / 100% 24px no-repeat;
  background-attachment: scroll;
}

/* The stub-metadata / eyebrow voice: Martian Mono, uppercase, wide tracking —
   the way anything printed onto a ticket is printed. Never used for a
   sentence. */
.home-section-label {
  font-family: var(--df-mono);
  font-size: var(--fs-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  line-height: 1.3;
  color: var(--df-ink-soft);
  margin-bottom: var(--space-xl);
}

/* ── The one shout ──
   DESIGN.md allows exactly one loud element per screen, and on this one it is
   the screen's own name — until now an `sr-only` h1 with a duplicate `h2`
   section label immediately under it. Promoting it removes that duplication
   and gives the page an anchor above the card grid; the count eyebrow beside
   it is derived from the list already fetched (loadMyTrips), so it costs no
   request and states nothing the user's own data doesn't. */
.home-masthead {
  margin-bottom: var(--space-4xl);
}

.home-title {
  font-family: var(--df-display);
  font-size: clamp(2rem, 5.4vw, 3.125rem);
  font-weight: 800;
  line-height: 0.98;
  letter-spacing: -0.03em;
  text-transform: uppercase;
  text-wrap: balance;
  color: var(--df-ink);
  margin: 0;
}

.home-count {
  font-family: var(--df-mono);
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--df-brand);
  margin: var(--space-md) 0 0;
  min-height: 1.3em; /* holds its line while loading, so the grid never jumps */
}

/* A dashed keyline rather than a solid one: nothing has been cut out yet. */
.home-empty {
  text-align: center;
  color: var(--df-ink-soft);
  font-size: var(--fs-md);
  padding: var(--space-5xl) var(--space-4xl);
  border: 2px dashed var(--df-hair);
  border-radius: var(--df-radius-card);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-xs);
}
/* Was an inline opacity:0.35 on the glyph, which on the apricot ground read
   as a printing fault rather than a quiet mark. */
.home-empty i {
  font-size: var(--fs-4xl);
  color: var(--df-ink-soft);
  opacity: 0.6;
  margin-bottom: var(--space-lg);
  display: block;
}
.home-empty-title {
  font-family: var(--df-display);
  font-size: var(--fs-xl);
  font-weight: 800;
  letter-spacing: -0.02em;
  color: var(--df-ink);
  margin-bottom: var(--space-xs);
}

/* Alert Red, never Stamp Coral — coral is branded and must not read as
   danger. The keyline takes --error (which clears the 3:1 a non-text
   component needs); the message text takes the darkened text-safe step. */
.home-error {
  text-align: center;
  padding: var(--space-5xl) var(--space-4xl);
  background: var(--df-panel);
  border: 2px solid var(--error);
  border-radius: var(--df-radius-card);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-xl);
}
.home-error i { font-size: var(--fs-4xl); color: var(--error); }
.home-error div { color: var(--df-ink); font-size: var(--fs-md); }
/* Retry ships as a .btn-secondary, which is the retired system's control: a
   pale grey fill with a soft edge and a 10px radius. Left alone it was the
   one unconverted object left on the screen, sitting inside a converted
   panel — precisely the half-converted seam DESIGN.md's whole-screen rule
   exists to prevent. Scoped here rather than rewriting .btn-secondary app-
   wide, which belongs to the surfaces that have not converted yet. */
.home-error button {
  min-width: 140px;
  min-height: var(--tap-min);
  padding: 0 22px;
  background: var(--df-panel);
  color: var(--df-ink);
  border: 2px solid var(--df-ink);
  border-radius: var(--radius-pill);
  box-shadow: var(--df-press);
  font-family: var(--df-sans);
  font-size: var(--fs-md);
  font-weight: 600;
  cursor: pointer;
  transition: transform var(--dur-feedback) var(--df-ease),
              box-shadow var(--dur-feedback) var(--df-ease);
}
.home-error button:hover {
  transform: translate(2px, 2px);
  box-shadow: var(--df-press-hover);
}
.home-error button:active {
  transform: translate(4px, 4px);
  box-shadow: var(--df-press-active);
}
.home-error button:disabled {
  opacity: 0.55;
  transform: none;
  box-shadow: var(--df-press);
  cursor: default;
}

/* ── Trip cards: the ticket stub ──
   A trip card is a boarding-pass stub, which is why DESIGN.md's `stub`
   component fits it without any DOM change: Cut Stock, a 2px ink keyline, a
   12px radius, a hard offset shadow, and — the part that makes it a stub
   rather than a card — a 2px dashed rule across it where the ticket was
   torn. The existing markup already splits exactly there: .home-trip-card-open
   is the passenger half, .home-trip-invite the counterfoil below the
   perforation.

   The card fill was --surface-1 on a --surface-1 ground: a 1.0:1 fill behind
   a 1.21:1 border, so the boundary of the screen's primary control failed
   WCAG 1.4.11 (3:1) and effectively wasn't drawn at all. Cut Stock on the
   apricot ground plus the 2px ink keyline is 13.9:1 on the keyline. */
.home-trip-card {
  position: relative;
  display: flex;
  flex-direction: column;
  background: var(--df-panel);
  border: 2px solid var(--df-ink);
  border-radius: var(--df-radius-stub);
  box-shadow: var(--df-press);
  margin-bottom: var(--space-4xl);
  overflow: hidden; /* keeps children's own padding/hover fill inside the rounded corners */
  /* Depth is hard offset ink, so the press IS the feedback and there is no
     colour-only hover: the stub moves into its own shadow. Transitions run
     the feedback tier — this is glanced at mid-tap, not lingered on. */
  transition: transform var(--dur-feedback) var(--df-ease),
              box-shadow var(--dur-feedback) var(--df-ease);
}
.home-trip-card:hover {
  transform: translate(2px, 2px);
  box-shadow: var(--df-press-hover);
}
.home-trip-card:active,
.home-trip-card:has(.home-trip-card-open:active) {
  transform: translate(4px, 4px);
  box-shadow: var(--df-press-active);
}

/* The stub you were last holding sits proudest on the pile — a deeper offset,
   not a coloured wash. The old rgba(15,110,86,0.04) fill was the *previous*
   brand teal (#0F6E56, not the current #10702A) and, being a literal rgba,
   never flipped for dark mode: at 4% over #1a1a1a it was invisible, so the
   active card lost its only fill signal after dark. */
.home-trip-card-active {
  box-shadow: var(--df-press-large);
}
.home-trip-card-active:hover {
  box-shadow: var(--df-press-hover);
}

/* A skeleton is not pressable — it must not offer the stub's press. */
.home-trip-card-skeleton { cursor: default; }
.home-trip-card-skeleton:hover,
.home-trip-card-skeleton:active {
  transform: none;
  box-shadow: var(--df-press);
}

/* The card's main action — everything except Invite below. A real <button>
   (not the whole card as a <div onclick>) so it's reachable by keyboard and
   announced to a screen reader; Invite is a sibling button rather than
   nested inside this one, since a <button> may not contain another. */
.home-trip-card-open {
  display: block;
  width: 100%;
  /* Grid rows stretch cards to equal height (≥680px), so the passenger half
     has to absorb the slack — otherwise the tear rule and the counterfoil
     float in the middle of a tall stub instead of sitting at its foot. */
  flex: 1;
  background: none;
  border: none;
  appearance: none;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
  /* The stub's body padding, with the extra foot DESIGN.md gives a print —
     optical, so the tear rule below doesn't crowd the metadata above it. */
  padding: var(--space-3xl) var(--space-3xl) var(--space-4xl);
  user-select: none;
}
/* No opacity dip — the stub's own press (above) is the feedback, and two
   overlapping acknowledgements of one tap read as a flicker. */
.home-trip-card-open:disabled { opacity: 0.55; cursor: default; }

.home-trip-card-top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  margin-bottom: var(--space-md);
}

/* Title tier — DESIGN.md names "stub names" as one of its call sites
   verbatim. Sentence case, not uppercase: the shout stops at headline tier. */
.home-trip-name {
  font-family: var(--df-display);
  font-size: var(--fs-lg);
  font-weight: 800;
  line-height: 1.15;
  letter-spacing: -0.02em;
  color: var(--df-ink);
  flex: 1;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.home-trip-arrow {
  color: var(--df-ink-soft);
  font-size: var(--fs-lg);
  flex-shrink: 0;
}

/* The kicker pill: outlined only — a 2px currentColor keyline, transparent
   fill, Martian Mono. The outline is what carries it, not the fill.
   Deliberately NOT the filled green pill this used to be: green is the
   system's one signal for "you can press this", and a status badge that
   isn't pressable spends it on decoration (the Pressable Green Rule).
   currentColor means it also just works on a photo stub, where the
   surrounding ink flips to the print's own foreground. */
.trip-card-current {
  background: transparent;
  color: inherit;
  border: 2px solid currentColor;
  font-family: var(--df-mono);
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  padding: 3px var(--space-md) 2px;
  border-radius: var(--radius-pill);
  white-space: nowrap;
  flex-shrink: 0;
}

/* Issue #553 §8 — "Pending-request count on the My Trips card, host/co-host
   only." Filled amber, unlike .trip-card-current's outline-only kicker
   above: this one IS actionable (tap the card, go approve someone), so it
   earns the same amber-light/amber-text notice pairing used for
   pending-status callouts throughout this file. */
.home-trip-pending-badge {
  background: var(--vn-amber-light);
  color: var(--vn-amber-text);
  font-family: var(--df-mono);
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: 0.05em;
  padding: 3px var(--space-md) 2px;
  border-radius: var(--radius-pill);
  white-space: nowrap;
  flex-shrink: 0;
}

/* City + country, in the app's own words rather than only feeding a photo
   search — the card used to fetch this and throw it away (see
   applyDestinationPhoto below), so two trips to different cities with
   similar names were indistinguishable without the photo loading. */
.home-trip-destination {
  display: block;
  font-family: var(--df-sans);
  font-size: var(--fs-md);
  font-weight: 600;
  color: var(--df-ink-soft);
  margin-bottom: var(--space-xl);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.home-trip-destination i {
  font-size: var(--fs-sm);
  margin-right: var(--space-xs);
  vertical-align: middle;
}

/* Stub metadata takes the ticket voice: Martian Mono, uppercase, wide
   tracking — printed onto the stub the way a gate and a seat number are.
   Martian Mono is a wide face, so this wraps to two rows on a narrow card by
   design rather than being shrunk below the label tier. */
.home-trip-meta {
  display: flex;
  gap: var(--space-sm) var(--space-xl);
  font-family: var(--df-mono);
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  line-height: 1.3;
  color: var(--df-ink-soft);
  flex-wrap: wrap;
}
.home-trip-meta i {
  font-size: var(--fs-sm);
  margin-right: var(--space-xs);
  vertical-align: -2px;
}

/* ── Destination photo (Pexels, best-effort — see applyDestinationPhoto in
   js/utils.js). Painted as a ::before layer rather than directly on the card
   so the gradient overlay can sit between the photo and the card's own
   content without the content needing its own background. Direct children
   get position:relative/z-index:1 so they (.home-trip-card-open and
   .home-trip-invite) paint above the photo regardless of DOM order. The
   card itself (name, destination, dates) always renders first — this class
   + its ::before only get added once the trip list is already on screen and
   the photo has actually finished downloading (applyDestinationPhoto), then
   fades in via the -visible modifier rather than popping in. */
/* ⚠️ The scrim's alpha floor is a contrast guarantee, not a taste setting.
   This used to open at rgba(10,20,20,.15) — a 15% wash — under white 17px
   text. Against a bright photo (sky, beach, snow: the dominant Pexels result
   for a "<city> cityscape" query) that measured 1.4:1, and against a dark one
   ~9.6:1, so whether the card's own title was legible was decided by which
   photo came back. That is WCAG 1.4.3 failing on the one string that says
   which trip the card is.

   0.64 is the solved floor: compositing --df-ink over a pure-white photo at
   0.64 lands the background at ~#666, where white text is 5.74:1 — so AA
   holds for *any* image, with no luminance test and no per-photo branching.
   It deepens to 0.90 down the card, where the metadata sits. Do not lower
   either stop.

   The halftone screen over the top is what makes this printed matter rather
   than a photo with a gradient on it: the photo is screened onto the stub in
   the same dot grid as the page ground, so it reads as an ink pass and not
   as glass. It also only ever darkens, so it cannot weaken the floor above. */
.home-trip-card-photo::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background-image:
    var(--df-halftone),
    linear-gradient(180deg, rgba(16, 26, 20, 0.64), rgba(16, 26, 20, 0.90)),
    var(--trip-card-photo);
  background-size: 6px 6px, auto, cover;
  background-position: center;
  z-index: 0;
  opacity: 0;
  transition: opacity var(--dur-view) var(--ease-out);
}
.home-trip-card-photo.home-trip-card-photo-visible::before {
  opacity: 1;
}
.home-trip-card-photo > * {
  position: relative;
  z-index: 1;
}
/* Text treatment waits for -visible — otherwise the print's foreground would
   sit on the plain Cut Stock for the fade-in's duration (or indefinitely, if
   the image never loads). Lamplight rather than a literal #fff: it is the
   system's warm ink, and it is what the .trip-card-current kicker's
   `currentColor` outline picks up so the pill needs no photo-specific rule.
   The whole subtree flips together, which is why --df-ink-soft is restated
   at full strength — the scrim is deep enough that a muted tier would be
   buying illegibility for nothing. */
.home-trip-card-photo-visible.home-trip-card-photo {
  color: #ffeeda;
}
.home-trip-card-photo-visible.home-trip-card-photo .home-trip-name,
.home-trip-card-photo-visible.home-trip-card-photo .home-trip-destination,
.home-trip-card-photo-visible.home-trip-card-photo .home-trip-meta,
.home-trip-card-photo-visible.home-trip-card-photo .home-trip-arrow,
.home-trip-card-photo-visible.home-trip-card-photo .home-trip-invite {
  color: #ffeeda;
}
.home-trip-card-photo-visible.home-trip-card-photo .home-trip-invite {
  border-top-color: rgba(255, 238, 218, 0.45);
}
.home-trip-card-photo-visible.home-trip-card-photo .home-trip-invite:hover {
  background: rgba(255, 238, 218, 0.12);
  color: #ffffff;
}
/* The copied confirmation needs its own photo-stub step, not the pale mint
   fill it takes on Cut Stock: these photo rules out-rank .home-trip-code-copied
   on `color` (0,3,0 vs 0,1,0) but not on `background`, so the unscoped pair
   would have put lamplight text on a #E3FBEA fill — 1.05:1. */
.home-trip-card-photo-visible.home-trip-card-photo .home-trip-code-copied,
.home-trip-card-photo-visible.home-trip-card-photo .home-trip-code-copied:hover {
  background: rgba(255, 238, 218, 0.20);
  color: #ffffff;
}

/* ── Invite — was a permanent "TRIP CODE / ABC123XYZ" row on every card
   forever, a third of the card's height for a job that's actually done once
   per trip. Demoted to a compact copy-to-clipboard action; the code itself
   is still readable in-trip (Settings tab, #settingsTripCode) once someone
   has joined, so nothing that needed the code at rest is lost.

   It is now the stub's counterfoil: it spans the full width and is separated
   from the passenger half by a 2px dashed rule — the perforation the ticket
   tears along, which is DESIGN.md's literal reproduction rather than a scale
   step. That tear is why it lost its side margins and its own rounded box; a
   stub is one piece of card with a line across it, not two stacked
   components. */
.home-trip-invite {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-md);
  width: 100%;
  min-height: var(--tap-min);
  padding: var(--space-xl) var(--space-4xl);
  background: none;
  border: none;
  border-top: 2px dashed var(--df-hair);
  border-radius: 0;
  appearance: none;
  font-family: var(--df-mono);
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--df-ink-soft);
  cursor: pointer;
  transition: background var(--dur-feedback) var(--df-ease),
              color var(--dur-feedback) var(--df-ease);
}
.home-trip-invite i {
  font-size: var(--fs-md);
  /* Martian Mono's uppercase sits high against the icon's optical centre. */
  margin-top: -1px;
}
/* Hover moves it to the brand green — this is a control, so green is exactly
   the right signal here (unlike the status pill above, which gave it up). */
.home-trip-invite:hover {
  background: var(--vn-teal-light);
  color: var(--vn-teal);
}

/* Was three literal rgba(15,110,86,…) values — the *previous* brand teal
   (#0F6E56), not the current #10702A — which additionally never flipped for
   dark mode, so the confirmation was invisible after dark. --vn-teal-light
   already ships a correct dark step. The !important flags went with them:
   they existed only to out-rank .home-trip-invite:hover, which a class
   ordered after it does not need. */
.home-trip-code-copied,
.home-trip-code-copied:hover {
  background: var(--vn-teal-light);
  color: var(--vn-teal);
}

/* copyTripCode() (js/auth.js) swaps the button's whole innerHTML to a check
   icon + "Copied!" — that <i> is a freshly-inserted element each call, so it
   plays this animation on its own with no JS retrigger needed (unlike the
   .panel-enter remove/reflow/add pattern, which exists because that element
   persists across renders). One small pop, not a bounce — same reasoning as
   --ease-out above: this is glanced at mid-tap, not a moment to linger on. */
@keyframes home-invite-check-pop {
  from { opacity: 0; transform: scale(0.5); }
}
.home-trip-code-copied i {
  animation: home-invite-check-pop var(--dur-feedback) var(--ease-out);
}

/* ── Home entrance — trip cards land with a short cascade on a genuine
   arrival at My Trips (login, session restore, returning from a trip or
   Discover). This is the screen the app opens on, reopened far more often
   than the itinerary tab, so — unlike itin-trace (css ~4940) — it stays
   fast and uses plain fade+rise rather than the route-trace's spine-drawing
   language; a bigger production here would read as page-load choreography
   on a screen visited many times a session, not as a rehearsed moment.
   Armed only by showMyTrips(), consumed once by loadMyTrips()'s next
   render — the background refreshes loadMyTrips() also serves (accepting
   an invite, deleting a trip while sitting on this screen) must not replay
   it. See _homeTripsTraceArmed in js/auth.js, same arm/consume split as
   _itinTraceArmed. Stagger caps at the 6th card — a long trip list arrives
   as two waves, not a card-by-card recital. */
@keyframes home-card-enter {
  from { opacity: 0; transform: translateY(8px); }
}
.home-trips-trace .home-trip-card,
.home-trips-trace .home-empty {
  animation: home-card-enter var(--dur-state) var(--ease-out) both;
  animation-delay: calc(var(--trace-i, 0) * 30ms);
}

/* ③ Footer — action cards.
   Opaque on the panel stock, not translucent: a bar with a backdrop-filter
   over the halftone ground blurs the dots inside the bar and leaves them
   crisp immediately below it, which reads as a ghost band. */
.home-footer {
  flex-shrink: 0;
  padding: var(--space-3xl) var(--space-4xl) var(--space-4xl);
  border-top: 2px solid var(--df-ink);
  background: var(--df-panel);
}

.home-footer .home-section-label {
  margin-bottom: var(--space-xl);
}

/* Issue #35 — footer legal notice */
.home-legal-notice {
  text-align: center;
  margin-top: var(--space-3xl);
  font-family: var(--df-mono);
  font-size: var(--fs-xs);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--df-ink-soft);
}
.home-legal-notice a { color: var(--df-ink-soft); text-decoration: underline; }
.home-legal-notice span { margin: 0 var(--space-sm); }

.home-action-cards {
  display: flex;
  gap: var(--space-xl);
}

/* These are stubs you press, so they take the stub's geometry and DESIGN.md's
   press states: hover translates 2px into a halved offset, active seats the
   object fully into the page. No colour-only hover — the press is the
   feedback.

   The three used to hover to three different hues (indigo / teal / coral),
   which spends the palette's colour *assignments* on decoration: green means
   pressable, coral means stamped, amber means routed. They also read as three
   co-equal actions when they are not. So the roles split the way the system's
   buttons do — New Trip is the primary and carries the green fill, the other
   two are secondary Cut Stock — which is both on-vocabulary and an honest
   hierarchy. (The indigo hover lived in css/discover.css and was removed
   there; see the note at .home-action-discover.) */
.home-action-card {
  flex: 1;
  min-height: var(--tap-min);
  background: var(--df-panel);
  border: 2px solid var(--df-ink);
  border-radius: var(--df-radius-stub);
  box-shadow: var(--df-press);
  padding: var(--space-xl) var(--space-md);
  text-align: center;
  appearance: none;
  font-family: var(--df-sans);
  color: var(--df-ink);
  cursor: pointer;
  transition: transform var(--dur-feedback) var(--df-ease),
              box-shadow var(--dur-feedback) var(--df-ease);
  user-select: none;
}
.home-action-card:hover {
  transform: translate(2px, 2px);
  box-shadow: var(--df-press-hover);
}
.home-action-card:active {
  transform: translate(4px, 4px);
  box-shadow: var(--df-press-active);
}

/* The one primary. --df-brand-fg, never a literal white: --vn-teal is a deep
   green in light mode and a bright mint after dark, where white on it is
   1.31:1 (issue #432). */
.home-action-create {
  background: var(--vn-teal);
  color: var(--df-brand-fg);
}
.home-action-create .home-action-icon,
.home-action-create .home-action-label,
.home-action-create .home-action-desc { color: inherit; }

.home-action-icon  { font-size: var(--fs-2xl); margin-bottom: var(--space-sm); display: block; }
/* Bricolage at 800 is a wider face than the system stack this replaced, and
   three of these share one row — so the label holds at --fs-md rather than
   taking the title tier's 17px, which pushed "Discover" onto two lines
   inside a ~67px column at 320px. */
.home-action-label {
  font-family: var(--df-display);
  font-size: var(--fs-md);
  font-weight: 800;
  line-height: 1.15;
  letter-spacing: -0.02em;
  margin-bottom: var(--space-2xs);
  color: var(--df-ink);
}
.home-action-desc  { font-size: var(--fs-sm); color: var(--df-ink-soft); }

/* Below this, three cards sharing one row leave too little width for the
   description line to read as a line — it wraps to three lines of 12px
   text instead. Icon + label alone still say what each card does. Raised
   from 380px with the conversion: Figtree at --fs-sm is a touch wider than
   the system stack it replaced, so "Explore shared trips" started wrapping
   before the old threshold rather than at it. */
@media (max-width: 420px) {
  .home-action-desc { display: none; }
}

/* Desktop: the mobile 3-zone phone-frame layout relaxes into a normal
   flowing page — .home-screen/.home-main stop being height-constrained,
   and the content column widens to match .app's (see the Desktop layout
   block above .stat-grid), now centered since there's no sidebar. */
@media (min-width: 1024px) {
  .home-screen {
    height: auto;
    /* ...but never shorter than the viewport. Dropping the height constraint
       lets a short list end mid-window, and with a painted ground rather than
       the old flat --surface-1 that leaves a visible band of the document's
       own background under the page. */
    min-height: 100vh;
    min-height: 100dvh;
    overflow: visible;
  }

  .home-main {
    flex: none;
    /* Unlike .app-main's desktop rule, this screen never grows a sidebar to
       eat the extra width, so it gets its own cap (the landing page's 1120px
       content measure, DESIGN.md) instead of running edge-to-edge. */
    max-width: 1120px;
    height: auto;
    overflow: visible;
    padding: 1.5rem 2.5rem;
  }

  .home-body {
    background: none; /* no inner scroll region left to hint at */
  }

  /* The hard offset shadow lives outside the stub's own box, so the grid gap
     has to clear it or the next column sits on top of its neighbour's
     shadow — the one place this system needs more gutter than the flat
     look did. */
  #my-trips-list:has(.home-trip-card) {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
    gap: var(--space-5xl) var(--space-4xl);
  }

  #my-trips-list .home-trip-card {
    margin-bottom: 0;
  }

  /* No inner scroll region on desktop, so the footer stops being a docked
     bar and becomes what it actually is: a panel of three actions. */
  .home-footer {
    border: 2px solid var(--df-ink);
    border-radius: var(--df-radius-card);
    box-shadow: var(--df-press-large);
    margin-top: var(--space-5xl);
  }
}

/* ==================== Day-Box Itinerary ==================== */
/* Chronological day-timeline — just the scrollable track (issue #73 moved
   "All" + the zoom controls down into #itin-category-filter below, so this
   row is the timeline alone). */
#itin-date-strip {
  display: flex;
  align-items: center;
  padding: 0 0 var(--space-md);
  margin-bottom: var(--space-sm);
}

/* Combined filter row: [All] [zoom -/+] [icon-only category chips].
   One scrolling rail, not a wrapping block. Wrapping cost the row a second
   and sometimes third line directly above the first day box — ~280px of
   mostly-empty height on a phone before any itinerary content, and at 1024px
   it degraded into four ragged rows of one to three chips. A rail keeps the
   controls on one line at every width and lets the overflow scroll, which is
   also what the timeline directly above it already does.
   overflow-y is hidden rather than left to compute to auto: the row is at
   least --tap-min tall (the date chip and zoom buttons set that), so the
   32px chips' 44px ::before hit zones sit inside it and are not clipped. */
#itin-category-filter {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  flex-wrap: nowrap;
  overflow-x: auto;
  overflow-y: hidden;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  margin-bottom: var(--space-md);
  /* Issue #545 follow-up (design critique): the hidden scrollbar left no
     signal at all that "Other"/"Transport" exist off the right edge of a
     390px viewport — a filter the user can't see is a filter that doesn't
     exist. A soft edge fade, not a hard clip: on a row with no overflow the
     last chip still sits flush against the container edge, so this only
     ever reads as "more content" cue, never as content going missing. */
  mask-image: linear-gradient(to right, black calc(100% - 28px), transparent 100%);
  -webkit-mask-image: linear-gradient(to right, black calc(100% - 28px), transparent 100%);
}
#itin-category-filter::-webkit-scrollbar { display: none; }

.date-chip {
  flex-shrink: 0;
  /* Issue #447 — measured 44x30, so it grows rather than faking the hit area:
     nothing in the date strip depends on its height. */
  min-height: var(--tap-min);
  padding: var(--space-sm) var(--space-2xl);
  border-radius: var(--radius-pill);
  border: 2px solid var(--df-ink);
  background: var(--df-panel);
  color: var(--df-ink-soft);
  font-size: var(--fs-sm);
  font-weight: 500;
  cursor: pointer;
  font-family: inherit;
  transition: border-color 0.15s, background 0.15s, color 0.15s;
  white-space: nowrap;
}
.date-chip:hover, .date-chip.active {
  border-color: var(--vn-teal);
  background: var(--vn-teal-light);
  color: var(--vn-teal);
}
.date-chip-ideas:hover, .date-chip-ideas.active {
  border-color: var(--vn-amber);
  background: var(--vn-amber-light);
  color: var(--vn-amber-text);
}

.itin-timeline-zoom {
  display: flex;
  gap: var(--space-2xs);
  flex-shrink: 0;
}
/* Issue #447 — was a hard-coded 24x24, 45% of --tap-min. Grown rather than
   given .stop-menu-btn's ::before expansion: the two buttons sit --space-2xs
   apart, so two 44px zones centered 26px apart would overlap by 18px and a tap
   on "zoom out" would land on "zoom in" (the same collision .discover-like-btn
   documents). At the token they also match the .date-chip beside them in
   #itin-category-filter. */
.itin-timeline-zoom-btn {
  min-width: var(--tap-min);
  min-height: var(--tap-min);
  display: flex;
  align-items: center;
  justify-content: center;
  border: 2px solid var(--df-ink);
  border-radius: var(--df-radius-stub);
  background: var(--df-panel);
  color: var(--df-ink-soft);
  font-size: var(--fs-base);
  cursor: pointer;
  font-family: inherit;
}
.itin-timeline-zoom-btn:hover { background: var(--df-hair); }
.itin-timeline-zoom-btn:disabled { opacity: 0.35; cursor: default; }

.itin-timeline-track-wrap {
  flex: 1;
  min-width: 0;
  overflow-x: auto;
  overflow-y: hidden;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  /* Each tick is horizontally centered on its position via translateX(-50%)
     (see .itin-timeline-tick below), so the first tick's dot+label extend
     left of the track's own x:0 edge, and the last tick's extend right of
     its right edge. Without this padding that overflow lands outside the
     scrollable range (scrollLeft can never go negative), so the first tick
     rendered permanently clipped no matter how far you scrolled. */
  padding: 0 var(--space-4xl);
}
.itin-timeline-track-wrap::-webkit-scrollbar { display: none; }

.itin-timeline-track {
  position: relative;
  height: 44px;
}

.itin-timeline-line {
  position: absolute;
  top: 14px;
  left: 0;
  right: 0;
  height: 2px;
  border-radius: 1px;
  background: var(--df-hair);
}

.itin-timeline-tick {
  position: absolute;
  top: 0;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-xs);
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  font-family: inherit;
}
/* Measured 27x38 — a day picker, and one of the densest rows on the tab.
   The visible tick has to stay narrow (its x position IS the date), so this
   is the .stop-menu-btn ::before expansion with one addition: the width is
   capped at --itin-day-pitch, the px-per-day the strip was rendered at
   (published by renderDateStrip(), js/itinerary.js). Neighbouring ticks are
   exactly one pitch apart, so capping there is what keeps two 44px zones
   from overlapping and sending a tap to the wrong day on a long trip or at
   the tightest zoom — the same collision .itin-timeline-zoom-btn above
   documents and dodges by growing instead. Height is uncapped: nothing sits
   above or below a tick. */
.itin-timeline-tick::before {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: min(var(--itin-day-pitch, 100%), max(var(--tap-min), 100%));
  height: max(var(--tap-min), 100%);
  transform: translate(-50%, -50%);
}

.itin-timeline-dot {
  width: 9px;
  height: 9px;
  margin-top: 9.5px; /* centers the dot on the line at top:14px */
  border-radius: 50%;
  background: var(--df-panel);
  border: 2px solid var(--df-ink);
  transition: background 0.15s, border-color 0.15s, transform 0.15s;
}
.itin-timeline-tick.active .itin-timeline-dot {
  background: var(--vn-teal);
  border-color: var(--vn-teal);
  transform: scale(1.15);
}

.itin-timeline-tick-label {
  font-size: var(--fs-xs);
  color: var(--df-ink-soft);
  white-space: nowrap;
}
.itin-timeline-tick.active .itin-timeline-tick-label {
  color: var(--vn-teal);
  font-weight: 600;
}

/* The draggable range anchor/scrubber — always present on the track (see
   renderDateStrip() in itinerary.js), not just after a day's been clicked,
   so there's never a click required before it can be dragged. Its body
   moves the whole range; the two handles at each edge resize it, growing or
   shrinking which days are included — see initTimelineRangeDrag(). Pointer
   Events make all three draggable with touch, mouse, or pen alike;
   touch-action:none on the handles stops touch-dragging them from also
   panning the page. */
.itin-timeline-range {
  position: absolute;
  /* Hugs just the dot/line row (dots sit at 9.5-18.5px within the 44px-tall
     track) rather than the taller box used previously, which reached down
     far enough to paint over the day labels sitting right below the dots. */
  top: 8px;
  height: 10px;
  min-width: 2px;
  background: var(--vn-teal-light);
  border: 2px solid var(--vn-teal);
  border-radius: var(--radius-pill);
  cursor: grab;
}
.itin-timeline-range.dragging { cursor: grabbing; }

.itin-timeline-range-handle {
  position: absolute;
  top: 50%;
  width: 16px;
  height: 16px;
  margin-top: calc(var(--space-md) * -1);
  border-radius: 50%;
  background: var(--vn-teal);
  border: 2px solid var(--surface-2);
  box-shadow: var(--df-press-hover);
  cursor: ew-resize;
  touch-action: none;
  z-index: 2;
  transition: transform 0.1s;
}
.itin-timeline-range-handle:hover { transform: scale(1.1); }
.itin-timeline-range-handle.dragging {
  transform: scale(1.15);
  box-shadow: var(--df-press);
}
.itin-timeline-range-handle-start { left: -8px; }
.itin-timeline-range-handle-end { right: -8px; }
/* Issue #447: gap widened from --space-sm (6px) to --space-xl (12px) so the
   chips' 44px ::before hit zones (below) abut instead of overlapping —
   32px chip + 12px gap = a 44px pitch on both axes, including between wrapped
   rows. Anything tighter and a tap near a boundary lands on the neighbouring
   chip, which is why .discover-like-btn (css/discover.css) caps its own zone
   at 32px instead. */
/* nowrap: this group is one item inside #itin-category-filter's rail, so it
   scrolls with the rail rather than wrapping inside it. */
.itin-cat-chips {
  display: flex;
  gap: var(--space-xl);
  flex-wrap: nowrap;
}
/* Icon-only (issue #73) — text labels dropped in favor of the more
   noticeable Tabler icons in PLACE_TYPES (js/state.js), so each chip is a
   small fixed-size circle rather than a padded label pill. */
.itin-cat-chip {
  flex-shrink: 0;
  width: 32px;
  height: 32px;
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  border: 2px solid var(--df-ink);
  background: var(--df-panel);
  color: var(--df-ink-soft);
  font-size: 15px;
  cursor: pointer;
  font-family: inherit;
  transition: border-color 0.15s, background 0.15s, color 0.15s;
  position: relative;
}
/* Issue #447 — the visible circle stays 32px (six icon-only chips read as a
   set; inflating them to 44px turns the filter row into the loudest thing on
   the tab), so the hit area is expanded with the same ::before trick as
   .stop-menu-btn. Depends on .itin-cat-chips' 12px gap — see there. */
.itin-cat-chip::before {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: max(var(--tap-min), 100%);
  height: max(var(--tap-min), 100%);
  transform: translate(-50%, -50%);
}
.itin-cat-chip:hover, .itin-cat-chip.active {
  border-color: var(--vn-indigo);
  background: var(--vn-indigo-light);
  color: var(--vn-indigo);
}
.itin-cat-chip.clear:hover {
  border-color: var(--error);
  background: var(--error-light);
  color: var(--error);
}

/* ==================== Itinerary calendar view (issue #159) ==================
   Multi-day time grid — days as columns, hours as rows. Sticky day headers
   (top) + sticky hour gutter (left) via position:sticky inside one scrolling
   grid, matching every itinerary drag control's Pointer Events pattern
   (initTimelineRangeDrag/onDayMapResizeStart) rather than SortableJS — that
   library is for list reordering, not 2-D time positioning. Column width is
   fixed (sized to fit one page — see _calColWidth() in js/itinerary.js — set
   inline on .itin-cal-grid's grid-template-columns), not part of what zoom
   (vertical only) affects. Issue #452: a page renders exactly as many
   columns as it has real days, never padded to fill the box — see
   _calendarPages() in js/itinerary.js for how a page's days are chosen
   (trip-leg boundaries, then container width). */
/* Zoom control row (see zoomCalendar()/resetCalendarZoom() in js/itinerary.js)
   — reuses the date-strip's own .itin-timeline-zoom-btn for the -/+ buttons. */
.itin-cal-zoom-row {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  justify-content: flex-end;
  margin-bottom: var(--space-sm);
}
/* Prev/next page pagination (see calPrevPage()/calNextPage() in
   js/itinerary.js) — only rendered when _calendarPages() produced more than
   one page (container width, a trip-leg boundary, or both — issue #452).
   Pushed to the opposite end of .itin-cal-zoom-row from the zoom controls
   via margin-right:auto, so the row still reads correctly (zoom controls
   flush right) on the common case where pagination isn't shown. */
.itin-cal-pagination {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  margin-right: auto;
  /* Issue #452 — the label can now carry a destination name ("Tokyo · Day
     1-2"), longer and more variable-width than the old fixed "Week N of M".
     Without this, .itin-cal-page-label's own min-width:0 (below) has nothing
     to shrink against — a flex item's default min-width is its content size,
     which would push this whole row wider than .itin-cal-zoom-row and force
     the zoom buttons off it at a narrow width instead of the label eliding. */
  min-width: 0;
}
.itin-cal-page-btn {
  width: 24px;
  height: 24px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 2px solid var(--df-ink);
  border-radius: var(--df-radius-stub);
  background: var(--df-panel);
  color: var(--df-ink-soft);
  cursor: pointer;
  font-family: inherit;
}
.itin-cal-page-btn:hover { background: var(--df-hair); }
.itin-cal-page-btn:disabled { opacity: 0.35; cursor: default; }
.itin-cal-page-label {
  font-size: var(--fs-xs);
  font-weight: 600;
  color: var(--df-ink-soft);
  white-space: nowrap;
  /* Issue #452 — elides rather than overflows when a destination name makes
     this longer than the old fixed-format "Week N of M" ever was. min-width:0
     is what lets a flex item shrink below its content size at all; paired
     with .itin-cal-pagination's own min-width:0 above. */
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}
.itin-cal-zoom-fit-btn {
  height: 24px;
  padding: 0 var(--space-lg);
  font-size: var(--fs-xs);
  font-weight: 600;
  font-family: inherit;
  border: 2px solid var(--df-ink);
  border-radius: var(--df-radius-stub);
  background: var(--df-panel);
  color: var(--df-ink-soft);
  cursor: pointer;
}
.itin-cal-zoom-fit-btn:hover { background: var(--df-hair); }
.itin-cal-zoom-fit-btn.active {
  background: var(--vn-teal-light);
  border-color: var(--vn-teal);
  color: var(--vn-teal);
}

.itin-calendar {
  --cal-gutter-w: 48px; /* keep in sync with CAL_GUTTER_WIDTH in js/itinerary.js */
  --cal-hour-h: 56px; /* keep in sync with CAL_HOUR_HEIGHT in js/itinerary.js */
  /* The grid only ever renders one page (up to CAL_WEEK_COLS days — often
     fewer, at a trip-leg boundary or a narrow container, issues #452/#445) at
     a time — see renderItineraryCalendar() in js/itinerary.js — and _calColWidth()
     shrinks each column to fit whatever width #itinerary-content actually
     has (down to CAL_MIN_COL_WIDTH), so in the common case there's nothing
     to scroll horizontally at all; prev/next re-renders a different slice
     instead. But CAL_MIN_COL_WIDTH is a real floor, not just a cosmetic
     minimum — once the Ideas sidebar (see .itin-layout's desktop
     grid-template-columns) is expanded on a narrower desktop width, seven
     columns at that floor can genuinely no longer fit, and clipping that
     instead of scrolling to it left part of the week impossible to see.
     overflow-x:auto only ever shows a scrollbar in that genuine overflow
     case — it's a no-op the rest of the time, same as hidden was. Vertical
     scroll (tall hour ranges) stays free either way — hence overflow-y, not
     a blanket `overflow: auto` shorthand that would look identical here.
     An earlier version rendered every week in one continuous grid and used
     CSS scroll-snap + JS-driven scrollLeft to page between them; that
     turned out to be fragile in ways only a real browser exposed (a
     gutter-offset math error, then Chrome cancelling a smooth scrollTo()
     mid-animation because mandatory snap kept re-evaluating and springing
     it back to page 1) — paginating by re-render sidesteps that class of
     bugs entirely; this scrollbar is purely a fallback for the one real
     page's own content not fitting, not a substitute for that pagination. */
  overflow-x: auto;
  overflow-y: auto;
  /* Keep in sync with _calAvailableBoxHeight() in js/itinerary.js — that
     function computes the "Fit" zoom level's target height without
     measuring the live DOM, so it has to assume this exact box size. */
  max-height: min(70vh, 640px);
  border: 2px solid var(--df-ink);
  border-radius: var(--df-radius-card);
  background: var(--df-panel);
}
.itin-cal-grid {
  display: grid;
  grid-template-rows: auto auto 1fr;
  width: max-content;
  min-width: 100%;
}
.itin-cal-corner,
.itin-cal-daycol-header {
  position: sticky;
  top: 0;
  z-index: 3;
  background: var(--df-panel);
  border-bottom: 2px solid var(--df-hair);
}
.itin-cal-corner { left: 0; z-index: 4; }
.itin-cal-daycol-header {
  /* Fixed, not padding-derived — keep in sync with CAL_HEADER_ROW_HEIGHT in
     js/itinerary.js, which assumes this exact height for the "Fit" zoom
     calculation. .itin-cal-corner (no explicit height) stretches to match
     via the grid's default align-items:stretch. Issue #154 Phase 3's plan
     badge lives inside this row too — gap only, height/centering untouched,
     since CAL_HEADER_ROW_HEIGHT must never grow. */
  height: 32px;
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-xs);
  text-align: center;
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--df-ink);
  border-left: 2px solid var(--df-hair);
}
/* Lets the plan badge keep its fixed size at 72px (CAL_MIN_COL_WIDTH) by
   giving the day label somewhere to shrink to first. */
.itin-cal-daycol-label {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Issue #154 Phase 3 — compact clickable A/B flip control. A bare letter,
   not planLabel()'s "Plan A"/"Plan B" text (used only for title/aria-label):
   the 32px header has no room for it, especially at the 72px column floor. */
/* Visible box stays 16px to fit the 32px header row (CAL_HEADER_ROW_HEIGHT
   must never grow) and the 72px column floor (CAL_MIN_COL_WIDTH) — the
   ::before pseudo expands the actual tappable area instead, same technique
   as .stop-menu-btn. Capped at 24px (the WCAG AA floor, not the fuller
   --tap-min) since the column is only 72px wide and adjacent-day badges
   sit close enough that a larger zone would start overlapping them. */
.itin-cal-plan-badge {
  position: relative;
  flex: none;
  width: 16px;
  height: 16px;
  padding: 0;
  line-height: 1;
  border-radius: 4px;
  border: 1px solid currentColor;
  background: transparent;
  font-size: var(--fs-xs);
  font-weight: 700;
  cursor: pointer;
}
.itin-cal-plan-badge::before {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: max(24px, 100%);
  height: max(24px, 100%);
  transform: translate(-50%, -50%);
}
.itin-cal-plan-badge.plan-a { color: var(--plan-a-accent); background: var(--plan-a-bg); }
.itin-cal-plan-badge.plan-b { color: var(--vn-sky-text); background: var(--vn-sky-light); }
.itin-cal-allday-label,
.itin-cal-hours {
  position: sticky;
  left: 0;
  z-index: 2;
  background: var(--df-panel);
}
.itin-cal-allday-label {
  font-size: var(--fs-xs);
  color: var(--df-ink-soft);
  padding: var(--space-xs) var(--space-sm);
  border-bottom: 2px solid var(--df-hair);
  display: flex;
  align-items: center;
}
.itin-cal-allday-cell {
  /* Fixed, not min-height — keep in sync with CAL_ALLDAY_ROW_HEIGHT in
     js/itinerary.js (same "Fit" calculation reasoning as the header row
     above). Scrolls internally rather than growing the row when a day has
     more untimed stops than fit — matches how e.g. Google Calendar handles
     an overflowing all-day band instead of pushing the whole grid down. */
  height: 40px;
  box-sizing: border-box;
  overflow-y: auto;
  padding: var(--space-xs);
  display: flex;
  flex-wrap: wrap;
  align-content: flex-start;
  gap: var(--space-xs);
  border-bottom: 2px solid var(--df-hair);
  border-left: 2px solid var(--df-hair);
}
.itin-cal-chip {
  position: relative; /* anchors the ::before plan stripe below */
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  font-size: var(--fs-xs);
  padding: var(--space-2xs) var(--space-sm);
  border-radius: var(--radius-pill);
  background: var(--vn-teal-light);
  color: var(--vn-teal);
  white-space: nowrap;
  max-width: 96px;
  overflow: hidden;
  text-overflow: ellipsis;
}
.itin-cal-chip.editable { cursor: grab; touch-action: none; }
.itin-cal-chip.dragging { cursor: grabbing; opacity: 0.6; }
.itin-cal-hours {
  position: relative;
  width: var(--cal-gutter-w);
}
.itin-cal-hour-label {
  font-size: var(--fs-xs);
  color: var(--df-ink-soft);
  text-align: right;
  padding: var(--space-2xs) var(--space-sm) 0 0;
  box-sizing: border-box;
  /* 1px, not the system's usual 2px keyline — this repeats once per hour
     row (up to ~24 times per column), and the full keyline weight reads as
     visual noise across a grid this dense. Same exception as
     .itin-cal-hourline below. */
  border-top: 1px solid var(--df-hair);
}
.itin-cal-hour-label:first-child { border-top: none; }
.itin-cal-daycol-body {
  position: relative;
  border-left: 2px solid var(--df-hair);
}

/* Precise drop preview — shows exactly where a dragged block/chip/idea will
   land (real duration-derived height, snapped time), not just which day
   column it's currently over. Replaces the old whole-column tint, which
   told the user which day but never what time (see _calShowDropGhost() /
   _calHideDropGhost() in js/itinerary.js — one shared ghost node reused
   across every calendar drag source, since only one drag runs at a time). */
.itin-cal-drop-ghost {
  position: absolute;
  left: 2px;
  right: 2px;
  box-sizing: border-box;
  border: 2px dashed var(--vn-teal);
  border-radius: 4px;
  background: var(--vn-teal-light);
  color: var(--vn-teal);
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  padding: var(--space-2xs) var(--space-sm);
  font-size: var(--fs-xs);
  font-weight: 600;
  overflow: hidden;
  white-space: nowrap;
  pointer-events: none;
  z-index: 40;
}

/* Highlights the Ideas sidebar itself while a scheduled stop/chip is
   dragged over it — dropping there unschedules it (see the dropOnIdeas
   branches in js/itinerary.js's calendar drag handlers). */
#itin-ideas-sidebar.cal-drop-target {
  outline: 2px dashed var(--vn-coral);
  outline-offset: -2px;
  background: var(--vn-coral-light);
}

.itin-cal-hourline {
  position: absolute;
  left: 0;
  right: 0;
  /* 1px hairline, not the 2px keyline — see .itin-cal-hour-label above. */
  border-top: 1px solid var(--df-hair);
  pointer-events: none;
}

.itin-cal-block {
  position: absolute;
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  gap: 1px;
  /* Issue #545 follow-up (design critique) — was 4px, not on DESIGN.md's
     radius scale (pill/12/14/3px) at all; 3px (the print/stamp radius) is
     the nearest fit for a small flat block. Border bumped 1.5px -> 2px for
     the same reason — the .cat-* rules below only ever override the colour,
     never the width/style. */
  border-radius: var(--df-radius-print);
  border: 2px solid var(--vn-teal);
  border-bottom-style: solid;
  background: var(--vn-teal-light);
  padding: var(--space-xs) var(--space-sm);
  font-size: var(--fs-xs);
  line-height: 1.3; /* accessibility floor (#445) — was 1.25 */
  overflow: hidden;
  cursor: pointer;
  z-index: 1;
}
/* Estimated (category-default fallback) duration — dashed bottom edge, so
   the user can always tell which numbers are theirs (issue #159 design
   decision #2). */
.itin-cal-block.estimated { border-bottom-style: dashed; }
.itin-cal-block.editable .itin-cal-block-draghandle { cursor: grab; touch-action: none; }
.itin-cal-block.dragging { opacity: 0.55; z-index: 50; box-shadow: var(--df-press-large); }
.itin-cal-block.resizing { box-shadow: 0 0 0 2px var(--vn-teal); }

/* Issue #154 Phase 3 — plan stripe on a calendar block/chip. A ::before,
   deliberately NOT an inset box-shadow like .itin-stop-card.plan-* uses:
   .dragging/.resizing above already own box-shadow, and box-shadow is
   REPLACED rather than merged by a later-winning rule, so an inset-shadow
   stripe would vanish mid-drag and mid-resize. A pseudo-element is a
   separate box that composes with anything, including states nobody's
   written yet. Stripe only, no background wash — unlike a list-view stop
   card, a calendar block already carries its category as fill + border
   (.itin-cal-block.cat-* below), which must survive untouched. */
.itin-cal-block.plan-a::before,
.itin-cal-block.plan-b::before,
.itin-cal-chip.plan-a::before,
.itin-cal-chip.plan-b::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 3px;
  border-radius: 4px 0 0 4px;
  pointer-events: none;
}
.itin-cal-block.plan-a::before, .itin-cal-chip.plan-a::before { background: var(--plan-a-accent); }
.itin-cal-block.plan-b::before, .itin-cal-chip.plan-b::before { background: var(--vn-sky); }
/* Icon + title share one row (rather than each being its own display:block
   line) so the title always has a single, predictable row to truncate
   into — see the .size-compact/.size-micro bands below for what happens
   when the block is too short to also fit the time underneath. */
.itin-cal-block-main {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  min-width: 0;
  overflow: hidden;
}
.itin-cal-block-icon { flex: none; }
.itin-cal-block-title {
  flex: 1;
  min-width: 0;
  font-weight: 600;
  color: var(--df-ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.itin-cal-block-time {
  flex: none;
  color: var(--df-ink-soft);
  font-size: var(--fs-xs);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Size bands (applied via JS from the rendered pixel height, see
   _calBlockSizeClasses()/_applyCalBlockSizeClasses() in js/itinerary.js) —
   dropping the time line, then the icon, then tightening padding, keeps the
   title itself visible no matter how short the block is resized to, instead
   of overflow:hidden silently clipping it out along with everything else. */
.itin-cal-block.size-compact { padding: var(--space-2xs) var(--space-xs); gap: 0; }
.itin-cal-block.size-compact .itin-cal-block-time { display: none; }
.itin-cal-block.size-micro { padding: 1px var(--space-xs); font-size: var(--fs-xs); }
.itin-cal-block.size-micro .itin-cal-block-icon { display: none; }
.itin-cal-block-draghandle {
  position: absolute;
  /* Leaves a 6px strip free at both the top and bottom edges for the two
     resize handles below — otherwise this (the move handle) would sit on
     top of them and swallow every resize-start pointerdown. */
  inset: 6px 0 6px 0;
  cursor: grab;
  touch-action: none;
}
.itin-cal-block-resizer,
.itin-cal-block-resizer-top {
  position: absolute;
  left: 0;
  right: 0;
  height: 6px;
  cursor: ns-resize;
  touch-action: none;
  z-index: 1;
}
.itin-cal-block-resizer { bottom: 0; }
.itin-cal-block-resizer-top { top: 0; }
.itin-cal-block-resizer:hover,
.itin-cal-block-resizer-top:hover { background: rgba(0,0,0,0.08); }

/* Alternative half of a shared slot (issue #59 + #159 design decision #7) —
   dashed/tinted, distinct from a genuine confirmed-vs-confirmed clash. */
.itin-cal-block-alt {
  border-style: dashed;
  background: var(--df-panel);
  opacity: 0.9;
}
.itin-cal-block-alt.editable { cursor: grab; touch-action: none; }
.itin-cal-swap-btn {
  position: absolute;
  top: 2px;
  right: 2px;
  width: 16px;
  height: 16px;
  border: none;
  border-radius: 50%;
  background: var(--vn-teal);
  color: var(--surface-2);
  font-size: var(--fs-xs);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  padding: 0;
}

/* Dedicated "drop here to demote" strip (issue #159 design decision #8) —
   hidden by default, revealed only along a confirmed block's bottom edge
   while ANY stop is mid-drag (see _revealCalAltStrips() in js/itinerary.js),
   never overlapping the resizer or drag handle above it. */
.itin-cal-alt-strip {
  display: none;
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 10px;
  background: var(--vn-coral-light);
  color: var(--vn-coral);
  border-top: 1px dashed var(--vn-coral);
  font-size: var(--fs-xs);
  align-items: center;
  justify-content: center;
  z-index: 2;
}
.itin-cal-alt-strip.hover {
  background: var(--vn-coral);
  color: var(--surface-2);
}

/* Category accents on calendar blocks — same palette as .cat-pill above. */
.itin-cal-block.cat-food { border-color: var(--vn-amber); background: var(--vn-amber-light); }
/* Sky, not teal. DESIGN.md's own Category Compass note recorded this drift
   ("uses Sky for its pill badge but Trail Green for its itinerary calendar
   block — worth reconciling to one hue the next time that component is
   touched"); this is that time. It also fixed a real collision: at block
   size Trail Green #10702A and Moss Green #166534 are indistinguishable, so
   a flight and an onsen read as the same category. */
.itin-cal-block.cat-transport { border-color: var(--vn-sky); background: var(--vn-sky-light); }
.itin-cal-block.cat-shopping { border-color: var(--vn-coral); background: var(--vn-coral-light); }
.itin-cal-block.cat-hotel { border-color: var(--vn-indigo); background: var(--vn-indigo-light); }
.itin-cal-block.cat-activity { border-color: var(--vn-green); background: var(--vn-green-light); }
.itin-cal-block.cat-other { border-color: var(--df-ink-soft); background: var(--df-panel); }
.itin-cal-block.cat-attraction { border-color: var(--vn-purple); background: var(--vn-purple-light); }

/* Itinerary layout: day list + Ideas sidebar, as a CSS Grid (not flex) so
   the Ideas sidebar can be pinned level with the day list's own top edge
   rather than the top of the whole layout. Mobile default (single column)
   stacks Ideas above the filter row above the day list; the desktop
   override below switches to two columns and, critically, leaves the
   filter row's cell empty in the Ideas column — that's what pushes Ideas
   down to start level with the day list instead of the filter row. */
.itin-layout {
  display: grid;
  grid-template-columns: 1fr;
  grid-template-areas:
    "ideas"
    "filter"
    "daylist";
  gap: var(--space-2xl);
}

.itin-filter-row {
  grid-area: filter;
  /* min-width: 0 is load-bearing, and for the same reason as the one on
     #itinerary-content below. A grid item's automatic minimum is
     min-content, and #itin-category-filter inside this row is a nowrap
     scrolling rail — so its min-content is the full un-scrolled width of
     every chip. Without this the `1fr` track inflates to that width and
     drags the whole day list with it: measured 486px of layout inside a
     390px viewport, i.e. the rail stopped scrolling and started widening
     the page instead. */
  min-width: 0;
}

#itinerary-content {
  grid-area: daylist;
  display: flex;
  flex-direction: column;
  /* One day is a bigger break than one stop, so the gap between day boxes
     has to clearly beat the space *inside* one — at the old 14px it was
     barely more than a stop row's own 12px padding, which is what made a
     multi-day list read as a single undifferentiated stack of rows. */
  gap: var(--space-5xl);
  /* Grid items default to min-width:auto, so a "1fr" track (see the desktop
     .itin-layout override below) refuses to shrink below its content's
     intrinsic width unless told otherwise — and the calendar view's grid
     (.itin-cal-grid, width:max-content, sized to *all* real+dummy day
     columns at once, not just the current page) has a very large intrinsic
     width by design, so without this the whole layout blew out to fit the
     entire un-paginated calendar instead of clipping/scrolling it inside a
     properly page-width box (issue #159 follow-up). */
  min-width: 0;
}

#itin-ideas-sidebar {
  grid-area: ideas;
  border: 2px solid var(--df-ink);
  border-radius: var(--df-radius-card);
  box-shadow: var(--df-press);
  overflow: hidden;
  background: var(--df-panel);
}

/* Issue #311 — hidden until the trip has a destination (see
   renderIdeasSidebar()). :has() lets the parent grid drop the "ideas" row
   entirely rather than leaving a collapsed-to-zero track with a stray gap
   next to it, same technique the .collapsed rule below already uses. */
.itin-layout:has(#itin-ideas-sidebar.itin-ideas-hidden) {
  grid-template-areas:
    "filter"
    "daylist";
}
#itin-ideas-sidebar.itin-ideas-hidden {
  display: none;
}

/* The header row is a plain container, NOT a button — see the markup comment
   in renderIdeasSidebar(). The collapse toggle is .ideas-sidebar-toggle
   inside it, so the Generate button can be a real sibling <button> instead of
   an illegal nested one. */
.ideas-sidebar-header {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  width: 100%;
  /* Issue #447 — the whole row is one collapse toggle, measured 508x33. */
  min-height: var(--tap-min);
  /* Was --vn-amber with white text: two problems at once. Amber is the
     "food" hue in the Category Compass, so the panel meaning "unscheduled"
     was painted the colour that means "a meal" — on the largest coloured
     surface in the tab. And white on #BA7517 measured 3.72:1 (title) and
     2.58:1 (count badge), both failing WCAG AA, at the exact value DESIGN.md
     flags as text-unsafe. Neutral surface keeps the panel legible and gives
     the category hues back their monopoly on meaning. */
  background: var(--df-panel);
  color: var(--df-ink);
  border-bottom: 2px solid var(--df-hair);
  /* No vertical padding: .ideas-sidebar-toggle's own min-height is what sets
     the row height, so the header stays at exactly --tap-min instead of
     --tap-min plus its own padding. */
  padding: 0 var(--space-2xl);
}

/* Takes the whole row minus the Generate button, so the click target is the
   full header the way it was when the header itself was the button. */
.ideas-sidebar-toggle {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  min-height: var(--tap-min);
  cursor: pointer;
}

.ideas-sidebar-title {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  font-size: var(--fs-base);
  font-weight: 700;
  letter-spacing: 0.02em;
}

/* Drawn from the same Tabler set as every other icon in this panel, replacing
   a 💡 emoji — an emoji renders in its own vendor palette at its own optical
   weight, so it never sat in the same family as the ti-* glyphs 40px below it
   in the very same list. Quiet by design: the header names the panel, the
   Generate button beside it is the row's one accent. */
.ideas-sidebar-bulb {
  font-size: var(--fs-lg);
  color: var(--df-ink-soft);
}

.ideas-sidebar-count {
  font-size: var(--fs-xs);
  /* Follows the header off amber: a white wash only reads as a badge on a
     saturated fill, and on the neutral header it measured 2.58:1. */
  background: var(--df-panel);
  border: 2px solid var(--df-ink);
  color: var(--df-ink);
  border-radius: var(--radius-pill);
  padding: 1px var(--space-md);
}

/* Issue #418 — #377 removed the History button that used to sit between
   Generate Ideas and this chevron, leaving it as the lone second icon next
   to Generate and reading as an orphaned leftover control. It isn't one —
   it's the open/closed indicator for the collapse toggle, and it predates
   both Idea Pool and History. Muted so it reads as a quiet state indicator
   rather than a second equal-weight action; it now also sits *inside*
   .ideas-sidebar-toggle, directly after the title it discloses, instead of
   at the far right where it read as a sibling of the Generate button. */
.ideas-sidebar-chevron {
  flex-shrink: 0;
  font-size: var(--fs-base);
  color: var(--df-ink-soft);
}

.ideas-sidebar-body {
  display: flex;
  flex-direction: column;
}

/* Ideas render as distinct bordered/shadowed cards with gaps between them
   (rather than the flat divided-row list #itinerary-content's own stops
   use) so each one reads as a separate, pick-up-able block you drag onto a
   day — reinforced by a lift on hover/while-dragging. Scoped to
   #itin-ideas-sidebar so the regular day-box stop rows are untouched. */
#itin-ideas-sidebar .day-box-stops {
  gap: var(--space-md);
  padding: var(--space-lg);
}

#itin-ideas-sidebar .itin-stop-card {
  border: 2px solid var(--df-ink);
  border-radius: var(--df-radius-stub);
  background: var(--df-panel);
  box-shadow: var(--df-press);
  transition: box-shadow 0.15s, transform 0.15s;
}
/* Overrides the base .itin-stop-card:last-child rule, which drops the
   border-bottom for the last row of a connected list — not applicable here
   since every idea is already its own fully-bordered card. */
#itin-ideas-sidebar .itin-stop-card:last-child {
  border: 2px solid var(--df-ink);
}
#itin-ideas-sidebar .itin-stop-card:hover {
  box-shadow: var(--df-press-hover);
  transform: translateY(-1px);
}
#itin-ideas-sidebar .itin-stop-card .stop-draghandle.sortable-chosen,
#itin-ideas-sidebar .stop-summary-row:has(.sortable-chosen) {
  cursor: grabbing;
}
#itin-ideas-sidebar .itin-stop-card:has(.sortable-drag) {
  box-shadow: var(--df-press-hover);
  transform: rotate(-1deg);
}

/* ── Itinerary route trace ────────────────────────────────────────────
   The one authored sequence on the trip screen. Entering the Itinerary, the
   day list assembles the way the day itself runs: the day box arrives, then
   its stops land in chronological order, and the spine between two
   consecutive stops (.stop-connector::before — the mark that turns the time
   column into an axis) draws downward into the next one. It is the route
   being traced, not a list fading in, and it is the reason the itinerary is
   the tab this app opens on.

   Bounded on purpose:
   - Only a genuine entry replays it — opening the trip, arriving on the tab
     from another tab, or switching back from Calendar view. NOT a re-render
     after an edit, a drag, a filter, or the FX-rate refresh, all of which
     call renderItinerary() too. See _itinTraceArmed in js/itinerary.js.
   - Only the first two day boxes trace their stops, and the day cascade caps
     at four boxes — a 14-day trip must not spend 3 seconds introducing
     itself, and everything past the second box is below the fold anyway.
   - Longest path is ~470ms end to end (6 stop steps × --trace-tick + 240ms).

   `both` fill is what holds a row invisible through its own delay, so
   applyItinTrace() always schedules the class removal in the same call that
   adds it — the list must never be able to end up permanently hidden. */
@keyframes itin-trace-box {
  from { opacity: 0; transform: translateY(10px); }
}

@keyframes itin-trace-row {
  from { opacity: 0; transform: translateY(6px); }
}

@keyframes itin-trace-spine {
  from { transform: scaleY(0); }
}

.itin-trace .day-box {
  animation: itin-trace-box var(--dur-view) var(--ease-out) both;
  animation-delay: calc(var(--trace-i, 0) * var(--trace-tick));
}

.itin-trace .itin-stop-card {
  animation: itin-trace-row 240ms var(--ease-out) both;
  animation-delay: calc(var(--trace-i, 0) * var(--trace-tick));
}

/* Custom properties inherit into pseudo-elements, so the connector element
   carries the index and its ::before spine reads it. transform-origin:top is
   the whole point — the spine grows down from the stop above toward the one
   below, in the direction the day is read. */
.itin-trace .stop-connector::before {
  transform-origin: top;
  animation: itin-trace-spine 220ms var(--ease-out) both;
  animation-delay: calc(var(--trace-i, 0) * var(--trace-tick));
}

/* Calendar view has no spine and no chronological row order to trace — the
   grid is one object, so it arrives as one. */
.itin-trace-simple {
  animation: itin-trace-box var(--dur-view) var(--ease-out) both;
}

.day-box {
  border: 2px solid var(--df-ink);
  border-radius: var(--df-radius-card);
  box-shadow: var(--df-press);
  overflow: hidden;
  background: var(--df-panel);
}

/* The tab's one shout: "today" is the only non-arbitrary reason a single
   day box should outrank its neighbours (design critique, issue #545
   follow-up). Route Amber because the itinerary IS a numbered route and
   amber is the route's own colour — a green rail would collide with the
   Pressable Green Rule the same way the old header fill did. */
.day-box-today {
  border-left-width: var(--space-xs);
  border-left-color: var(--vn-amber);
}
.day-box-today-stamp {
  display: inline-block;
  font-family: var(--df-mono);
  font-size: 0.625rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--vn-coral-text);
  border: 2px solid currentColor;
  /* 3px — DESIGN.md's print/stamp radius; the app-side token set has no
     variable for it since this is the only stamp-family mark in-app. */
  border-radius: 3px;
  padding: 1px var(--space-sm) 0;
  transform: rotate(-2.6deg);
  margin-right: var(--space-sm);
  vertical-align: middle;
}

.day-box-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  row-gap: var(--space-xs);
  /* Issue #impeccable-critique-545: was a full-bleed var(--vn-teal) fill —
     a day boundary is a LABEL, not something you press, and painting it in
     the system's one pressable colour meant "this is Tuesday" and "add a
     stop here" (the filled .day-box-add-btn below) carried identical
     weight. Green now appears exactly once per day box: on the actual
     button. The header wins the squint test through Bricolage + a keyline
     instead, per the One Shout Rule — the day boxes shouldn't all shout. */
  background: var(--df-panel);
  color: var(--df-ink);
  border-bottom: 2px solid var(--df-ink);
  padding: var(--space-xl) var(--space-2xl);
}

.day-box-title {
  font-family: var(--df-display);
  font-size: 1.0625rem;
  font-weight: 800;
  line-height: 1.15;
  letter-spacing: -0.02em;
}

/* Issue #366: now a <button> (was a plain <span>) since it absorbed the
   dropped .day-expense-strip's click-to-toggle-currency behavior — needs the
   usual button resets on top of its existing pill look. */
.day-box-spend {
  font-size: var(--fs-xs);
  background: var(--df-chip-fill);
  border-radius: var(--radius-pill);
  padding: var(--space-2xs) var(--space-md);
  border: none;
  color: inherit;
  font-family: inherit;
  cursor: pointer;
  transition: background 0.15s;
  position: relative;
}
/* Issue #447 — measured 117x19, and it is a currency toggle, not a label.
   Expanded rather than grown: it sits in .day-box-header-right, where a
   min-height would add ~25px to every day header. The chip is always wider
   than --tap-min, so max(…, 100%) grows it vertically only and never reaches
   sideways into the weather chip's or Plan B's hit zone. */
.day-box-spend::before {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: max(var(--tap-min), 100%);
  height: max(var(--tap-min), 100%);
  transform: translate(-50%, -50%);
}
.day-box-spend:hover { background: var(--df-chip-fill-hover); }

/* Groups the weather chip / Plan B trigger / spend chip into one flex item so
   .day-box-header's justify-content:space-between only ever splits the row
   into "title" vs "everything else" — without this a 3rd bare flex sibling
   would land dead-center of the header instead of beside the spend chip. */
.day-box-header-right {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
}

/* Issue #154 — "Plan B" trigger. .day-box-header is a neutral --df-panel
   field now (not a solid teal fill), so this is a plain ghost chip — no
   on-brand light-on-dark treatment needed. */
.day-box-planb-btn {
  background: var(--df-chip-fill);
  border: none;
  border-radius: var(--radius-pill);
  padding: var(--space-2xs) var(--space-lg);
  font-size: var(--fs-xs);
  font-weight: 700;
  color: inherit;
  cursor: pointer;
  font-family: inherit;
  white-space: nowrap;
  transition: background 0.15s;
  position: relative;
}
/* Issue #447 — measured 55x21. Same vertical-only expansion as .day-box-spend
   above, and for the same reason: this is the dense day header, and the
   button is already wider than --tap-min. */
.day-box-planb-btn::before {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: max(var(--tap-min), 100%);
  height: max(var(--tap-min), 100%);
  transform: translate(-50%, -50%);
}
.day-box-planb-btn:hover { background: var(--df-chip-fill-hover); }

/* Issue #206 — icon-only, same expanded-tappable-area trick as .stop-menu-btn
   (visible glyph stays small, hit region doesn't) since the day header is a
   dense row. Same ghost-chip fill as its two neighbours above, now that the
   header sits on --df-panel rather than a solid teal fill. */
.day-box-history-btn {
  background: var(--df-chip-fill);
  border: none;
  border-radius: 50%;
  width: 22px;
  height: 22px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: inherit;
  cursor: pointer;
  font-size: var(--fs-sm);
  position: relative;
  flex-shrink: 0;
}
.day-box-history-btn:hover { background: var(--df-chip-fill-hover); }
.day-box-history-btn::before {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: max(var(--tap-min), 100%);
  height: max(var(--tap-min), 100%);
  transform: translate(-50%, -50%);
}

.day-box-stops {
  display: flex;
  flex-direction: column;
}

.day-box-empty {
  padding: var(--space-xl) var(--space-2xl);
  font-size: var(--fs-sm);
  color: var(--df-ink-soft);
  text-align: center;
}

.day-box-add-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-sm);
  width: 100%;
  /* Issue #447 — measured 37px, the shortest primary action on the tab. */
  min-height: var(--tap-min);
  padding: var(--space-lg);
  border: none;
  border-top: 2px dashed var(--df-hair);
  background: var(--df-panel);
  color: var(--vn-teal);
  font-size: var(--fs-sm);
  font-weight: 500;
  cursor: pointer;
  font-family: inherit;
  transition: background 0.15s;
}
.day-box-add-btn:hover { background: var(--vn-teal-light); }

/* "Add idea" swaps to this drop zone for the duration of a drag that
   started on an idea card itself (see onStopDragStart() in itinerary.js) —
   dropping the card here deletes it (onIdeaDropOnDeleteZone()). Hidden by
   default; toggled via inline style, not re-rendered, so it survives
   mid-drag DOM churn. */
.ideas-delete-zone {
  color: var(--error);
  border-top: 1px dashed var(--error);
  background: var(--error-light);
  cursor: default;
}

/* Calendar view's Ideas→delete drag (startCalIdeaDrag() in itinerary.js) is
   a manual pointer drag, not SortableJS, so it doesn't get Sortable's own
   drop-target styling for free — this is that same "about to drop here"
   feedback, toggled directly via classList during the drag. */
.itin-delete-zone.hover {
  background: var(--error);
  color: #fff;
  border-top-color: var(--error);
}

/* Issue #73: "Add stop" + the day-route trigger share one row instead of two
   full-width rows — see renderDayActionsRow() in itinerary.js. The row
   itself (not the individual buttons) is the direct .day-box child, so it's
   what carries `grid-area: add` in the desktop override below. */
.day-box-actions-row {
  display: flex;
}
.day-box-actions-row .day-box-add-btn,
.day-box-actions-row .day-box-route-btn,
.day-box-actions-row .day-box-split-btn {
  flex: 1;
  width: auto;
}
.day-box-actions-row > :not(:first-child) {
  border-left: 2px dashed var(--df-hair);
}

.day-box-route-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-sm);
  padding: var(--space-lg);
  border: none;
  border-top: 2px dashed var(--df-hair);
  background: var(--df-panel);
  color: var(--df-ink-soft);
  font-size: var(--fs-sm);
  font-weight: 500;
  cursor: pointer;
  font-family: inherit;
  transition: background 0.15s;
}
.day-box-route-btn:hover { background: var(--vn-teal-light); color: var(--vn-teal); }

/* Issue #154 — "Split this day" / "Remove split". Deliberately its own
   class, not reusing .day-box-route-btn: unlike the day-route toggle, this
   must stay visible on mobile (see the .day-box-route-btn { display:none }
   override in the desktop-map media query below — that one is specific to
   the route panel that gets replaced by a persistent side panel on desktop). */
.day-box-split-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-sm);
  padding: var(--space-lg);
  border: none;
  border-top: 2px dashed var(--df-hair);
  background: var(--df-panel);
  color: var(--df-ink-soft);
  font-size: var(--fs-sm);
  font-weight: 500;
  cursor: pointer;
  font-family: inherit;
  transition: background 0.15s;
}
.day-box-split-btn:hover { background: var(--vn-teal-light); color: var(--vn-teal); }

.day-box-actions-row .day-box-add-btn,
.day-box-actions-row .day-box-route-btn,
.day-box-actions-row .day-box-split-btn {
  flex: 1;
  width: auto;
}
.day-box-actions-row > :not(:first-child) {
  border-left: 2px dashed var(--df-hair);
}

/* Issue #447 — hierarchy, not sizing: "+ Add stop" is the itinerary tab's
   primary action but rendered as the same grey link-weight strip as the
   secondary "Open day route in maps" beside it, so the two read as equals.
   Filled teal (the .btn-primary treatment) makes the primary one primary.
   Scoped to the actions row on purpose — .day-box-add-btn is reused for the
   Ideas sidebar's "Add idea" (#ideas-add-btn) and its drag-to-delete zone
   (.ideas-delete-zone), neither of which should become a filled button. */
.day-box-actions-row .day-box-add-btn {
  background: var(--vn-teal);
  color: var(--surface-2);
  font-weight: 600;
  border-top-color: var(--vn-teal);
}
.day-box-actions-row .day-box-add-btn:hover { background: var(--vn-teal-mid); }

/* ── Day route map (collapsible, link-out) — the toggle button itself now
   lives in .day-box-actions-row above; this section is just the expandable
   body + (on desktop) the persistent side panel. ── */
.day-map-section { border-top: 2px solid var(--df-hair); }

.day-map-body { display: none; padding: 0 var(--space-2xl) var(--space-2xl) var(--space-2xl); }
.day-map-section.expanded .day-map-body { display: block; }

/* "Day route" → "Delete" drop zone: swapped in (via the .delete-armed
   class — see onStopDragStart() in itinerary.js) only while a card is being
   dragged out of THIS day box's own stop list, so it's always right next to
   where the drag started. Same delete flow/styling as the Ideas shelf's own
   zone (.ideas-delete-zone) — both share .itin-delete-zone. */
.day-map-delete-zone {
  display: none;
  align-items: center;
  justify-content: center;
  gap: var(--space-sm);
  text-align: center;
  min-height: 60px;
  padding: var(--space-2xl);
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--error);
  background: var(--error-light);
}
.day-map-section.delete-armed .day-map-body { display: none; }
.day-map-section.delete-armed .day-map-delete-zone { display: flex; }

.day-map-link {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-sm);
  text-decoration: none;
  padding: var(--space-md);
  font-size: var(--fs-sm);
}

.day-map-stop-list {
  margin: var(--space-lg) 0 0 var(--space-4xl);
  font-size: var(--fs-sm);
  color: var(--df-ink-soft);
  line-height: 1.6;
}

.day-map-empty {
  font-size: var(--fs-sm);
  color: var(--df-ink-soft);
  padding: var(--space-xs) 0;
}

/* Desktop: turn the day route section from a collapsed toggle into a
   persistent side panel next to the stop list, since there's now width to
   spare. Only day-boxes that actually have a map section get the two-column
   treatment — :has() leaves any other .day-box as a normal single-column
   block. Grid areas (rather than flexbox) let the existing
   child elements (header/stops/add-btn/expense-strip) drop into a left
   column exactly as they already appear in markup, with no JS/HTML
   restructuring needed.
   The panel's width is a CSS var (not a literal in grid-template-columns)
   so dragging .day-map-resizer (see initDayMapResizers() in itinerary.js)
   can resize every day-box's panel at once from a single shared handle,
   instead of each box needing its own tracked width. */
:root {
  --day-map-width: 360px;
  /* The narrowest a stop column may get before the day route panel has to
     yield. Below this a stop name is not abbreviated, it is destroyed — the
     measured failure this floor exists to stop was "Senso-ji Temple"
     rendering into 10px of a 100px requirement.
     380, not a round 280: a stop row is [category pill][name][menu][caret],
     and the pill is deliberately flex-shrink:0 with an 84px label cap
     (issue #451 — the pill leads the row and must not be what collapses),
     so the name block absorbs every shortfall on its own. Measured, the row
     spends ~120px on the pill, ~64px on the two trailing buttons and ~48px
     on padding and gaps before the name gets anything. 380 leaves the name
     ~148px — enough for a real place name rather than an initial. */
  --day-stops-min: 380px;
}

/* Below 1024px the resizer plays no layout role (the grid override below
   never applies), so it's hidden rather than rendering as a stray block.
   Declared before the desktop override so that override's own `display`
   can win at >=1024px despite equal selector specificity. */
.day-map-resizer { display: none; }

/* ⚠️ Two independent fixed tracks used to stack here — Ideas at 510px and the
   day route panel at 360px — each reasonable alone, neither reserving room
   for the other. Both turned on at 1024px, but together they need roughly
   1400px of viewport before a stop name has any width left. In between, the
   `1fr` day-list track (whose automatic minimum is defeated by the
   deliberate `min-width: 0` on #itinerary-content) collapsed toward zero:
   at 1440px every one of twelve stop names was clipped, the worst to 10px of
   the 100px it needed, and at 1024px the day box itself overflowed its
   column (scrollWidth 477 against clientWidth 166) and clipped stop names
   away entirely.

   The fix is a priority order, expressed as two breakpoints rather than one:
   stop names outrank the day route panel, which outranks the Ideas sidebar.
   So the panel arrives first (1024px, day list full width), the sidebar
   column second (--bp-ideas-col, 1280px), and every flexible track now
   carries an explicit floor so neither can starve what it sits beside.
   A `1fr` track is not a floor. Any future desktop panel here arrives as a
   minmax(), not a bare length. */
/* The Ideas sidebar becomes a real column here, not at 1024px. Keep this
   number in sync with BP_IDEAS_COLUMN in js/itinerary.js, which makes the
   same structural decision for the sidebar's collapsed-by-default state. */
@media (min-width: 1280px) {
  .itin-layout {
    /* minmax() on both tracks, not `1fr 510px`. The day list keeps a floor
       wide enough for the day route panel plus a readable stop column; Ideas
       is allowed to give width back rather than holding a fixed column
       hostage. 340px is ample for an idea card — the old 510px was measured
       filling 167px of an 1833px-tall layout while stop names beside it were
       clipped to a single character. */
    grid-template-columns:
      minmax(calc(var(--day-stops-min) + var(--day-map-width) + 6px), 1fr)
      minmax(260px, 340px);
    grid-template-areas:
      "filter  ."
      "daylist ideas";
    /* Grid items stretch to fill their row by default, which would inflate
       Ideas to the day list's full height. align-items:start sizes it back
       to its own content height (its original size) while grid-template-areas
       above still keeps it starting on the same row line as the day list. */
    align-items: start;
  }
  #itin-ideas-sidebar {
    position: sticky;
    top: 80px;
    max-height: calc(100vh - 100px);
    overflow-y: auto;
  }

  /* Issue #311 — hidden (see the base-layout rule above), the 510px column
     itself needs to go too, or the day list gets stranded in a narrower 1fr
     track with a bare empty 510px gap beside it. grid-template-areas must be
     overridden here too, not just grid-template-columns — the desktop base
     rule below declares a 2-column "daylist ideas" area, and leaving that in
     place implicitly adds back a second (auto-sized) column the moment
     grid-template-columns only lists one, since the areas imply more
     columns than the explicit track list provides. That implicit column
     still picks up .itin-layout's gap, reproducing the exact stray-gap bug
     this technique exists to avoid — caught in code review since jsdom has
     no layout engine to catch it in tests. */
  .itin-layout:has(#itin-ideas-sidebar.itin-ideas-hidden) {
    grid-template-columns: 1fr;
    grid-template-areas:
      "filter"
      "daylist";
  }

  /* Collapsing Ideas (toggleIdeasExpanded()) shrinks the grid column itself
     down to a thin strip, instead of just hiding the body while the header
     keeps holding the full 510px column hostage — that's the whole point of
     collapsing at this breakpoint: give the Day box/calendar back the space.
     :has() lets the parent grid react to the child's .collapsed class. */
  .itin-layout:has(#itin-ideas-sidebar.collapsed) {
    grid-template-columns: 1fr 44px;
  }
  #itin-ideas-sidebar.collapsed {
    /* Overrides .itin-layout's align-items:start above — with the body
       gone, the header is all that's left, and stretching it the full row
       height reads as a proper vertical tab strip rather than a stray small
       button floating at the top. */
    align-self: stretch;
  }
  #itin-ideas-sidebar.collapsed .ideas-sidebar-header {
    flex-direction: column;
    height: 100%;
    padding: var(--space-2xl) var(--space-sm);
    gap: var(--space-lg);
  }
  /* Stacked, and still the full height of the strip, so the whole vertical
     tab stays one click target the way it was before the toggle became its
     own element. */
  #itin-ideas-sidebar.collapsed .ideas-sidebar-toggle {
    flex-direction: column;
    justify-content: flex-start;
    align-self: stretch;
    gap: var(--space-lg);
  }
  /* Generate belongs to the open panel, not to a 44px indicator strip: it
     would have to squeeze between the bulb and the chevron, and clicking it
     expands the panel first regardless (openIdeaSuggestions()). One click
     later it is right there in the header. */
  #itin-ideas-sidebar.collapsed .ideas-sidebar-generate-btn {
    display: none;
  }
  /* Collapsed, the title is just the bulb icon (see renderIdeasSidebar() —
     "Ideas" text only renders while expanded), so it stays upright rather
     than sideways like the old rotated text did: nothing left to rotate, and
     an upright glyph reads better in a narrow vertical strip anyway. */
  #itin-ideas-sidebar.collapsed .ideas-sidebar-bulb {
    font-size: var(--fs-xl);
    color: var(--df-ink);
  }
  /* The one place the label and count genuinely do not fit: a 44px-wide
     vertical strip. Everywhere else the collapsed panel is a full-width bar
     and keeps them (renderIdeasSidebar() now always emits both). */
  #itin-ideas-sidebar.collapsed .ideas-sidebar-label,
  #itin-ideas-sidebar.collapsed .ideas-sidebar-count {
    display: none;
  }
  /* The chevron keeps meaning "expand"/"collapse" from the JS's up/down
     class names (shared with the mobile vertical-collapse layout below
     1024px) — a single 90deg rotation reorients both to the horizontal axis
     this breakpoint actually collapses on: chevron-up (expanded, click to
     collapse) becomes right-pointing, chevron-down (collapsed, click to
     expand) becomes left-pointing, since they start from opposite tips. */
  .ideas-sidebar-chevron {
    transform: rotate(90deg);
    transition: transform 0.15s;
  }
}

@media (min-width: 1024px) {
  /* The Plan A/B flip row (issue #154, split days only) needs a named area
     like every other direct .day-box child. Without one it was auto-placed
     into an implicit row *after* the last named area, so on desktop the
     "Current view / I'm on Plan A / I'm on Plan B" controls rendered at the
     bottom of the day box — below "Add stop", detached from the header they
     belong to and from the stops they scope — while the markup and the
     mobile layout both put them directly under the header. On an unsplit day
     nothing occupies the row and it sizes to zero. */
  /* minmax() on both flexible tracks so the panel yields before the stop
     list does. With `1fr 6px var(--day-map-width)` the map held its full
     width unconditionally and the stops track absorbed every shortfall,
     which is how a 216px column ended up rendering a 100px stop name into
     10px. Now the stops track cannot resolve below --day-stops-min, and the
     map track — minmax(0, …) rather than a bare length — gives width back
     instead of forcing an overflow. The resizer's own 220–560px clamp
     (DAY_MAP_WIDTH_MIN/MAX, js/itinerary.js) sits inside this, so a
     dragged-wide panel degrades the same way a narrow viewport does. */
  .day-box:has(.day-map-section) {
    display: grid;
    grid-template-columns: minmax(var(--day-stops-min), 1fr) 6px minmax(0, var(--day-map-width));
    grid-template-areas:
      "header  resizer map"
      "plan    resizer map"
      "stops   resizer map"
      "add     resizer map";
  }

  .day-box-header       { grid-area: header; }
  .day-plan-flip-row    { grid-area: plan; }
  /* min-width: 0 is load-bearing, not defensive. The stops column is a `1fr`
     track, and a `1fr` track's automatic minimum is min-content — so a single
     long stop name ("Meiji Jingu Shrine and the Inner Garden walk through
     Yoyogi Park to Harajuku") widened this track from 743px to 870px, pushing
     the 298px map panel 126px past a parent with overflow:hidden and clipping
     42% of the day route mid-word. Same fix, same reason, as the one already
     applied one level up at .itin-layout. */
  .day-box-stops        { grid-area: stops; min-width: 0; }
  .day-box-actions-row  { grid-area: add; }

  .day-map-resizer {
    grid-area: resizer;
    display: block;
    position: relative;
    cursor: col-resize;
  }
  .day-map-resizer::after {
    content: '';
    position: absolute;
    top: 0;
    bottom: 0;
    left: 2px;
    width: 2px;
    border-radius: 1px;
    background: var(--df-hair);
    transition: background 0.15s;
  }
  .day-map-resizer:hover::after,
  .day-map-resizer.dragging::after {
    background: var(--vn-teal);
  }

  .day-map-section {
    grid-area: map;
    border-top: none;
    background: var(--df-panel);
  }

  /* No toggle needed when the panel is always visible — a plain label
     replaces the interactive day-route trigger from the actions row. */
  .day-box-route-btn { display: none; }
  .day-map-section::before {
    content: "Day route";
    display: block;
    padding: var(--space-lg) var(--space-2xl) 0;
    font-size: var(--fs-xs);
    font-weight: 700;
    color: var(--df-ink-soft);
    text-transform: uppercase;
    letter-spacing: 0.04em;
  }
  .day-map-body { display: block; padding: var(--space-lg) var(--space-2xl) var(--space-2xl); }

  /* The "Day route" label is CSS-generated content (above), not a real
     element, so it needs its own hide rule here rather than the base
     .day-map-section.delete-armed .day-map-body rule above. Height is 100%
     rather than the base rule's min-height:60px since the panel itself
     already stretches to the full day-box height on desktop (see the
     .day-box:has(.day-map-section) grid override above) — filling it avoids
     a mismatched short zone floating in a tall column. */
  .day-map-section.delete-armed::before { display: none; }
  .day-map-delete-zone { height: 100%; }
}

/* ── Itinerary lock / unlock panels ──
   Was inline `style="…"` on both banners in index.html. Lifted here so the
   confirm button's fill can take the documented var(--surface-2) foreground
   instead of a literal white — on a --vn-teal fill that literal measured
   1.31:1 after dark. `display` stays inline because updateLockUI() writes
   it directly. ── */
.itin-lock-panel { margin-top: var(--space-5xl); }

/* Was a soft pastel fill (--vn-teal-light / --vn-green-light) with no
   keyline and no shadow — the one card on this tab still in the retired
   Field Journal language while every day-box and stub around it carries a
   2px ink keyline and a hard offset shadow. Brought onto the same card
   language here: neutral --df-panel body (a full pastel fill would also
   read as a giant instance of "this is pressable", which only the button
   inside actually is), differentiated by a --day-box-today-style accent
   stripe instead — the same "colour on the edge, not the field" move this
   file already uses for "today". */
.itin-lock-card {
  border: 2px solid var(--df-ink);
  border-radius: var(--df-radius-card);
  box-shadow: var(--df-press);
  background: var(--df-panel);
  padding: var(--space-3xl);
  text-align: center;
  border-left-width: var(--space-xs);
}
.itin-lock-card-open { border-left-color: var(--vn-teal); }
.itin-lock-card-locked { border-left-color: var(--vn-green); }

.itin-lock-copy {
  font-size: var(--fs-base);
  color: var(--df-ink);
  margin-bottom: 0.75rem;
  font-weight: 500;
}
.itin-lock-copy i { color: var(--vn-teal); }
.itin-lock-title {
  font-family: var(--df-display);
  font-size: var(--fs-lg);
  color: var(--df-ink);
  font-weight: 800;
  letter-spacing: -0.02em;
}
.itin-lock-title i { color: var(--vn-green); }
.itin-lock-note { font-size: var(--fs-sm); color: var(--df-ink-soft); margin-top: var(--space-xs); }

/* No .itin-lock-btn here on purpose: the Confirm & Lock button takes
   .btn-primary (issue #482's call, and the right one) — that class already
   carries min-height: var(--tap-min) and color: var(--surface-2), which is
   what this rule existed to provide, and it is the convention every other
   primary action in the app already follows. The panel around it still
   needs its own classes, below, because it was entirely inline-styled.
   Scoped here to #confirm-itinerary-wrap rather than a redeclaration of
   .btn-primary itself — that class is the whole app's default button and a
   wholesale pill/keyline conversion of it is a separate, much larger change
   than this tab's own redesign; every other .btn-primary this tab actually
   uses already gets the DF treatment for free via .df-modal-content. */
#confirm-itinerary-wrap .btn-primary {
  width: auto;
  border-radius: var(--radius-pill);
  border: 2px solid var(--df-ink);
  box-shadow: var(--df-press);
  transition: transform var(--dur-feedback) var(--ease-out), box-shadow var(--dur-feedback) var(--ease-out);
}
#confirm-itinerary-wrap .btn-primary:hover {
  background: var(--vn-teal-mid);
  transform: translate(2px, 2px);
  box-shadow: var(--df-press-hover);
}
#confirm-itinerary-wrap .btn-primary:active {
  transform: translate(4px, 4px);
  box-shadow: var(--df-press-active);
}

.itin-lock-unlock-btn {
  margin-top: 0.75rem;
  display: inline-flex;
  align-items: center;
  gap: var(--space-sm);
  background: var(--df-panel);
  border: 2px solid var(--vn-green);
  color: var(--vn-green);
  border-radius: var(--radius-pill);
  min-height: var(--tap-min);
  padding: var(--space-sm) var(--space-2xl);
  font-size: var(--fs-sm);
  font-weight: 600;
  cursor: pointer;
  font-family: inherit;
  box-shadow: 4px 4px 0 var(--vn-green);
  transition: transform var(--dur-feedback) var(--ease-out), box-shadow var(--dur-feedback) var(--ease-out);
}
.itin-lock-unlock-btn:hover {
  transform: translate(2px, 2px);
  box-shadow: 2px 2px 0 var(--vn-green);
}
.itin-lock-unlock-btn:active {
  transform: translate(4px, 4px);
  box-shadow: 0 0 0 var(--vn-green);
}

/* ── Stop Cards ──
   Collapsed by default: one summary row (Time - Location - Edit - X). Tap
   the row to expand and reveal address/notes/photos/comments below it. ── */
.itin-stop-card {
  border-bottom: 2px solid var(--df-hair);
  /* Named container so the @container rule below (issue #547) can gate the
     address subtitle AND the expanded body's .stop-addr-place off the same
     measurement — the actual row width, not the viewport. .stop-name-block
     alone isn't a shared ancestor of both (.stop-addr-place lives in
     .stop-expanded-body, a sibling of .stop-summary-row), which is why an
     earlier version of this fix (issue #545 follow-up) could only correct
     the inline subtitle and left .stop-addr-place's unconditional
     `display: none` below stranding the address nowhere at all once the
     inline copy hid itself. */
  container-type: inline-size;
  container-name: stop-row;
}
.itin-stop-card:last-child { border-bottom: none; }

/* Issue #154 — Plan A/B wash + left stripe. A shared stop (the common case,
   and every stop on an unsplit day) gets neither — that absence IS the "this
   row is the same for everyone" signal, not just a default. inset box-shadow
   rather than border-left so this never affects layout/box sizing. */
.itin-stop-card.plan-a { background: var(--plan-a-bg); box-shadow: inset 3px 0 0 var(--plan-a-accent); }
.itin-stop-card.plan-b { background: var(--vn-sky-light); box-shadow: inset 3px 0 0 var(--vn-sky); }

/* Issue #449 — the expanded card had no containment at all, so its last
   section (ALTERNATIVES) read as though it owned the stop rendered below it.
   Each card publishes its own category hue as --stop-cat (class emitted by
   renderStopCard()); only the expanded state spends it, on a left rail. A
   variable rather than seven copies of the .expanded rule, and separate from
   the .cat-* pill classes on purpose: those carry a background+color sized
   for a pill and would paint the entire row. */
.itin-stop-card.stop-cat-food       { --stop-cat: var(--vn-amber); }
.itin-stop-card.stop-cat-transport  { --stop-cat: var(--vn-sky); }
.itin-stop-card.stop-cat-shopping   { --stop-cat: var(--vn-coral); }
.itin-stop-card.stop-cat-hotel      { --stop-cat: var(--vn-indigo); }
.itin-stop-card.stop-cat-activity   { --stop-cat: var(--vn-green); }
.itin-stop-card.stop-cat-attraction { --stop-cat: var(--vn-purple); }
/* "Other" has no hue in the Category Compass and must not borrow one — the
   rail still marks the card's extent, just without claiming a category.
   --text-muted, not --border-strong: real-browser measurement (npm run
   verify-mobile) put --border-strong at 1.41:1 against the --surface-1 tint
   in light mode and 1.96:1 in dark — invisible, which makes the containment
   this rail exists to provide decorative for every "other" stop. The rail IS
   the signal, so WCAG 2.2 SC 1.4.11's 3:1 floor for a meaningful non-text
   element applies; --text-muted clears it at 4.68:1 / 5.74:1 and is still a
   neutral. Pinned by a computed test in test/itinerary-expanded-card.test.js
   so the next token swap can't quietly reintroduce an invisible one. */
.itin-stop-card.stop-cat-other      { --stop-cat: var(--df-ink-soft); }

/* :not(.plan-a):not(.plan-b) because a Plan A/B row already carries a wash
   and a 3px stripe saying which plan it belongs to — the stronger signal, and
   one this rule would otherwise overwrite on source order. Those rows are
   already delimited by that wash. */
.itin-stop-card.expanded:not(.plan-a):not(.plan-b) {
  background: var(--df-panel);
  box-shadow: inset 2px 0 0 var(--stop-cat, var(--df-ink-soft));
}

/* .itin-stop-card:has(+ .stop-connector) below drops the card hairline so a
   connected pair shows exactly one separator. Right for two collapsed rows,
   precisely wrong for an expanded one: it deletes the only line marking where
   the expanded body ends, which is what let ALTERNATIVES annex the next stop.
   --border-strong, not --border: this edge closes a tall card, so it has to
   carry further than a hairline between two 46px rows. */
.itin-stop-card.expanded:has(+ .stop-connector) {
  border-bottom: 2px solid var(--df-ink);
}

/* The tint above is the same --surface-1 that .stop-summary-row:hover uses,
   which would leave an expanded row with no hover feedback at all. Flipping
   its hover to --surface-2 keeps the feedback and works in both themes:
   --surface-2 is the lighter of the pair in light mode and in dark mode. */
.itin-stop-card.expanded .stop-summary-row:hover { background: var(--df-ground); }

/* Day header split badge + flip control. Wraps on narrow screens — this row
   now carries 4 elements (Current View / Edit Plan Setting / Remove X /
   Switch to Y), too many to guarantee one line on a phone-width day box. */
.day-plan-flip-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-md);
  padding: var(--space-sm) var(--space-2xl);
  font-size: var(--fs-xs);
  border-bottom: 2px solid var(--df-hair);
}
.day-plan-badge {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  color: var(--df-ink-soft);
  font-weight: 600;
}
.day-plan-flip-row.plan-a .day-plan-badge { color: var(--plan-a-accent); }
.day-plan-flip-row.plan-b .day-plan-badge { color: var(--vn-sky-text); }
.day-plan-flip-btn.plan-a { color: var(--plan-a-accent); border-color: var(--plan-a-accent); }
.day-plan-flip-btn.plan-b { color: var(--vn-sky-text); border-color: var(--vn-sky); }

/* Issue #154 Phase 2 — self-declared roster controls in the flip row. */
.day-plan-roster-declare { display: flex; align-items: center; gap: var(--space-sm); flex-wrap: wrap; }
.day-plan-declare-btn.plan-a.active { background: var(--plan-a-accent); color: #fff; border-color: var(--plan-a-accent); }
.day-plan-declare-btn.plan-b.active { background: var(--vn-sky); color: #fff; border-color: var(--vn-sky); }
.day-plan-roster-tally { color: var(--df-ink-soft); font-size: var(--fs-xs); }

.stop-summary-row {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  /* 12px rather than 10px so a stop row stays visibly taller than the
     connector gap between stops — the content has to outweigh the link. */
  padding: var(--space-xl);
  cursor: pointer;
}
.stop-summary-row:hover { background: var(--df-hair); }

/* The day's spine — the vertical axis .stop-connector's tick lines up with.
   It used to be the 62px time-badge column. Issue #451 removed that badge:
   it was the loudest element on every row and identical on all of them, so
   the eye had nowhere to land, while the category — DESIGN.md's designated
   meaning-carrier — was a 27x15 icon-only pill. The category pill now leads
   the row with its label, and the spine is its icon column instead, so the
   tick still threads through a real vertical axis rather than empty space. */
:root { --stop-spine: 22px; }

/* Issue #451 — the label half of the leading category pill. Not rendered at
   all on an idea card, whose pill is a 30px circle by design (see
   .itin-stop-card.idea-card .cat-pill) and would be broken by text inside it.
   Capped because "Accommodation" at full width would push a long place name
   off a 390px row; it truncates rather than wraps, since the hue and icon
   already carry the meaning if the word clips. */
.cat-pill-label {
  max-width: 84px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The quiet second line: time, then who is talking. Everything here is
   deliberately --fs-xs/muted — this is the row's supporting evidence, not its
   headline (the place name is). */
.stop-meta-line {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  font-size: var(--fs-xs);
  color: var(--df-ink-soft);
  min-width: 0;
}
.stop-meta-time { white-space: nowrap; }
.stop-meta-sep { color: var(--df-ink-soft); }

/* "No time yet" is real planning state, not an empty cell — it stays visible
   but drops to the muted weight so a scheduled row still reads louder. */
.stop-no-time {
  font-style: italic;
  opacity: 0.85;
}

/* Issue #451's sharper half: a three-person trip rendered identically to a
   solo one. This is the only thing on a collapsed row that says anyone else
   is here, so it gets the one non-muted colour on the meta line — still
   quieter than the place name, but not invisible. */
.stop-comment-signal {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2xs);
  color: var(--vn-teal);
  font-weight: 600;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Issue #638 — same shape as .stop-comment-signal above, purely
   informational (tapping the row already expands the card via
   .stop-summary-row's own handler). position:relative + ::before below
   still meets the --tap-min 44px minimum CLAUDE.md requires of any small
   interactive/near-interactive glyph, even with no onclick of its own —
   insurance against it ever gaining one. */
.stop-booking-signal {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2xs);
  color: var(--vn-indigo);
  font-weight: 600;
  white-space: nowrap;
  position: relative;
}
.stop-booking-signal::before {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: max(var(--tap-min), 100%);
  height: max(var(--tap-min), 100%);
  transform: translate(-50%, -50%);
}

/* Wraps the grip + type pill + place name, kept separate from the time
   badge so dragging to reorder never touches time. Deliberately NOT the
   drag surface itself (see .stop-drag-grip) — it used to be, and
   touch-action:none on a flex:1 element covering the photo/category/name
   meant the largest touch region on the row didn't scroll. */
.stop-draghandle {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: var(--space-sm);
}

/* The actual SortableJS handle (issue #545 follow-up) — an explicit grip
   column instead of the whole row, so touch-action:none is scoped to
   something a thumb only lands on deliberately. Only rendered on an
   editable card (renderStopCard's `draggable` gate). */
.stop-drag-grip {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 20px;
  min-height: var(--tap-min);
  touch-action: none;
  color: var(--df-ink-soft);
  cursor: grab;
  position: relative;
}
/* Same expanded-hit-box idiom as .stop-menu-btn — the glyph stays a slim
   20px column, the tappable/grabbable region doesn't. */
.stop-drag-grip::before {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: max(var(--tap-min), 100%);
  height: max(var(--tap-min), 100%);
  transform: translate(-50%, -50%);
}
.stop-drag-grip.sortable-drag,
.stop-drag-grip.sortable-chosen { cursor: grabbing; }
/* Issue #159 — Ideas sidebar cards dragged into the calendar grid (Pointer
   Events, see startCalIdeaDrag() in js/itinerary.js) rather than SortableJS,
   which only wires the day-list view's own drop targets. */
.itin-stop-card.dragging { opacity: 0.5; }

/* Issue #226: direct A-to-B Google Maps directions link-out between two
   consecutive stops (replaces the old walk/ride time estimate). Still a real
   44px tappable target — min-height is deliberately unchanged, one-handed
   in-trip use depends on it — but it no longer *reads* as a row peer to the
   stops it joins. It used to carry a full-bleed hover fill, its own
   hairline, and fs-sm/500 text at 44px tall against a 46px stop row: the
   link between two places weighed exactly as much as the places, so a
   five-stop day squinted down to ten identical bands with no primary. Now
   the 44px is whitespace with a quiet mark in it, and the spine tick below
   spends that height saying "these two stops are consecutive" instead. */
.stop-connector {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  /* Indented past the time spine + the summary row's own gap so the label
     starts on the same axis as the category pills above and below it. */
  padding: 0 var(--space-xl) 0 calc(var(--space-xl) + var(--stop-spine) + var(--space-md));
  min-height: 44px;
  font-size: var(--fs-xs);
  font-weight: 500;
  color: var(--df-ink-soft);
  text-decoration: none;
  transition: color 0.15s;
}
/* The spine, continued through the gap — and, per DESIGN.md's "Dashed
   Route" signature, a real journey drawn between two consecutive stops
   rather than a plain rule: 3px, Route Amber, 9px-on/9px-off, the same
   repeating-linear-gradient the landing page draws between its numbered
   pins. It's the mark that turns a column of category icons into an actual
   axis, and it only ever spans the gap between two stops that really are
   consecutive (no connector is rendered when either stop lacks a place to
   route from — see buildStopDirectionsUrl()). */
.stop-connector::before {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  left: calc(var(--space-xl) + (var(--stop-spine) / 2) - 1.5px);
  width: 3px;
  border-radius: 1px;
  background-image: repeating-linear-gradient(
    to bottom,
    var(--vn-amber) 0,
    var(--vn-amber) 9px,
    transparent 9px,
    transparent 18px
  );
}
.stop-connector:hover,
.stop-connector:focus-visible { color: var(--vn-teal); }
.stop-connector:hover::before,
.stop-connector:focus-visible::before { background: var(--vn-teal); }

/* Exactly one separator between any two stops: the connector's own
   whitespace when there is one, the card hairline when there isn't.
   Without this a joined pair got both — a hairline hard against the bottom
   of one card and then 44px of air — which read as an off-balance gap
   belonging to the card above rather than to the pair. */
.itin-stop-card:has(+ .stop-connector) { border-bottom: none; }

/* Issue #400 Phase 1.5 — same 56px-square idiom as .idea-suggestion-photo
   (css/features/idea-pool.css), so the app has one photo thumbnail look,
   not two. A real <img>, not that card's background-image div — loading="lazy"
   only applies to <img>. onerror (see renderStopCard()) removes it outright
   on a miss/cap-exceeded 404/503, so there's never a broken-image icon. */
.stop-photo-thumb {
  flex-shrink: 0;
  width: 56px;
  height: 56px;
  border-radius: var(--df-radius-stub);
  object-fit: cover;
  background: var(--df-panel);
}

/* Wider banner variant for the expanded body — tappable to open the
   attributed lightbox (viewPlacePhoto()). Design canvas gap-close: reuses
   the exact Instant Print material .dtl-print established (css/discover.css)
   — cream stock, two strips of tape, a slight tilt — rather than a plain
   full-bleed rectangle, so the app has one photographic-print idiom, not
   two. Deliberately compact and left-aligned (not full width) the way the
   canvas's own .banner was: a wide tilted rectangle would risk pushing past
   .stop-expanded-inner's padding, which is why the frame is capped and that
   ancestor already carries overflow:hidden as a clean clip, never a scrollbar. */
.stop-photo-banner-frame:has(.stop-photo-banner) {
  position: relative;
  display: inline-block;
  max-width: min(280px, 100%);
  margin: 0 0 var(--space-md);
  padding: 8px 8px 22px;
  background: #fdf6ea;
  border-radius: var(--df-radius-print);
  /* No border — matching .dtl-print/.invite-print (css/discover.css), the
     shipped Instant Print's own edge is background + shadow only. The
     design canvas draft this was built from had one; the real signature
     never did, so it was dropped here to match. */
  /* The one sanctioned blurred shadow (--df-press-lift) — a print lifted off
     the page, not a flat sticker. Doesn't flip with theme; see its :root def. */
  box-shadow: var(--df-press-lift);
  transform: rotate(-1.4deg);
}
/* Tape corners — pinned amber, not theme-aware, same reasoning as the print
   stock above: a strip of tape doesn't change color at midnight. 42% alpha
   is DESIGN.md's own documented figure for the Instant Print signature
   ("Route Amber at 42% alpha") and matches --dtl-tape (css/discover.css) —
   not reused directly since that token is scoped to .dtl, not global. */
.stop-photo-banner-frame:has(.stop-photo-banner)::before,
.stop-photo-banner-frame:has(.stop-photo-banner)::after {
  content: '';
  position: absolute;
  top: -7px;
  width: 42px;
  height: 15px;
  background: rgba(186, 117, 23, 0.42);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.25);
}
.stop-photo-banner-frame:has(.stop-photo-banner)::before { left: -8px; transform: rotate(-4deg); }
.stop-photo-banner-frame:has(.stop-photo-banner)::after { right: -8px; transform: rotate(3.4deg); }

.stop-photo-banner {
  display: block;
  width: 100%;
  max-width: 264px;
  max-height: 180px;
  object-fit: cover;
  border-radius: 1px;
  cursor: zoom-in;
}

.stop-name-compact {
  flex: 1;
  min-width: 0;
  /* Issue #453 — this IS the tab's primary content, so it takes DESIGN.md's
     documented Body size (--fs-md, 14px, "primary card text") rather than
     --fs-base (13px, documented in this file as form labels/section
     subheadings/default UI text — one step below body copy). */
  font-size: var(--fs-md);
  font-weight: 600;
  color: var(--df-ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Wraps the name (+ an idea card's address subtitle, see below) — takes over
   .stop-name-compact's old flex:1/min-width:0 role as the direct flex item
   inside .stop-draghandle, now that the name isn't always its only child. */
.stop-name-block {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 1px;
}

/* Ideas cards show their address directly under the name at every width —
   there's no day/time to preview an idea by, so the address is the next most
   useful thing to surface without a tap. A scheduled stop only gets it from
   the desktop breakpoint up, where there's spare row width; see the rules
   below and in the @media (min-width: 1024px) block above. */
.stop-name-subtitle {
  font-size: var(--fs-xs);
  color: var(--df-ink-soft);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* A scheduled stop now carries its address in the markup too (not just an
   idea, which is why the isIdea branch in renderStopCard() went away), but
   below the desktop breakpoint there is no room for it: at 390px the place
   name is already down to 162px of a 341px row and truncating, so a second
   line would be competing for space the name itself doesn't have. It stays
   an expand-only detail there, exactly as before. */
.itin-stop-card:not(.idea-card) .stop-name-subtitle { display: none; }

/* Density at the desktop breakpoint, where .app-main drops its 680px cap and
   a stop row runs ~680px wide (more on a wide monitor) to hold one short
   place name — roughly 70% of the row sat empty while that same name
   truncates on a phone. The address rides on the name's own line rather than
   waiting for an expand: it's the one field that answers "where is this"
   without a tap, and the row already has the width for it.
   This block has to live *after* the base display:none above, not up in the
   earlier @media (min-width: 1024px) block with the rest of the itinerary's
   desktop layout — the two selectors are specificity-identical and a media
   query adds none, so source order is the only thing deciding between them. */
@media (min-width: 1024px) {
  .itin-stop-card:not(.idea-card) .stop-name-subtitle {
    display: block;
    /* The address is what gives ground when the row runs out: it shrinks and
       truncates while the name keeps its intrinsic width. A percentage cap on
       the name instead would have truncated the row's primary content with
       whitespace still sitting to its right. */
    flex: 1 1 auto;
    min-width: 0;
  }
  /* Gated on a subtitle actually being present — an address-less stop keeps
     the original single-item column rather than becoming a row with one
     child that then shares its width with nothing. */
  .itin-stop-card:not(.idea-card) .stop-name-block:has(.stop-name-subtitle) {
    flex-direction: row;
    align-items: baseline;
    gap: var(--space-lg);
  }
  /* ⚠️ The shrink factor is doing the work here, and `1` was wrong.
     flex-shrink is weighted by flex-basis, so `0 1 auto` on the name against
     `1 1 auto` on the address made the two give ground in PROPORTION to
     their content width — the opposite of what the comment above intends.
     Measured at 1440px: the name rendered into 55px of the 100px
     "Senso-ji Temple" needs while the secondary address kept 130px of its
     own. A near-zero factor makes the name the last thing to yield: the
     address absorbs essentially the whole shortfall, and the name only
     starts truncating once the address has nothing left to give. Not `0`,
     which would let a genuinely long name overflow the row instead of
     ellipsising as a last resort. */
  .itin-stop-card:not(.idea-card) .stop-name-block:has(.stop-name-subtitle) .stop-name-compact {
    flex: 0 0.04 auto;
  }
  /* Same string as the inline one above, so the expanded body stands down
     here instead of printing the address twice. Only the place address —
     .stop-addr is shared with the expense and history lines. This is the
     default for the >=1024px case; the @container rule below (issue #547)
     flips it back to shown whenever the row is too narrow for the inline
     copy, so the address is never hidden in both places at once. */
  .itin-stop-card:not(.idea-card) .stop-addr-place { display: none; }
}

/* Issue #545 follow-up (design critique) found the flex-shrink weighting
   above absorbing the address down to an 8-11px sliver — an ellipsis and
   nothing else — rather than actually yielding, while the place name (the
   row's one primary string) still truncated too. Issue #547 then found that
   fix's own gate (viewport width) was the same wrong proxy the original bug
   used: PR #544 gave the day list an explicit floor (--day-stops-min:
   380px) so the map panel and Ideas sidebar can legitimately squeeze a
   >=1024px row down to a width the phone layout already treats as "no
   room". A container query measuring the CARD's own width (not the
   viewport's, and not just .stop-name-block's — see .itin-stop-card's
   container-type above for why it has to be the whole card) hides the
   address outright once there isn't room to show it as more than a stub, so
   the name gets the width back instead — and un-hides .stop-addr-place in
   the same breath, since that's the only remaining place the address can be
   read. 534px, not the 340px this superseded: converts the old
   .stop-name-block-relative threshold to the full row via the constant
   ~194px both rules measured between the two (photo thumb, drag handle,
   category pill, menu/caret buttons and padding — everything the row
   spends before the name block gets anything, see --day-stops-min above).
   Re-verify with a real browser at 1280/1440 with the map panel dragged
   wide and the Ideas sidebar open if that gap ever changes. */
@container stop-row (max-width: 534px) {
  .itin-stop-card:not(.idea-card) .stop-name-subtitle {
    display: none;
  }
  .itin-stop-card:not(.idea-card) .stop-name-block:has(.stop-name-subtitle) .stop-name-compact {
    flex: 1 1 auto;
  }
  .itin-stop-card:not(.idea-card) .stop-addr-place {
    display: flex;
  }
}

/* Ideas read as a compact preview card (icon + title + address) rather than
   a line item — dropping the day/time column (never applicable to an idea)
   frees the row for a bit more breathing room and a proper circular icon
   badge in place of the small text-pill category tag Stops use. */
.itin-stop-card.idea-card .stop-summary-row {
  padding: var(--space-xl) var(--space-2xl);
  gap: var(--space-lg);
}
.itin-stop-card.idea-card .cat-pill {
  width: 30px;
  height: 30px;
  padding: 0;
  justify-content: center;
  border-radius: 50%;
  font-size: 15px;
  flex-shrink: 0;
}

/* Design canvas gap-close — an idea is parked, never pinned to a route the
   way a scheduled stop is, so its card carries a strip of tape and its
   thumbnail sits very slightly askew: the same "taped down" grammar as
   .stop-photo-banner-frame above and .dtl-print (css/discover.css)
   elsewhere in the app. #itin-ideas-sidebar's own padding (var(--space-lg),
   see .day-box-stops there) gives this room even though .day-box carries
   overflow:hidden — the -7px offset stays well inside it. Scoped to
   .idea-card only — .stop-photo-thumb itself stays the one shared look
   everywhere else it's reused (accommodation rows, the idea-suggestion
   carousel), per issue #400 Phase 1.5's "one photo thumbnail look" rule. */
.itin-stop-card.idea-card {
  position: relative;
}
.itin-stop-card.idea-card::before {
  content: '';
  position: absolute;
  top: -7px;
  left: 22px;
  width: 44px;
  height: 14px;
  /* 42% alpha — DESIGN.md's own documented figure for the Instant Print
     signature's tape ("Route Amber at 42% alpha"), same as the banner
     frame above. */
  background: rgba(186, 117, 23, 0.42);
  border-left: 1px dashed rgba(16, 26, 20, 0.3);
  border-right: 1px dashed rgba(16, 26, 20, 0.3);
  transform: rotate(-3deg);
  pointer-events: none;
}
.itin-stop-card.idea-card .stop-photo-thumb {
  transform: rotate(-2deg);
  box-shadow: 0 1px 0 rgba(9, 20, 13, 0.1), 0 6px 10px -8px rgba(9, 20, 13, 0.6);
}

.stop-expand-caret {
  flex-shrink: 0;
  font-size: var(--fs-md);
  color: var(--df-ink-soft);
  transition: transform 0.15s;
}
.itin-stop-card.expanded .stop-expand-caret { transform: rotate(180deg); }

/* Expanding a stop is the itinerary's primary interaction, and as a
   display:none→block flip it moved every row below it in a single frame with
   nothing to say where the new content came from. The 0fr→1fr grid row is the
   content-height-agnostic way to transition it: no measured pixel height to
   go stale when comments load in afterwards, and nothing animating on `height`
   itself. The global `* { box-sizing: border-box }` at the top of this file is
   what lets the inner box's padding collapse to nothing inside a 0fr track;
   min-height:0 defeats the grid item's automatic min-content floor. */
.stop-expanded-body {
  display: grid;
  /* minmax(0, …) rather than a bare 0fr/1fr, and this is not stylistic: an
     fr track's automatic minimum is its content's min-content size, and
     padding contributes to that no matter what overflow or min-height the
     item carries. A bare 0fr therefore collapsed to 12px, not 0 — the inner
     box's own padding-bottom — leaving every collapsed stop in the list
     carrying a sliver of dead space. Measured in Chrome; jsdom has no layout
     engine and reports nothing here either way. */
  grid-template-rows: minmax(0, 0fr);
  transition: grid-template-rows var(--dur-state) var(--ease-out);
}
.itin-stop-card.expanded .stop-expanded-body { grid-template-rows: minmax(0, 1fr); }

/* visibility, not just the clip, is load-bearing: display:none used to keep
   a collapsed stop's address, notes, photos and comments out of the
   accessibility tree and out of the Tab order. A 0-height overflow:hidden box
   would leave every one of them focusable and readable — so the state that
   replaces display:none has to do that job too. The delay only applies on the
   way out, so the content stays visible for the length of the collapse and
   disappears the moment it finishes. */
.stop-expanded-inner {
  min-height: 0;
  overflow: hidden;
  visibility: hidden;
  padding: 0 var(--space-xl) var(--space-xl) var(--space-xl);
  transition: visibility 0s linear var(--dur-state);
}
.itin-stop-card.expanded .stop-expanded-inner {
  visibility: visible;
  transition-delay: 0s;
}

.stop-addr {
  font-size: var(--fs-xs);
  color: var(--df-ink-soft);
  margin-bottom: var(--space-xs);
  display: flex;
  align-items: center;
  gap: var(--space-xs);
}

/* Per-stop estimate within/exceed (issue #410) — overrides .stop-addr's
   default muted color; exceed reuses the same coral this app already uses
   for every other "over" state (Budget tab's overbudget band, the balance
   card's owed/owes coloring). */
.stop-estimate-within { color: var(--vn-teal); }
.stop-estimate-exceed { color: var(--vn-coral); font-weight: 600; }

/* Issue #638 — the "Booking says 20:00 [Match booking]" drift line, same
   coral-for-attention convention as .stop-estimate-exceed above. */
.stop-booking-drift { color: var(--vn-coral); }

/* Issue #631 P4 — the Bookings tab card's "Recorded"/"Not in budget" line.
   A direct child of .itin-stop-card, sibling to .stop-summary-row rather
   than nested in .stop-expanded-inner, so it's visible without expanding
   the card — matches .stop-expanded-inner's own horizontal padding for
   alignment with the summary row's icon/text above it. Both states are a
   real <button> (js/bookings.js's _bookingMoneyLineHtml()) — the "Recorded"
   state used to be a plain onclick div, unreachable by keyboard — so the
   button reset lives on the shared class both states carry. */
.booking-money-line {
  padding: 0 var(--space-xl) var(--space-sm);
  font-size: var(--fs-xs);
  background: none;
  border: none;
  width: 100%;
  text-align: left;
  cursor: pointer;
  color: inherit;
  min-height: var(--tap-min);
}
.booking-money-line-missing {
  color: var(--vn-coral);
}

.stop-link {
  font-size: var(--fs-xs);
  margin-bottom: var(--space-xs);
  display: flex;
  align-items: center;
  gap: var(--space-xs);
}
.stop-link a { color: var(--vn-teal); overflow-wrap: anywhere; }

/* Issue #159 — only shown when durationMin was explicitly set (not the
   calendar view's category-default fallback), matching .stop-addr's styling */
.stop-duration {
  font-size: var(--fs-xs);
  color: var(--df-ink-soft);
  margin-bottom: var(--space-xs);
  display: flex;
  align-items: center;
  gap: var(--space-xs);
}

/* Issue #206 — quiet "who added/changed this" line, same look as .stop-addr/.stop-duration */
.stop-attribution {
  font-size: var(--fs-xs);
  color: var(--df-ink-soft);
  margin-bottom: var(--space-xs);
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  opacity: 0.75;
}

/* Issue #59 — alternative places, nested under their confirmed stop's card */
.stop-alternatives {
  margin-top: var(--space-md);
  padding-top: var(--space-md);
  border-top: 2px dashed var(--df-hair);
}
.stop-alternatives-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  font-size: var(--fs-xs);
  color: var(--df-ink-soft);
  text-transform: uppercase;
  margin-bottom: var(--space-sm);
}
.stop-alternatives .itin-stop-card {
  margin-bottom: var(--space-sm);
  opacity: 0.85;
}

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

/* ── Bookings (Hotel Stays / Flights) — core section headers, above the
   day-by-day itinerary content. Issue #73: each section is collapsible (see
   toggleHotelSection()/toggleFlightSection() in js/bookings.js) — it's a
   <button>, not a <div>, hence the width/border/background reset. ── */
.booking-section-header {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  width: 100%;
  border: none;
  background: none;
  padding: 0;
  min-height: var(--tap-min);
  font-size: var(--fs-base);
  font-weight: 700;
  color: var(--df-ink-soft);
  margin: var(--space-2xl) 0 var(--space-sm);
  cursor: pointer;
  font-family: inherit;
  text-align: left;
}
.booking-section-header:hover { color: var(--df-ink); }
.booking-section-count {
  font-size: var(--fs-xs);
  font-weight: 500;
  color: var(--df-ink-soft);
}
.booking-section-caret {
  margin-left: auto;
  font-size: var(--fs-md);
  transition: transform 0.15s;
}
.booking-section-caret.collapsed { transform: rotate(-90deg); }

/* Expansion only. Collapsing stays an instantaneous `style.display = 'none'`
   in applyHotelSectionExpanded()/applyTransportSectionExpanded()
   (js/bookings.js) — that state is asserted synchronously by the bookings
   tests, and a display flip cannot be transitioned anyway. Exit faster than
   entrance is the rule here regardless, and instant is faster. */
@keyframes booking-section-enter {
  from { opacity: 0; transform: translateY(-6px); }
}
.booking-section-enter {
  animation: booking-section-enter var(--dur-state) var(--ease-out) both;
}
.booking-empty {
  font-size: var(--fs-base);
  color: var(--df-ink-soft);
  padding: var(--space-md) 0;
}

.accommodation-dropzone {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-sm);
  padding: var(--space-4xl) var(--space-lg);
  border: 2px dashed var(--df-ink);
  border-radius: var(--df-radius-card);
  font-size: var(--fs-base);
  color: var(--df-ink-soft);
  text-align: center;
  cursor: pointer;
}
.accommodation-dropzone i { font-size: var(--fs-2xl); }
.accommodation-dropzone.dragover {
  border-color: var(--vn-teal);
  background: var(--vn-teal-light);
  color: var(--vn-teal);
}
.accommodation-dropzone.has-file {
  border-style: solid;
  border-color: var(--vn-teal);
  color: var(--vn-teal);
}
.accommodation-dropzone.parsing {
  border-color: var(--vn-indigo);
  color: var(--vn-indigo);
  pointer-events: none;
}

/* Issue #705 — booking-confirmation dropzone in the Add-a-booking sheet.
   Own copy rather than sharing .accommodation-dropzone, per this codebase's
   per-file/feature duplication convention (see CLAUDE.md). */
.booking-dropzone {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-sm);
  padding: var(--space-4xl) var(--space-lg);
  border: 2px dashed var(--df-ink);
  border-radius: var(--df-radius-card);
  font-size: var(--fs-base);
  color: var(--df-ink-soft);
  text-align: center;
  cursor: pointer;
}
.booking-dropzone i { font-size: var(--fs-2xl); }
.booking-dropzone.dragover {
  border-color: var(--vn-teal);
  background: var(--vn-teal-light);
  color: var(--vn-teal);
}
.booking-dropzone.has-file {
  border-style: solid;
  border-color: var(--vn-teal);
  color: var(--vn-teal);
}
.booking-dropzone.parsing {
  border-color: var(--vn-indigo);
  color: var(--vn-indigo);
  pointer-events: none;
}

/* Issue #473 PR4b — fixed-anatomy comparison row: every suggested option
   renders the same slots (thumb / name+meta / price / votes / actions) in
   the same position, "—" standing in for an absent price, so comparing
   options is a scan down one column instead of a reparse of prose per row. */
.accommodation-option-row {
  display: grid;
  grid-template-columns: 56px 1fr auto auto;
  align-items: center;
  gap: var(--space-md);
  padding: var(--space-md) 0;
  border-bottom: 2px solid var(--df-hair);
}
.accommodation-option-row:last-child { border-bottom: none; }
/* Issue #473 PR4b — "not available for booking" (PRD: "keeps it visible
   with its reason") stays in the same fixed-anatomy row, just dimmed and
   with its vote/confirm actions removed rather than disabled — there's
   nothing left to vote on or confirm. */
.accommodation-option-row.unavailable { opacity: 0.6; }
.accommodation-option-row.unavailable .accommodation-option-name { text-decoration: line-through; }
.accommodation-option-row.unavailable .accommodation-option-meta { color: var(--vn-coral); }
.accommodation-option-info { min-width: 0; }
.accommodation-option-name {
  font-weight: 600;
  font-size: var(--fs-md);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.accommodation-option-meta {
  font-size: var(--fs-xs);
  color: var(--df-ink-soft);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  margin-top: 2px;
}
.accommodation-option-price {
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--df-ink);
  white-space: nowrap;
  text-align: right;
  min-width: 4.5em;
}
.accommodation-option-actions {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
}
@media (max-width: 680px) {
  .accommodation-option-row {
    grid-template-columns: 44px 1fr auto;
    grid-template-areas: "thumb info actions" "price price actions";
    row-gap: var(--space-xs);
  }
  .accommodation-option-row .stop-photo-thumb { grid-area: thumb; width: 44px; height: 44px; }
  .accommodation-option-info { grid-area: info; }
  .accommodation-option-price { grid-area: price; text-align: left; }
  .accommodation-option-actions { grid-area: actions; }
}

/* A field autofilled from a parsed booking confirmation, an unfurled link,
   or a Places pick — stays a plain editable input, this is just a marker so
   the user knows to double-check it (issue #473 PR4b brief: "every
   autofilled field marked and stays editable"). */
.accommodation-autofill-tag {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: var(--fs-xs);
  font-weight: 600;
  color: var(--vn-indigo);
  margin-left: var(--space-sm);
}
.accommodation-autofill-tag i { font-size: 0.9em; }

/* Issue #705 — same marker, own copy for the Add-a-booking sheet (per this
   codebase's per-file/feature duplication convention). */
.booking-autofill-tag {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: var(--fs-xs);
  font-weight: 600;
  color: var(--vn-indigo);
  margin-left: var(--space-sm);
}
.booking-autofill-tag i { font-size: 0.9em; }

/* Confirm is the one animated beat in the suggest-first flow (issue #473
   PR4b brief) — a single indigo pulse on the card that just picked a
   winner, category-accurate (Dusk Indigo = accommodation everywhere else in
   the app). Resolves to the settled state below, never a mid-flight one. */
@keyframes accommodation-confirm-pulse {
  0% { box-shadow: inset 0 0 0 2px var(--vn-indigo); background: var(--vn-indigo-light); }
  100% { box-shadow: inset 0 0 0 2px transparent; background: transparent; }
}
.accommodation-confirm-pulse {
  animation: accommodation-confirm-pulse var(--dur-view) var(--ease-out) both;
}

/* Booker handoff — shown on the Itinerary tab when the signed-in member has
   just been assigned to book a confirmed accommodation. Reuses
   .hotel-affiliate-box's indigo-tinted-box idiom rather than inventing a
   second banner look. */
.accommodation-booker-banner {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  background: var(--vn-indigo-light);
  border: 2px solid var(--vn-indigo);
  border-radius: var(--df-radius-card);
  padding: var(--space-lg) var(--space-xl);
  margin-bottom: var(--space-lg);
}
.accommodation-booker-banner i.ti-briefcase {
  font-size: var(--fs-2xl);
  color: var(--vn-indigo);
  flex-shrink: 0;
}
.accommodation-booker-banner-text { flex: 1; min-width: 0; font-size: var(--fs-sm); }
.accommodation-booker-banner-text strong { color: var(--vn-indigo); }

/* Hotel-type stop suggestion box (Add/Edit Stop modal + expanded stop card) */
.hotel-affiliate-box {
  background: var(--vn-indigo-light);
  border: 2px solid var(--vn-indigo);
  border-radius: var(--df-radius-card);
  padding: var(--space-lg) var(--space-xl);
  margin-top: var(--space-md);
}
.hotel-affiliate-label {
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--vn-indigo);
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  margin-bottom: var(--space-md);
}
.hotel-affiliate-links {
  display: flex;
  gap: var(--space-md);
}
.hotel-affiliate-links a.btn-secondary {
  flex: 1;
  text-align: center;
  text-decoration: none;
  padding: var(--space-md);
  font-size: var(--fs-sm);
}

/* Issue #473 PR8 design brief — Bookings tab coverage strip: "a night-by-
   night bar across the trip: covered nights indigo, transport-spanned
   nights sky, uncovered nights an open hairline gap. Tapping a gap opens
   the add sheet pre-dated to that gap." Indigo/sky reuse the exact same
   tokens the Category Compass already assigns to hotel/transport (see
   .cat-bar-fill.cat-hotel/.cat-transport above) rather than inventing a
   fourth pairing. "ok" (fully covered) reuses the settled-state green
   token, same as .settle-status-confirmed elsewhere. */
.bookings-coverage-strip {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  margin-bottom: var(--space-lg);
  font-size: var(--fs-sm);
}
.bookings-coverage-strip.ok {
  flex-direction: row;
  align-items: center;
  gap: var(--space-sm);
  background: var(--vn-green-light);
  color: var(--vn-green);
  border-radius: var(--df-radius-card);
  padding: var(--space-lg) var(--space-xl);
}
.bookings-coverage-bar {
  display: flex;
  align-items: center;
  gap: 3px;
  /* A trip long enough that flex:1 + each cell's 6px floor (below) exceeds
     the viewport width would otherwise push this row past the page edge —
     scroll it in its own container instead, same as every other
     always-visible-in-full row in this app (e.g. the print wall). */
  overflow-x: auto;
  /* Setting overflow-x alone forces the UA to compute overflow-y as auto
     too (CSS 2.1 11.1.1) — and each .gap cell's invisible 44px ::before tap
     zone (below) then reads as vertical scroll overflow against this row's
     20px content height, producing a real, visible vertical scrollbar for
     no reason. min-height gives the row enough box to contain that pseudo
     without clipping it back down below --tap-min. */
  min-height: var(--tap-min);
}
/* Plain span for covered/transit nights; a real <button> only for a tappable
   gap — same "interactive elements are real buttons" rule as everywhere
   else in this app, not a click handler on a decorative span. */
.bookings-coverage-night {
  flex: 1;
  min-width: 6px;
  height: 20px;
  border-radius: 3px;
  padding: 0;
  border: none;
  cursor: default;
}
.bookings-coverage-night.covered { background: var(--vn-indigo); }
.bookings-coverage-night.transit { background: var(--vn-sky); }
.bookings-coverage-night.gap {
  position: relative;
  background: transparent;
  border: 1px dashed var(--df-hair);
  cursor: pointer;
}
.bookings-coverage-night.gap:hover,
.bookings-coverage-night.gap:focus-visible {
  border-color: var(--vn-indigo);
  background: var(--vn-indigo-light);
}
/* --tap-min (issue #473 PR8): the bar is deliberately dense — every night of
   the trip visible at once — so the visible cell stays a sliver, but the
   tappable area is expanded via an invisible centered ::before instead of
   growing the cell itself. */
.bookings-coverage-night.gap::before {
  content: '';
  position: absolute;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  width: max(var(--tap-min), 100%);
  height: var(--tap-min);
}
.bookings-coverage-row {
  display: flex;
  align-items: flex-start;
  gap: var(--space-sm);
  /* --vn-amber (the Route Amber fill) is ~3.4:1 as text on this panel's
     ground, below the 4.5:1 AA floor — use the paired ink token, same rule
     DESIGN.md states for amber-as-text everywhere else. */
  color: var(--vn-amber-text);
}
.bookings-coverage-row i { flex-shrink: 0; margin-top: 2px; }

/* Issue [audit 2026-08-24] — the Bookings tab's cards (.itin-stop-card,
   .bookings-coverage-*) already carry the Departure Fever geometry
   (--df-ink keylines, --df-press hard-offset shadows, --df-radius-card),
   but its buttons and body type were still on the retired Field Journal
   system (--radius-sm hairline buttons, the system-font stack) — exactly
   the half-converted state DESIGN.md's migration posture forbids ("a panel
   carrying both a soft blurred card shadow and a hard ink offset reads as
   a bug, not a transition"). Scoped to #panel-bookings only: the shared
   .btn-primary/.btn-secondary base classes and the page ground stay legacy
   until a dedicated app-wide conversion pass — that's a bigger unit of
   work than one tab and isn't taken on here. */
#panel-bookings,
#panel-bookings input,
#panel-bookings select,
#panel-bookings textarea,
#panel-bookings button {
  font-family: var(--df-sans);
}
#panel-bookings .btn-secondary {
  background: var(--df-panel);
  color: var(--df-ink);
  border: 2px solid var(--df-ink);
  border-radius: var(--radius-pill);
  box-shadow: var(--df-press);
  transition: transform var(--dur-feedback) var(--ease-out), box-shadow var(--dur-feedback) var(--ease-out);
}
#panel-bookings .btn-secondary:hover {
  background: var(--df-panel);
  transform: translate(2px, 2px);
  box-shadow: var(--df-press-hover);
}
#panel-bookings .btn-secondary:active {
  transform: translate(4px, 4px);
  box-shadow: var(--df-press-active);
}

/* Sits inline on the stop-summary row, right after the destination text. */
.stop-edit-btn {
  position: relative;
  background: none;
  border: 2px solid var(--vn-teal);
  color: var(--vn-teal);
  border-radius: var(--radius-pill);
  padding: var(--space-xs) var(--space-md);
  font-size: var(--fs-xs);
  font-family: inherit;
  cursor: pointer;
  white-space: nowrap;
  flex-shrink: 0;
}
.stop-edit-btn:hover { background: var(--vn-teal-light); }
/* Same expanded-hit-zone trick as .stop-menu-btn below — this sits with
   enough breathing room next to a card's delete button and chevron
   (unlike the tightly packed .assignee-btn row) that the full --tap-min
   zone doesn't start overlapping a neighbor. */
.stop-edit-btn::before {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: max(var(--tap-min), 100%);
  height: max(var(--tap-min), 100%);
  transform: translate(-50%, -50%);
}

/* Visible box stays small and dense to match .stop-edit-btn/.stop-expand-caret
   in the same row — the ::before pseudo-element below expands the actual
   tappable area to the WCAG 2.2 AA floor without inflating row height or
   nudging neighboring controls (was ~20x22px, well under even the 24x24
   minimum, let alone --tap-min). */
/* .member-menu-btn (js/members.js, issue #642 P1) shares every rule below
   with .stop-menu-btn — same generic dense-row "⋯" trigger, nothing
   stop-specific about the styling. Combined selectors, not a duplicated
   block: each feature still owns a distinct class name so its own
   close*Menu() can never remove or restyle the other's popover (see the
   comment above toggleMemberMenu() in members.js). */
.stop-menu-btn,
.member-menu-btn {
  background: none;
  border: 1px solid var(--df-hair);
  color: var(--df-ink-soft);
  border-radius: var(--df-radius-stub);
  padding: var(--space-xs) var(--space-sm);
  font-size: var(--fs-sm);
  cursor: pointer;
  flex-shrink: 0;
  position: relative;
}
.stop-menu-btn:hover,
.member-menu-btn:hover { background: var(--df-hair); }
.stop-menu-btn::before,
.member-menu-btn::before {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: max(var(--tap-min), 100%);
  height: max(var(--tap-min), 100%);
  transform: translate(-50%, -50%);
}

/* position:fixed with top/right set inline (see positionStopMenu()/
   positionMemberMenu() in itinerary.js/members.js) rather than
   absolute-inside-the-button — the popover is appended to <body> so it's
   never clipped by an ancestor's overflow:hidden (a .day-box's own rounded
   corners for the stop menu; potentially a scrollable Settings panel for
   the member menu). */
.stop-menu-popover,
.member-menu-popover {
  position: fixed;
  min-width: 160px;
  background: var(--df-panel);
  border: 2px solid var(--df-ink);
  border-radius: var(--df-radius-card);
  box-shadow: var(--df-press-large);
  z-index: 1000;
  overflow: hidden;
}
.stop-menu-item,
.member-menu-item {
  display: block;
  width: 100%;
  text-align: left;
  padding: var(--space-md) var(--space-lg);
  font-size: var(--fs-base);
  background: none;
  border: none;
  cursor: pointer;
  color: var(--df-ink);
  font-family: inherit;
}
.stop-menu-item:hover,
.member-menu-item:hover { background: var(--vn-teal-light); }
.stop-menu-item-danger,
.member-menu-item-danger { color: var(--vn-coral); }
.stop-menu-item-danger:hover,
.member-menu-item-danger:hover { background: rgba(153,60,29,0.1); }

/* ── "(!)" disclosure popover (issue #642 P3) — see toggleInfoPopover()
   (js/ui.js) for why this is body-appended/fixed-positioned like the two
   menu popovers above, rather than absolute-inside-the-trigger. */
.set-info-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  flex-shrink: 0;
  border-radius: 50%;
  border: 1px solid var(--df-hair);
  background: var(--surface-1);
  color: var(--df-ink-soft);
  font-size: 11px;
  font-weight: 700;
  line-height: 1;
  cursor: pointer;
  position: relative;
  vertical-align: middle;
}
.set-info-btn:hover { background: var(--df-hair); }
.set-info-btn::before {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: max(var(--tap-min), 100%);
  height: max(var(--tap-min), 100%);
  transform: translate(-50%, -50%);
}
.set-info-popover {
  position: fixed;
  max-width: 260px;
  background: var(--df-panel);
  border: 2px solid var(--df-ink);
  border-radius: var(--df-radius-card);
  box-shadow: var(--df-press-large);
  z-index: 1000;
  padding: var(--space-md) var(--space-lg);
  font-size: var(--fs-sm);
  color: var(--df-ink);
}

/* ── Share permission radios ── */
.share-radio-group {
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
  margin-top: var(--space-md);
}

.share-radio-label {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  font-size: var(--fs-base);
  color: var(--df-ink);
  cursor: pointer;
}

.share-radio-label input[type=radio] {
  width: auto;
  margin: 0;
  accent-color: var(--vn-teal);
}

/* ── Memory section ── */
.memory-section {
  margin-top: var(--space-lg);
  border-top: 2px dashed var(--df-hair);
  padding-top: var(--space-md);
}

.memory-header {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  margin-bottom: var(--space-sm);
}

.memory-add-btn {
  background: none;
  border: 2px solid var(--vn-teal);
  color: var(--vn-teal);
  border-radius: var(--radius-pill);
  padding: var(--space-xs) var(--space-md);
  font-size: var(--fs-xs);
  font-family: inherit;
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: var(--space-xs);
}

.memory-add-btn:hover {
  background: var(--vn-teal-light);
}

.memory-photos {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xl);
  margin-top: var(--space-sm);
  margin-bottom: var(--space-sm);
}

.memory-photo-wrap {
  position: relative;
  width: 72px;
  height: 72px;
  flex-shrink: 0;
}

/* Rounded on the image itself (not the wrap's overflow) so the corner
   delete button can protrude outside the thumbnail without being clipped. */
.memory-photo-img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  cursor: zoom-in;
  border-radius: var(--df-radius-stub);
  display: block;
}

.memory-photo-del {
  position: absolute;
  top: 0;
  right: 0;
  transform: translate(50%, -50%);
  background: var(--df-panel) !important;
  color: var(--df-ink) !important;
  border: 2px solid var(--df-ink);
  box-shadow: var(--df-press);
  border-radius: 50%;
  width: 18px;
  height: 18px;
  font-size: var(--fs-xs);
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  line-height: 1;
  z-index: 2;
}

/* ── Comments (merges what used to be separate review + suggestion features) ── */
.comments-section {
  margin-top: var(--space-lg);
  border-top: 2px dashed var(--df-hair);
  padding-top: var(--space-md);
}

.comments-label {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  font-size: var(--fs-xs);
  font-weight: 600;
  color: var(--vn-teal);
  text-transform: uppercase;
  letter-spacing: 0.5px;
  margin-bottom: var(--space-sm);
}

.comments-list { margin-bottom: var(--space-sm); }

.comments-empty {
  font-size: var(--fs-sm);
  color: var(--df-ink-soft);
  padding: var(--space-xs) 0;
}

.comment-item {
  background: var(--df-panel);
  border: 2px solid var(--df-ink);
  border-radius: var(--df-radius-stub);
  padding: var(--space-md) var(--space-lg);
  margin-bottom: var(--space-sm);
  font-size: var(--fs-sm);
}

.comment-meta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  color: var(--vn-teal);
  margin-bottom: var(--space-xs);
  font-size: var(--fs-xs);
}

.comment-text {
  color: var(--df-ink);
  line-height: 1.4;
  word-break: break-word;
}

.comment-del {
  background: none;
  border: none;
  color: var(--df-ink-soft);
  cursor: pointer;
  font-size: var(--fs-xs);
  line-height: 1;
  padding: var(--space-2xs);
}
.comment-del:hover { color: var(--vn-coral); }

.comment-add-row {
  display: flex;
  gap: var(--space-sm);
  align-items: flex-end;
}

.comment-add-row textarea {
  /* No own font-size (issue #446) — see .pack-reassign-select above; this
     one was confirmed live at 12px on a real 390px viewport, an active
     iOS auto-zoom trigger since the comment box lives inside an expanded
     stop, not caught by an at-rest scan. */
  flex: 1;
  min-height: 32px;
  max-height: 100px;
  border: 2px solid var(--df-ink);
  border-radius: var(--df-radius-stub);
  padding: var(--space-sm) var(--space-md);
  font-family: inherit;
  resize: vertical;
}

.comment-send-btn {
  flex-shrink: 0;
  width: 32px;
  height: 32px;
  border: none;
  border-radius: var(--df-radius-stub);
  background: var(--vn-teal);
  color: var(--surface-2);
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
}
.comment-send-btn:hover { background: var(--vn-teal-mid, var(--vn-teal)); }

.suggestion-loading {
  font-size: var(--fs-sm);
  color: var(--df-ink-soft);
  padding: var(--space-xs) 0;
}

/* ==================== Reduced motion ====================
   Kept last so it wins on source order against every rule above at equal
   specificity. css/skeleton.css and css/landing.css each already carry their
   own block for their own effects; this one covers the app shell and the
   trip screen.

   Not a blanket `* { animation: none }`: the existing 0.12–0.15s color,
   border and background transitions throughout this file are state feedback
   on a control the user is touching, they involve no movement, and taking
   them away costs clarity without buying any relief. What goes is
   everything that travels — the route trace, the panel slide, the marker,
   the toast rise, the sheet — plus the two rotations. Every one of them
   resolves to its finished state, never to a hidden one. */
@media (prefers-reduced-motion: reduce) {
  .itin-trace .day-box,
  .itin-trace .itin-stop-card,
  .itin-trace .stop-connector::before,
  .itin-trace-simple,
  .panel.panel-enter,
  .booking-section-enter,
  .modal.active,
  .modal.active .modal-content,
  .progress-fill,
  .home-trips-trace .home-trip-card,
  .home-trips-trace .home-empty,
  .home-trip-code-copied i,
  .notification-bell-badge-pop,
  .accommodation-confirm-pulse {
    animation: none;
  }

  /* An indeterminate "scanning" shimmer has no real percentage to resolve
     to, so a static 40% bar would misleadingly read as "stuck" — filling
     it to 100% instead says "active" without implying travel or a
     specific, fake progress value. */
  .progress-fill {
    width: 100%;
    transform: none;
  }

  .tabs::after,
  .tab i,
  .stop-expanded-body,
  .stop-expand-caret,
  .booking-section-caret,
  .exp-chevron,
  .exp-card-details,
  .form-more-chevron,
  .form-more-details,
  /* The two determinate progress fills. Their scaleX IS the value — it must
     survive — so only the travel between values is dropped; the bar still
     arrives at the correct length, it just does not slide there. (They were
     `transition: width` and were never answered in this block at all; the
     transform swap is what made the omission worth fixing.) */
  .pack-progress-fill,
  .wizard-progress-segment-fill {
    transition: none;
  }

  /* The marker still moves — it has to, it is the state — but it arrives
     instead of travelling. */
  .tab.active i { transform: none; }

  /* Departure Fever's press: the object moves INTO its own shadow, which is
     travel, however short. It resolves to its FINISHED state — seated, with
     the offset gone — never to a hidden or half-pressed one, so the control
     still visibly acknowledges the tap without anything sliding. The resting
     offset itself is not motion and is deliberately left alone.

     Issue #545 extended this same press mechanic across the trip page's own
     controls (.app-/.df-modal-content-scoped buttons, .expense-item,
     .scan-btn, .settle-action-btn, .x-close-btn) — same rule applies. */
  .home-trip-card,
  .home-action-card,
  .home-error button,
  .app .btn-primary,
  .app .btn-secondary,
  .df-modal-content .btn-primary,
  .df-modal-content .btn-secondary,
  .df-modal-content .modal-close,
  .scan-btn,
  .expense-item,
  .settle-action-btn,
  .x-close-btn {
    transition: none;
  }
  .home-trip-card:hover,
  .home-action-card:hover,
  .home-error button:hover,
  .app .btn-primary:hover,
  .app .btn-secondary:hover,
  .df-modal-content .btn-primary:hover,
  .df-modal-content .btn-secondary:hover,
  .df-modal-content .modal-close:hover,
  .scan-btn:hover,
  .expense-item:hover,
  .settle-action-btn:hover,
  .x-close-btn:hover {
    transform: none;
    box-shadow: var(--df-press-hover);
  }
  .home-trip-card:active,
  .home-trip-card:has(.home-trip-card-open:active),
  .home-action-card:active,
  .home-error button:active,
  .app .btn-primary:active,
  .app .btn-secondary:active,
  .df-modal-content .btn-primary:active,
  .df-modal-content .btn-secondary:active,
  .scan-btn:active,
  .settle-action-btn:active,
  .x-close-btn:active {
    transform: none;
    box-shadow: var(--df-press-active);
  }

  /* translateX(-50%) is this element's centering, not its motion, so it
     survives; only the 8px rise is dropped. */
  .toast-msg,
  .toast-msg.toast-in {
    transform: translateX(-50%);
    transition: opacity var(--dur-feedback);
  }
}
