* {
  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;
}

/* Issue #756 — a skip link is .sr-only until focused, at which point it
   must become the single most visible thing on screen (a keyboard user
   just tabbed to it and needs to see where focus landed). */
.sr-only-focusable:focus {
  position: fixed;
  top: var(--space-md);
  left: var(--space-md);
  width: auto;
  height: auto;
  padding: var(--space-sm) var(--space-lg);
  margin: 0;
  overflow: visible;
  clip: auto;
  white-space: normal;
  z-index: 10000;
  background: var(--vn-teal);
  /* --vn-teal flips to a bright mint in dark mode — #432's own fix, see
     that issue's note further down this file. */
  color: var(--surface-2);
  border-radius: var(--radius-sm);
  font-weight: 600;
}

/* Issue #756 — the avatar preset picker's radio buttons (renderAvatarPresets(),
   js/auth.js). 44px matches --tap-min exactly; a plain-<img> version of this
   used to skip the button chrome entirely and rely on an inline border swap
   for selection. */
.avatar-preset-btn {
  width: var(--tap-min);
  height: var(--tap-min);
  padding: 0;
  border: 2px solid transparent;
  border-radius: 50%;
  cursor: pointer;
  background: none;
  overflow: hidden;
  line-height: 0;
}
.avatar-preset-btn img {
  width: 100%;
  height: 100%;
  border-radius: 50%;
  display: block;
}
.avatar-preset-btn.sel {
  border-color: var(--vn-teal);
}

:root {
  /* Issue #757 — the app's own CSS themes via `light-dark()` (issue #1000),
     but without this, browser-owned chrome (<select> panels,
     date/datetime-local pickers, scrollbars, autofill) stays light-themed
     regardless, against a dark app. The base value here tracks the OS,
     same as before #1000 — it's what a visitor with JS disabled (or before
     the boot script runs) gets. The two overrides right after the token
     list below are what let the account-menu theme toggle (index.html's
     boot script setting `data-theme` on <html>) pin it one way or the
     other regardless of the OS setting. */
  color-scheme: light dark;
  --vn-teal: light-dark(#10702A, #54ff82);
  --vn-teal-light: light-dark(#E3FBEA, rgba(84,255,130,0.16));
  /* Issue #757 — light value was #1E9E52 (white-on-this measured 3.46:1,
     below AA's 4.5:1) everywhere it pairs with `color: var(--surface-2)`
     (white in light mode): .btn-primary:hover, .settle-confirm-btn's
     resting state, .day-box-add-btn:hover, .comment-send-btn:hover.
     #15803d clears ~5:1 against white. The dark value (a bright mint
     paired with a dark foreground per #432) is untouched. */
  --vn-teal-mid: light-dark(#15803d, #7CFFA0);
  /* Tints for a control sitting ON a --vn-teal field (the day-box header's
     Sub-Trip 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 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 dark half is ink, not white — derived
     from --text-primary's dark value (#1a1a1a) with a green bias so it
     reads as a shadow on the brand rather than a grey wash. */
  /* ⚠️ The light-mode 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: light-dark(rgba(255, 255, 255, 0.12), rgba(12, 26, 16, 0.16));
  --on-brand-fill-hover: light-dark(rgba(255, 255, 255, 0.14), rgba(12, 26, 16, 0.28));
  --on-brand-line: light-dark(rgba(255, 255, 255, 0.6), rgba(12, 26, 16, 0.45));
  --on-brand-line-hover: light-dark(rgba(255, 255, 255, 0.95), rgba(12, 26, 16, 0.8));
  --vn-amber: light-dark(#BA7517, #E8A33D);
  --vn-amber-light: light-dark(#FAEEDA, rgba(232,163,61,0.16));
  --vn-amber-mid: light-dark(#EF9F27, #F2B45A);
  /* --vn-amber fails WCAG AA as text in light mode (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. Already clears AA as text on both dark
     surfaces, so the dark half needs no separate darkening (issue #354
     Phase 2 audit). */
  --vn-amber-text: light-dark(#8A5610, #E8A33D);
  --vn-coral: light-dark(#993C1D, #EC8862);
  --vn-coral-light: light-dark(#FAECE7, rgba(226,114,74,0.16));
  --vn-coral-mid: light-dark(#D85A30, #EE9270);
  /* 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. Unchanged between themes, unlike its
     text-safe ink just below. */
  --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 below: --error stays correct as a border/
     background hue, this is the text-safe ink for when it's read as copy.
     The dark value is lightened the same way --vn-teal/--vn-coral lighten
     for dark mode, rather than reusing the light-mode value verbatim. */
  --error-text: light-dark(#B3261E, #FF3B30);
  /* #1805 — fill for a destructive button carrying white text: #B3261E is
     6.5:1 against #fff in both themes (--error is ~4:1, --error-text's dark
     value is a text ink, too bright to fill under white). */
  --error-solid: #B3261E;
  --text-primary: light-dark(#1a1a1a, #f0f0f0);
  --text-secondary: light-dark(#666666, #b3b3b3);
  /* WCAG AA fix (issue #354 Phase 2) — light value #999999 was 2.85:1 /
     2.61:1 against white/--surface-1, well under the 4.5:1 floor for body
     text; the dark value #808080 failed the same audit against dark
     --surface-2 (3.93:1) when it was first set with only light mode
     measured. */
  /* #1030 critique — was a cold neutral grey (#6E6E6E / #949494); DESIGN.md's
     secondary tier is warm, "never a cold grey". Now the same values as
     --df-ink-soft (Green-Black Soft / Lamplight Soft): 7.6:1 on white, 6.2:1
     on Cut Stock, ~7:1 on the dark surfaces, so every use still clears 4.5:1. */
  --text-muted: light-dark(#4a5851, #9aa79f);
  --surface-1: light-dark(#f5f5f5, #1a1a1a);
  --surface-2: light-dark(#ffffff, #242424);
  --border: light-dark(#e0e0e0, #3a3a3a);
  --border-strong: light-dark(#d0d0d0, #4a4a4a);
  --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;

  /* Issue #941 (reverses #873) — #873 gave every desktop panel a shared
     1180px container cap; this replaces it with a shared *prose* measure
     instead. Panels themselves are full-bleed now (see .panel, >=1024px
     block), but a long run of body copy inside one — .section-sub, help
     text, empty-state paragraphs — still shouldn't stretch edge to edge on
     a wide monitor. ~68ch sits in the normal 60-75ch readable range. Applied
     at the text block, never at the panel/container level — that's the
     substitution that makes full-bleed safe for prose without recapping
     everything else. */
  --prose-measure: 68ch;

  /* 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: light-dark(#4338CA, #8B93F5);
  --vn-indigo-light: light-dark(#EEF2FF, rgba(139,147,245,0.16));
  --vn-green: light-dark(#166534, #4ADE80);
  --vn-green-light: light-dark(#F0FDF4, rgba(74,222,128,0.14));
  --vn-green-mid: light-dark(#22883f, #22c55e);
  --vn-purple: light-dark(#7E22CE, #C084FC);
  --vn-purple-light: light-dark(#F5E8FF, rgba(192,132,252,0.16));
  --vn-sky: light-dark(#0284C7, #38BDF8);
  --vn-sky-light: light-dark(#E0F2FE, rgba(56,189,248,0.16));
  /* --vn-sky fails WCAG AA as text in light mode (4.10:1 / 3.76:1, issue
     #354 Phase 2) — same fill/border-vs-text split as --vn-amber-text
     above. Already clears AA as text on both dark surfaces, same reasoning
     as --vn-amber-text. */
  --vn-sky-text: light-dark(#046494, #38BDF8);
  /* 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 dark
     values are lightened against each hue's own 16% tint over dark
     --surface-2 (#242424): 5.29:1, 5.43:1 and 5.42:1 respectively, up from
     the ratios above. Coral's dark 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: light-dark(#993C1D, #EE9270);
  --vn-indigo-text: light-dark(#4338CA, #A8AEF8);
  --vn-purple-text: light-dark(#7E22CE, #CB9AFD);

  /* 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. Main Trip gets a new warm-brown pair; Sub-Trip
     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. */
  --trip-main-accent: light-dark(#8A5A2B, #C9925C);
  --trip-main-bg: light-dark(#F6EEE4, rgba(201,146,92,0.16));

  /* 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. */
  /* Night market (dark half): only ground/stock/ink change here — no brand
     ink is restated, because --df-brand/-coral/-amber alias --vn-* tokens,
     which already flip via light-dark() above. Restating them would be a
     second source of truth for one colour. */
  --df-ground: light-dark(#ffe3c4, #0c1410);   /* Noon Apricot / Night Market — the day ground */
  --df-panel: light-dark(#fff7ec, #151d17);    /* Cut Stock / Night Stock — what a card, stub or panel is cut from */
  --df-ink: light-dark(#101a14, #ffeeda);      /* Green-Black / Lamplight — body text, every keyline; warm, never a cold grey */
  --df-ink-soft: light-dark(#4a5851, #9aa79f); /* 5.9:1 on --df-ground in light mode */
  --df-hair: light-dark(rgba(16, 26, 20, 0.16), rgba(255, 238, 218, 0.18));
  /* 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: light-dark(rgba(16, 26, 20, 0.06), rgba(255, 238, 218, 0.08));
  --df-chip-fill-hover: light-dark(rgba(16, 26, 20, 0.12), rgba(255, 238, 218, 0.16));

  --df-brand: var(--vn-teal);
  --df-brand-fg: var(--surface-2);
  --df-coral: var(--vn-coral);
  --df-amber: var(--vn-amber);
  /* Route Amber Ink — the foreground for anything set ON a --df-amber fill
     (the route's numbered pins). PINNED in both themes, exactly like
     --lp-amber-fg on the landing page and for the same reason: --vn-amber
     LIGHTENS after dark (#BA7517 to #E8A33D), so a theme-aware foreground
     would flip the wrong way and leave dark copy on a lighter fill getting
     lighter. It is the brand green run almost to black rather than a true
     black, so it sits warm on the amber. This is the amber counterpart to
     --df-brand-fg above; do not use it as text on any other ground. */
  --df-amber-fg: #1c1509;

  /* Instant Stock — the film stock a photograph is printed on, and Instant
     Stock Ink for the fine print on its bottom margin. PINNED in both
     themes, like --df-amber-fg above and for a related reason: a print is
     white at midnight too, which is what makes it the brightest object on
     the night page — correct for a photograph of somewhere sunny. Anything
     set on this stock must stay dark in both themes. */
  --df-instant-stock: #fdf6ea;
  --df-instant-stock-ink: #5f6b63;

  /* 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(light-dark(rgba(16, 26, 20, 0.16), rgba(255, 238, 218, 0.09)) 0.9px, transparent 1px);
  --df-wash:
    radial-gradient(85% 60% at 15% -5%, light-dark(rgba(239, 159, 39, 0.30), rgba(232, 163, 61, 0.13)), transparent 60%),
    radial-gradient(75% 55% at 92% 4%, light-dark(rgba(153, 60, 29, 0.11), rgba(226, 114, 74, 0.15)), 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);
  --df-scrim-60: rgba(16, 26, 20, 0.6);
}

/* Issue #1000 — every token that only ever swapped colour between themes was
   folded into a single light-dark() declaration above (and across
   landing.css/discover.css/recap.css/skeleton.css), so the OS-only
   @media (prefers-color-scheme: dark) block that used to live here is gone.
   `data-theme` is set on <html> by index.html's boot script from the
   account-menu theme preference ('system' resolves against the OS and keeps
   listening for changes; 'light'/'dark' pin it). These two rules are what
   let that override the browser-chrome half of the theme (color-scheme
   already flips the light-dark() tokens above via inheritance — no
   selector needed for those). Without JS (or before the boot script runs),
   :root's own `color-scheme: light dark` still tracks the OS, same as
   before #1000. */
:root[data-theme="light"] { color-scheme: light; }
:root[data-theme="dark"] { color-scheme: dark; }

/* --shadow-resting/--shadow-hover are the one pair in this file that can't
   fold into light-dark() — the dark values aren't a recolour of the same
   shadow, they drop from two stacked layers to one, and light-dark() can
   only swap a <color>, not restructure the whole property. Kept as an
   explicit override, gated by data-theme the same way as color-scheme
   above; a visitor with JS disabled keeps the light-mode (two-layer) shadow
   even under a dark OS, which issue #1000 accepted as out of scope. */
:root[data-theme="dark"] {
  --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: background var(--dur-state) var(--ease-out), color var(--dur-state) var(--ease-out), border-color var(--dur-state) var(--ease-out), box-shadow var(--dur-state) var(--ease-out), transform var(--dur-state) var(--ease-out), opacity var(--dur-state) var(--ease-out);
}

/* .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;
}

/* Issue #623 F9 — generic version of css/landing.css's #auth-panel-scoped
   field-error styling, for applyFieldErrors() (js/auth.js) callers outside
   the auth panel (expense, itinerary, accommodation, transport forms). */
input[aria-invalid="true"], select[aria-invalid="true"] {
  border-color: var(--error);
}
input[aria-invalid="true"]:focus, select[aria-invalid="true"]:focus {
  outline-color: var(--error);
  border-color: var(--error);
}
.field-error {
  display: none;
  margin: var(--space-sm) 0 0;
  font-size: var(--fs-xs);
  line-height: 1.5;
  font-weight: 600;
  color: var(--error-text);
}
.field-error.is-visible { display: block; }

.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-error { color: var(--danger, #b3261e); font-size: var(--fs-sm, 13px); margin-top: var(--space-xs, 4px); }
.form-error[hidden] { display: none; }
.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); }

/* Issue #1094 — GeoNames CC BY attribution for the city search box above. */
.geonames-credit {
  margin-top: var(--space-xs);
  font-size: var(--fs-xs);
  color: var(--text-muted);
}
.geonames-credit a { color: inherit; }

.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 #952 — the create actions are one group for show/hide
   (renderItinToolbar()), but their buttons flow in the toolbar's own row
   alongside its other buttons, so the wrapper adds no box of its own. */
.itin-create-actions {
  display: contents;
}
/* A disabled create action's reason, inline and on its own line. */
.itin-create-reason {
  flex-basis: 100%;
  font-size: var(--fs-sm);
  color: var(--text-muted);
}

/* Issue #911 follow-up (code review) — Destination/Dates/Member flex-wrap as
   ONE unit rather than each wrapping independently. Without this wrapper,
   each .itin-member-count-btn is its own flex item in .itin-toolbar, and at
   a narrow width where Create Itinerary + Proposals already fill the row,
   only the overflowing item (Member, being last) wraps — leaving it
   orphaned alone on the next line while Destination/Dates stay behind on
   the row above. margin-left:auto lives here (on the group), not on each
   button, so the whole cluster moves together; flex-wrap here is a safety
   net for a viewport too narrow even for the group's own 3 buttons side by
   side (not hit at any real device width tested). */
.itin-decide-group {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  margin-left: auto;
  flex-wrap: wrap;
}

/* Issue #498 — the permanent member/invite control. flex:0/margin-left:auto
   (not .itin-action-btn's flex:1) so it doesn't compete for width with the
   other toolbar buttons — it's a compact glance badge, not a full action
   button. min-height/min-width match --tap-min directly since the visible
   glyph (icon + 1-2 digit count) is already close to that size; unlike
   .builder-stop-del (#758) there's no smaller visual to preserve underneath
   a ::before expansion. */
.itin-member-count-btn {
  flex: 0 0 auto;
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  min-height: var(--tap-min);
  min-width: var(--tap-min);
  padding: 0 var(--space-lg);
  border: 1.5px solid var(--border);
  border-radius: var(--radius-pill);
  background: var(--surface-2);
  color: var(--text-primary);
  font-size: var(--fs-sm);
  font-weight: 600;
  cursor: pointer;
}
.itin-member-count-btn:hover {
  border-color: var(--border-strong);
}

/* Issue #911 follow-up (code review) — user's design critique: "the Decide
   button is not according to the design.md." Create Itinerary/Proposals
   were still the retired Field Journal (--radius-sm, no keyline, no offset
   shadow) and Decide/member-count were a 1.5px hairline pill — a screen
   DESIGN.md says should read as one printed sheet was carrying three
   different button treatments. Per the user's own choice, converted as one
   unit rather than the Decide button alone (DESIGN.md's migration posture:
   convert a screen/control-group whole, never half). Scoped to
   .itin-toolbar only, mirroring the #panel-bookings precedent below — the
   shared .btn-primary/.btn-secondary base classes stay legacy everywhere
   else until a dedicated app-wide pass. Only shape, type and shadow change
   here; the palette is already shared, so nothing needs a colour edit. */
.itin-toolbar .itin-action-btn,
.itin-toolbar .itin-member-count-btn {
  font-family: var(--df-sans);
  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);
}
.itin-toolbar .itin-action-btn:hover,
.itin-toolbar .itin-member-count-btn:hover {
  border-color: var(--df-ink);
  transform: translate(2px, 2px);
  box-shadow: var(--df-press-hover);
}
.itin-toolbar .itin-action-btn:active,
.itin-toolbar .itin-member-count-btn:active {
  transform: translate(4px, 4px);
  box-shadow: var(--df-press-active);
}
.itin-toolbar .itin-action-btn:disabled,
.itin-toolbar .itin-action-btn:disabled:hover {
  box-shadow: var(--df-press);
  transform: none;
}

.itin-member-row {
  display: flex;
  align-items: center;
  gap: var(--space-lg);
  padding: var(--space-sm) 0;
  border-bottom: 0.5px solid var(--border);
}
.itin-member-row:last-child {
  border-bottom: none;
}
.itin-member-avatar {
  width: 36px;
  height: 36px;
  border-radius: 50%;
  background: var(--surface-1);
  color: var(--text-muted);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--fs-md);
  font-weight: 700;
  flex-shrink: 0;
}
.itin-member-name {
  font-size: var(--fs-base);
  color: var(--text-primary);
}
.itin-member-you {
  color: var(--text-muted);
  font-weight: 400;
}

/* 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 was a third instance until the Bookings tab
   became one continuous route and its Upcoming/All/By-kind filter was
   retired with it.) */
#itin-view-toggle,
#members-subtab-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 {
  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. */
#members-subtab-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 .checklist-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;
}

/* Issue #895 — the legs picked so far, shown above the where:more confirm
   chips so "That's everything" isn't confirming an invisible list. Same
   numbered-pill-plus-remove shape as Settings → General's .set-dest-chip
   (js/settings.js), but drawn in the wizard's own Departure Fever voice
   (--df-panel/--df-ink/--df-press-hover) rather than that screen's flatter
   --df-hair pill, and without the reorder buttons that screen needs and
   this one-shot confirm step doesn't. */
.wizard-leg-chip-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
  margin: var(--space-sm) 0;
}
.wizard-leg-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  padding: var(--space-xs) var(--space-xs) var(--space-xs) var(--space-md);
  background: var(--df-panel);
  border: 2px solid var(--df-ink);
  border-radius: var(--radius-pill);
  box-shadow: var(--df-press-hover);
  font-size: var(--fs-sm);
  font-weight: 600;
  max-width: 100%;
}
.wizard-leg-chip-label {
  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 .set-dest-chip-btn. */
.wizard-leg-chip-remove {
  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);
}
.wizard-leg-chip-remove::before {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: max(var(--tap-min), 100%);
  height: max(var(--tap-min), 100%);
  transform: translate(-50%, -50%);
}
.wizard-leg-chip-remove:hover { color: var(--vn-coral-text); }

.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;
}
/* Issue #896 — 50/50 split, currency and amount each take half the row, the
   same shape as #accommodationPriceCurrency's price/currency pair. This was
   a 30/70 split with a color:transparent select and a code-only badge
   painted on top; at ~105px the badge couldn't contain the label and Blink
   painted the select's own "CODE (Full Name)" through it (see #630). No
   fixed height on either child: they stay normal in-flow so the shared
   input/select rule's @media(max-width:680px) --tap-min bump still reaches
   them (a fixed height here previously pinned this control to 40px, under
   the 44px touch floor — bug #483's class). */
.create-trip-budget-currency-sel {
  flex: 1 1 50%;
  min-width: 0;
}
.create-trip-budget-row input {
  flex: 1 1 50%;
  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); }

/* #1805 — destructive button fill: Alert Red, never Stamp Coral. */
.btn-primary.btn-danger,
.btn-secondary.btn-danger,
.df-modal-content .btn-primary.btn-danger {
  background: var(--error-solid);
  border-color: var(--error-solid);
  color: #fff;
}
.btn-primary.btn-danger:hover,
.btn-secondary.btn-danger:hover,
.df-modal-content .btn-primary.btn-danger:hover { background: var(--error-solid); filter: brightness(0.9); }
.danger-note {
  font-size: var(--fs-sm);
  color: var(--error-text);
  background: var(--surface-2);
  border: 0.5px solid var(--error-text);
  border-radius: var(--radius-sm);
  padding: var(--space-lg);
  margin-bottom: 1rem;
}

/* 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;
}

/* The Route Mark on the Field Green top bars: the all-apricot treatment, the
   only one that goes on green (coral dot never sits on green; brand/logo/
   README.md). No badge ground — apricot on green is the mark. The My Trips
   bar carries the 120px horizontal lockup (the 120px minimum); the
   trip view carries the small-cut symbol alone, since the trip name beside
   it is the page's h1. flex-shrink:0 so a long trip name never squashes it. */
.home-topbar-lockup {
  display: block;
  width: 120px;
  height: auto;
  flex-shrink: 0;
}

.home-topbar-symbol {
  display: block;
  width: 36px;
  height: 36px;
  flex-shrink: 0;
}

.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 */
  /* #546 — a safety net, not a layout: the row's hard floor is 44px × the
     visible tab count (see .tab below), so on a phone too narrow for that the
     bar scrolls sideways rather than pushing Settings off the screen edge.
     At every width the tabs fit, nothing scrolls and nothing changes. The
     travelling marker (::after) is positioned inside this scroll container,
     so it scrolls with the tabs and stays under the active one. */
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
}
.tabs::-webkit-scrollbar { display: none; }

/* #546 — 10px side padding is what tips eight tabs (a finished trip, #892)
   over a 360px phone: 8 × 44px + 2 × 10px = 372px. At 4px it is exactly
   360px, so the common Android width fits without the scroll fallback. */
@media (max-width: 400px) {
  .tabs {
    padding-left: max(var(--space-xs), env(safe-area-inset-left));
    padding-right: max(var(--space-xs), env(safe-area-inset-right));
  }
}

/* The active-tab marker, as one element that travels rather than one per tab
   blinking 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 labelled siblings kept theirs (#544). 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.
   The floor is also a hard minimum on the whole row: 44px × the visible tab
   count, plus the bar's side padding. Collapsing Bookings to icon-only
   (applyTripPhaseUI(), js/ui.js) frees label width, not tab width, so .tabs
   above tightens its padding below 400px (eight tabs fit at 360px) and
   scrolls sideways below that (#546; the tab set itself was decided
   2026-09-26 to stay). */
.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
   (#546, decided 2026-09-26 to keep the current set). */
@media (max-width: 430px) {
  .tab {
    font-size: var(--fs-sm);
  }
}

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

.panel.active {
  display: block;
}

/* Issue #892 — the post-trip Recap tab + panel ship with this class and
   applyTripPhaseUI() (js/ui.js) removes it once tripPhase(tripData) ===
   'past', the same reveal-by-removing-a-class shape as
   css/features/itinerary-map.css's .feature-itinerary-map. !important
   because it has to beat .panel.active's own `display: block` in the brief
   window where a trip's dates are edited back out of the past while the
   viewer is sitting on the Recap panel. */
.tab-phase-hidden {
  display: none !important;
}


/* Tab switching. The tabs are an ordered row (Itinerary → Bookings → Expenses
   → Budget → People → Check List → Recap → Settings, in DOM order, Recap only
   on a finished trip) 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%);
  /* Issue #755 — light-dark() covers the recolour; the border just below
     is what gives separation from whatever's behind it in dark mode
     (--surface-1 is nearly indistinguishable from this background there).
     A transparent border in light mode costs nothing visually — box-sizing
     is border-box app-wide, so it doesn't shift layout either. */
  background: light-dark(#1a1a1a, var(--surface-2));
  color: light-dark(#fff, var(--text-primary));
  border: 1px solid light-dark(transparent, var(--border));
  padding: var(--space-lg) var(--space-4xl);
  border-radius: var(--radius-md);
  font-size: var(--fs-base);
  font-weight: 500;
  z-index: 9999;
  pointer-events: none;
  box-shadow: 0 4px 16px rgba(0,0,0,0.25);
  /* #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. */
/* Issue #755 — #1a1a1a is byte-identical to dark-mode --surface-1, so this
   and .toast-msg above had no visible boundary against the page (body's own
   background) behind them in dark mode. --surface-2 is dark mode's next
   elevation step up, plus a real border rather than relying on the shadow
   alone (a black shadow on a near-black page is itself nearly invisible).
   See .toast-msg above for why the border is safe to declare unconditionally
   (transparent in light mode, border-box sizing app-wide). */
.version-update-banner {
  position: fixed;
  bottom: calc(76px + env(safe-area-inset-bottom)); /* clears the bottom tab bar */
  left: 50%;
  transform: translateX(-50%);
  background: light-dark(#1a1a1a, var(--surface-2));
  color: light-dark(#fff, var(--text-primary));
  border: 1px solid light-dark(transparent, var(--border));
  padding: var(--space-lg) var(--space-xl) var(--space-lg) var(--space-3xl);
  border-radius: var(--radius-md);
  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: var(--radius-sm);
  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;
  }
}

/* Issue #1140 — the consent gate and its toggle rows (js/cookie-consent.js).
   The same rows render in three places: the gate modal, the register form
   (#reg-consent), and Profile → Cookie preferences. Every
   switch has identical visual weight — no pre-coloured or highlighted
   option — so nothing nudges toward "on" (EDPB Guidelines 03/2022 look at
   styling, not order). Replaces #1089's bottom banner and #1131's floating
   "Cookie choices" pill, both removed. */
.consent-toggles {
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
  margin-bottom: var(--space-md);
}

.consent-row {
  display: flex;
  align-items: flex-start;
  gap: var(--space-md);
  justify-content: space-between;
}

.consent-row-text { flex: 1 1 auto; min-width: 0; }

.consent-row-title {
  display: block;
  font-weight: 600;
  font-size: var(--fs-base);
  cursor: pointer;
}

.consent-row-desc {
  margin: 2px 0 0;
  font-size: var(--fs-sm);
  line-height: 1.45;
  color: var(--text-muted);
}

.consent-row-desc a { color: inherit; text-decoration: underline; }

.consent-subrows {
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
  margin-top: calc(-1 * var(--space-xs));
  padding-left: var(--space-lg);
  border-left: 2px solid var(--border);
}

.consent-subrow .consent-row-title { font-weight: 500; font-size: var(--fs-sm); }

.consent-terms-state {
  margin: calc(-1 * var(--space-sm)) 0 0;
  font-size: var(--fs-sm);
  font-weight: 600;
  text-align: right;
}

.consent-note,
.consent-policy-link {
  margin: 0;
  font-size: var(--fs-sm);
  color: var(--text-muted);
}

.consent-policy-link a { color: inherit; text-decoration: underline; }

/* A native checkbox drawn as a switch — keeps checkbox semantics (and the
   master row's `indeterminate` mixed state) for assistive tech. The visible
   track is 44x26; the tap target meets --tap-min via ::before. */
.consent-switch {
  position: relative;
  flex-shrink: 0;
  appearance: none;
  -webkit-appearance: none;
  width: 44px;
  height: 26px;
  margin: 0;
  border-radius: 13px;
  background: var(--border);
  cursor: pointer;
  transition: background var(--dur-feedback) var(--ease-out);
}

.consent-switch::before {
  content: '';
  position: absolute;
  inset: 50% auto auto 50%;
  width: max(var(--tap-min), 100%);
  height: max(var(--tap-min), 100%);
  transform: translate(-50%, -50%);
}

.consent-switch::after {
  content: '';
  position: absolute;
  top: 3px;
  left: 3px;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 1px 2px rgba(0,0,0,0.3);
  transition: transform var(--dur-state) var(--ease-out);
}

.consent-switch:checked { background: var(--vn-green-mid); }
.consent-switch:checked::after { transform: translateX(18px); }
/* Mixed (some sub-categories on): knob centred, track neither colour. */
.consent-switch:indeterminate { background: color-mix(in srgb, var(--vn-green-mid) 45%, var(--border)); }
.consent-switch:indeterminate::after { transform: translateX(9px); }
.consent-switch:focus-visible { outline: 2px solid var(--vn-green-mid); outline-offset: 2px; }

.consent-gate-content { max-width: 480px; }

.consent-gate-intro {
  margin: 0 0 var(--space-lg);
  font-size: var(--fs-sm);
  line-height: 1.5;
  color: var(--text-muted);
}

.consent-gate-actions {
  display: flex;
  gap: var(--space-md);
  margin-top: var(--space-lg);
}

/* margin: 0 — .btn-primary/.btn-secondary each carry their own stacked-
   layout bottom margins, which misalign them side by side. */
.consent-gate-actions > button { flex: 1 1 0; margin: 0; }

.consent-gate-blocked p { margin: 0; line-height: 1.5; }

/* ---- 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);
    overflow: visible; /* the phone bar's #546 scroll fallback has no job in the sidebar */
  }

  .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 content tabs above it, rather than reading as one more
     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 (Check List 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;
  }

  /* Issue #941 (reverses #873's cap) — every desktop panel is full-bleed:
     no max-width here at all, so a .panel simply fills .app-main's content
     box like any other block element. The left edge already stays fixed
     against the sidebar with no margin-inline needed — that's just how a
     block-level div sits in its container, nothing to declare. What used to
     live here (a shared var(--panel-measure) cap, a .panel-wide opt-out for
     Itinerary/Map, Bookings overriding the token narrower) is gone: capping
     the container was standing in for giving each panel's interior
     something to do with the width. See the panels themselves for that —
     .itin-layout's minmax() grid (below), the Bookings card grid, the
     Packing/Budget column splits — and .section-sub below for the prose
     measure that replaces the old container measure for body copy. */
}

/* ==================== Summary Cards ==================== */
.stat-grid {
  margin-bottom: 1rem;
}

/* The standing figures as one boarding-pass stub. Two peer .stat-cards made
   the trip total and the viewer's own spend look like equals and cost a
   second keyline and a second hard shadow on a screen already made almost
   entirely of both. Below the perforation they are stub fields — your two
   numbers, subordinate to the group's one. */
.exp-stub {
  background: var(--df-panel);
  border: 2px solid var(--df-ink);
  border-radius: var(--df-radius-stub);
  box-shadow: var(--df-press);
  overflow: hidden;
}

.exp-stub-head {
  padding: var(--space-2xl) var(--space-3xl);
}

.exp-stub-fields {
  display: flex;
  border-top: 2px dashed var(--df-hair);
}

.exp-stub-field {
  flex: 1;
  min-width: 0;
  padding: var(--space-xl) var(--space-3xl) var(--space-2xl);
}

.exp-stub-field + .exp-stub-field {
  border-left: 2px solid var(--df-hair);
}

/* The stub's own two figures: below the trip total, above body copy.
   Scoped to the field rather than left as a bare `.stat-value-field`, which
   ties on specificity with `.stat-value` below and loses to it on order —
   the rule was silently doing nothing when measured in a real browser. */
.exp-stub-field .stat-value-field {
  font-size: var(--fs-lg);
}

/* #2238 — To pay / To receive under the tear. The whole block is one button
   that opens Balances. */
button.exp-stub-open {
  width: 100%;
  background: none;
  border-left: 0;
  border-right: 0;
  border-bottom: 0;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
  min-height: var(--tap-min);
}
.exp-stub-line {
  display: flex;
  justify-content: space-between;
  gap: var(--space-sm);
  font-size: var(--fs-sm);
}
.exp-stub-name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.exp-stub-amt {
  white-space: nowrap;
  font-weight: 700;
}
.exp-stub-none,
.exp-stub-square {
  font-size: var(--fs-sm);
  color: var(--df-ink-soft);
}
.exp-stub-square {
  padding: var(--space-xl) var(--space-3xl);
}
/* One-line form for the Balances and Shared fund views (below 1024px). */
.exp-stub-compact {
  display: none;
}
@media (max-width: 1023px) {
  .exp-panel[data-exp-view="balances"] .exp-stub-head,
  .exp-panel[data-exp-view="balances"] .exp-stub-fields,
  .exp-panel[data-exp-view="fund"] .exp-stub-head,
  .exp-panel[data-exp-view="fund"] .exp-stub-fields {
    display: none;
  }
  .exp-panel[data-exp-view="balances"] .exp-stub-compact,
  .exp-panel[data-exp-view="fund"] .exp-stub-compact {
    display: block;
    padding: var(--space-xl) var(--space-3xl);
    font-weight: 700;
  }
}

.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,
.exp-stub-head .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);
}
.exp-stub-head .stat-value-stacked {
  font-size: var(--fs-xl);
}
.exp-stub-field .stat-value-stacked {
  font-size: var(--fs-md);
}

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

/* Budget tab "Spent so far" card (#2689, board BudgetScreen). One card style:
   mono eyebrow inside, 2px ink keyline, hard offset shadow. Colours are
   DESIGN.md's only: ink, Route Amber, Amber Ink (#8A5610). */
#budget-spent-card { max-width: 560px; }
.budget-card {
  background: var(--df-panel);
  border: 2px solid var(--df-ink);
  border-radius: 14px;
  box-shadow: 5px 5px 0 var(--df-ink);
  padding: 18px 16px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.budget-card-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.budget-eyebrow { font-family: 'Martian Mono', monospace; font-size: 11px; font-weight: 700; letter-spacing: 0.1em; }
.budget-money-switch { display: flex; padding: 3px; border: 2px solid var(--df-ink); border-radius: 999px; }
.budget-money-switch button {
  min-height: var(--tap-min); min-width: var(--tap-min); padding: 0 12px; border: 0; border-radius: 999px;
  background: none; color: inherit; font-weight: 700; cursor: pointer;
}
.budget-money-switch button[aria-pressed="true"] { background: var(--df-ink); color: var(--df-panel); }
.budget-paid-lines { display: flex; flex-direction: column; gap: 4px; }
.budget-paid-line {
  display: flex; justify-content: space-between; gap: 0.5em; white-space: nowrap;
  font-family: 'Bricolage Grotesque', 'Arial Black', sans-serif; font-weight: 800; font-size: 28px;
  line-height: 1.05; letter-spacing: -0.03em; max-width: 360px;
}
.budget-paid-amt { text-align: right; font-variant-numeric: tabular-nums; }
.budget-hero-home { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.budget-hero-amount {
  font-family: 'Bricolage Grotesque', 'Arial Black', sans-serif; font-weight: 800; font-size: 36px;
  line-height: 1; letter-spacing: -0.03em; font-variant-numeric: tabular-nums;
}
.budget-approx-btn {
  min-width: var(--tap-min); min-height: var(--tap-min); padding: 0 8px; border: 2px solid var(--df-ink);
  border-radius: 999px; background: var(--df-ground); color: var(--df-ink); font-weight: 800; font-size: 22px; cursor: pointer;
}
.budget-sub { font-size: 15px; line-height: 1.4; }
.budget-bar-wrap { position: relative; height: 22px; }
.budget-bar {
  position: absolute; left: 0; right: 0; top: 2px; height: 18px; box-sizing: border-box;
  border: 2px solid var(--df-ink); border-radius: 999px; overflow: hidden; background: var(--df-panel);
}
.budget-bar-fill { position: absolute; left: 0; top: 0; bottom: 0; }
.budget-bar-tick { position: absolute; top: -4px; height: 30px; width: 4px; border-radius: 2px; background: var(--df-ink); }
.budget-bar-none { display: flex; align-items: center; gap: 8px; }
.budget-bar-none-fill { flex: 1; height: 18px; box-sizing: border-box; border: 2px solid var(--df-ink); border-radius: 999px; background: var(--df-ink); }
.budget-infinity { font-family: 'Bricolage Grotesque', sans-serif; font-weight: 800; font-size: 30px; line-height: 1; }
.budget-legend { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }
.budget-legend > div { display: flex; flex-direction: column; gap: 3px; }
.budget-legend-end { align-items: flex-end; }
.budget-legend-label {
  display: flex; align-items: center; gap: 6px;
  font-family: 'Martian Mono', monospace; font-size: 10px; font-weight: 700; letter-spacing: 0.08em;
}
.budget-legend-val { font-size: 15px; font-weight: 700; font-variant-numeric: tabular-nums; }
.budget-swatch { width: 12px; height: 12px; box-sizing: border-box; border: 2px solid var(--df-ink); border-radius: 3px; }
.budget-swatch-empty { background: var(--df-panel); }
.budget-message { margin: 0; font-size: 15px; line-height: 1.45; font-weight: 700; }
.budget-note { margin: 0; padding-top: 10px; border-top: 1.5px dashed var(--df-ink-soft); font-size: 15px; line-height: 1.45; }
.budget-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.budget-link-btn {
  min-height: var(--tap-min); padding: 0 4px; border: 0; background: none; color: var(--vn-teal);
  font-size: 15px; font-weight: 700; text-decoration: underline; text-underline-offset: 3px; cursor: pointer;
}
.budget-conv-list { display: flex; flex-direction: column; border: 2px solid var(--df-ink); border-radius: 12px; overflow: hidden; }
.budget-conv-row { display: flex; justify-content: space-between; padding: 12px 14px; border-bottom: 1.5px dashed var(--df-ink-soft); font-size: 15px; }
.budget-conv-total { background: var(--df-ground); color: var(--df-ink); border-bottom: 0; }
.budget-conv-source { font-size: 14px; line-height: 1.45; }

/* Issue #943 (removes #941's interim cap) — #panel-budget stays full-bleed;
   its content (renderBudget(), js/budget.js) gets the real second axis
   instead. .budget-top-cards holds My Budget and the trip-wide reference:
   two peer cards with no ordering relationship, so a wrap grid is the
   correct axis, same reasoning as Packing's .checklist-columns (#941). Single
   column below the breakpoint for the same reason every other panel's
   second axis is desktop-only: .app-main is a 680px strip below 1024px, so
   there's no real second column to put anything in yet. minmax(280px, 1fr)
   matches .stat-card's own comfortable width rather than a bare 1fr, so two
   cards on a very wide monitor don't stretch to look like empty billboards. */
.budget-top-cards {
  display: flex;
  flex-direction: column;
  gap: var(--space-xl);
}
@media (min-width: 1024px) {
  .budget-top-cards {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
    gap: var(--space-xl);
    align-items: start;
    max-width: 900px;
  }
}


/* Same pattern for the "Other members" list — however many other members
   have shared a budget, each is an independent peer card. auto-fill (not
   auto-fit) so a lone card doesn't stretch to fill the row when there's
   only one other member — it stays its natural width instead of ballooning
   to match .budget-top-cards' pair above it. */
.budget-others-grid {
  display: flex;
  flex-direction: column;
  gap: var(--space-xl);
}
@media (min-width: 1024px) {
  .budget-others-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
    gap: var(--space-xl);
    align-items: start;
    max-width: 900px;
  }
}

/* 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;
}
/* Audit 2026-09-02: the interactive "Mark as booked" chip measured 123x24,
   under the 44px target floor (WCAG 2.5.8). Only the button variant grows —
   the read-only "Pending"/"Booked" span stays compact. */
button.transport-member-status-chip {
  min-height: var(--tap-min);
}
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;
  /* Issue #941 — panels are full-bleed now; this is the prose measure that
     stops a tab's intro line stretching edge to edge on a wide monitor
     instead of a container cap doing it for the whole panel. */
  max-width: var(--prose-measure);
}

/* ==================== 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/checklist.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: nowrap;
  overflow-x: auto;
  gap: var(--space-xs);
  padding-bottom: var(--space-xs);
}

/* #2233 — icon-only chips: 44px square targets, glyph tinted with the
   Category Compass hue even when unselected; the name lives in aria-label. */
.exp-cat-chip-row .exp-cat-chip {
  flex: 0 0 auto;
  justify-content: center;
  min-width: var(--tap-min);
  padding: var(--space-sm);
}
.exp-cat-chip-row .exp-cat-chip i { font-size: var(--fs-xl, 1.25rem); }
.exp-cat-chip.cat-food:not(.sel) i { color: var(--vn-amber-text); }
.exp-cat-chip.cat-transport:not(.sel) i { color: var(--vn-sky-text); }
.exp-cat-chip.cat-shopping:not(.sel) i { color: var(--vn-coral-text); }
.exp-cat-chip.cat-hotel:not(.sel) i { color: var(--vn-indigo-text); }
.exp-cat-chip.cat-activity:not(.sel) i { color: var(--vn-green); }
.exp-cat-from-stop { font-weight: 400; color: var(--df-ink-soft); }

/* #2233 — Split between: summary line and the "Not equal?" link. */
.split-summary { margin: var(--space-sm) 0 0; font-size: var(--fs-sm); color: var(--df-ink-soft); }
.split-notequal-link {
  min-height: var(--tap-min);
  padding: 0;
  border: none;
  background: none;
  color: var(--vn-teal);
  font: inherit;
  font-size: var(--fs-sm);
  text-decoration: underline;
  cursor: pointer;
}
.split-modes { margin-top: var(--space-xs); }
.split-modes .split-btn + .split-btn { border-left: 2px solid var(--df-ink); }
.split-modes .split-btn { padding-left: var(--space-sm); padding-right: var(--space-sm); }

.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. Issue #873 gave
   this panel's own max-width: 1180px to the base .panel rule as
   var(--panel-measure). Issue #941 made full-bleed the *default* for the
   .panel layer and removed that shared token, and kept a 1180px interim
   cap on .exp-panel itself pending a real second axis — but that axis
   already existed (.exp-panel.active's ledger + sticky standing rail
   grid, below), just not checked closely enough when #941 was scoped.
   Issue #943 deletes the interim cap outright rather than replacing it:
   the ledger column (minmax(0, 720px), was minmax(0, 1fr)) now carries
   its own measure directly, so the panel needs no cap of its own — see
   that grid's comment for why the ledger content, not the panel, is what
   got capped. */

/* 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) {
  /* Narrower, not full-bleed. The old rule let this span the row on a phone,
     which made a saturated green slab the loudest object on the screen —
     a role that belongs to the trip total (DESIGN.md's One Shout Rule), and
     one it only got away with while every other object was shouting too. */
  .exp-add-btn {
    padding-left: var(--space-3xl);
    padding-right: var(--space-3xl);
  }

  /* 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;
  }
}

/* ── Category filter chips ────────────────────────────────────
   Wraps instead of scrolling: the old .subtabs-scroll row clipped three of
   eight categories at 390px behind a gradient fade with no hit box. Outlined
   pills, per DESIGN.md's kicker-pill rule that the OUTLINE carries a chip,
   not a fill — so an inactive chip adds a keyline and no ink, and eight of
   them do not become eight blocks of colour. The active chip takes its own
   category hue, the same Category Compass as the ledger spines and the
   breakdown bars. */
.exp-filters {
  display: flex;
  flex-wrap: nowrap;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
  gap: var(--space-sm);
  margin-bottom: 1rem;
  /* room for the 2px keyline + focus ring that overflow would clip */
  padding: 2px;
}
.exp-filters::-webkit-scrollbar { display: none; }

/* #2239 — Shared fund line above the ledger. */
.exp-fund-line {
  display: block;
  width: 100%;
  min-height: var(--tap-min);
  margin-bottom: var(--space-lg);
  padding: 0 var(--space-2xl);
  border: 2px solid var(--df-hair);
  border-radius: var(--radius-pill);
  background: none;
  color: var(--df-ink);
  font-family: inherit;
  font-size: var(--fs-sm);
  text-align: left;
  cursor: pointer;
}
.exp-fund-line:hover { border-color: var(--df-ink); }

/* #2239 — fixed Add pill: bottom right above the trip tab bar, below 1024px,
   on Entries and Balances. The header button covers desktop. */
.exp-add-pill { display: none; }
@media (max-width: 1023px) {
  .exp-add-pill {
    display: inline-flex;
    align-items: center;
    gap: var(--space-sm);
    position: fixed;
    /* .app-main is a centred 680px strip below 1024px, so between 681px and
       1023px (a desktop window at 200% zoom) the viewport edge is not the
       column edge. Percentages keep the pill on the column. */
    right: max(var(--space-lg), env(safe-area-inset-right), calc((100% - 680px) / 2 + var(--space-lg)));
    bottom: calc(76px + env(safe-area-inset-bottom) + var(--space-lg)); /* clears the bottom tab bar */
    z-index: 30;
    width: auto;
    margin: 0;
    min-height: var(--tap-min);
    padding-left: var(--space-3xl);
    padding-right: var(--space-3xl);
  }
  .exp-panel[data-exp-view="fund"] .exp-add-pill { display: none; }
  .exp-panel .exp-ledger-head .exp-add-btn { display: none; }
  /* keep the last ledger row clear of the pill */
  .exp-panel { padding-bottom: 5rem; }
}

.exp-filter {
  display: inline-flex;
  align-items: center;
  gap: var(--space-sm);
  /* #2239 — full --tap-min tall now that the row scrolls sideways instead
     of wrapping: the old 34px chip + 44px ::before overlapped on wrapped rows. */
  min-height: var(--tap-min);
  position: relative;
  flex: 0 0 auto;
  padding: 0 var(--space-xl);
  /* #2231 approved board: a solid 2px ink keyline on cut stock with the
     category icon in its own hue. The hairline + soft-ink version read as
     disabled at phone size. */
  border: 2px solid var(--df-ink);
  border-radius: var(--radius-pill);
  background: var(--df-panel);
  color: var(--df-ink);
  font-family: inherit;
  font-size: var(--fs-md);
  font-weight: 700;
  cursor: pointer;
  transition: border-color var(--dur-feedback) var(--df-ease), color var(--dur-feedback) var(--df-ease), background var(--dur-feedback) var(--df-ease);
}
.exp-filter i { font-size: var(--fs-xl); }
.exp-filter.cmp-food:not(.active) i { color: var(--vn-amber-text); }
.exp-filter.cmp-transport:not(.active) i { color: var(--vn-sky-text); }
.exp-filter.cmp-shopping:not(.active) i { color: var(--vn-coral-text); }
.exp-filter.cmp-hotel:not(.active) i { color: var(--vn-indigo-text); }
.exp-filter.cmp-activity:not(.active) i { color: var(--vn-green); }

.exp-filter:hover {
  background: var(--df-chip-fill-hover);
}

.exp-filter-count {
  font-variant-numeric: tabular-nums;
  font-size: var(--fs-md);
}

.exp-filter.active {
  border-color: var(--df-brand);
  background: var(--df-brand);
  color: var(--df-brand-fg);
}

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

/* ── Breakdown disclosures ────────────────────────────────────
   Both blocks are analysis nobody opens this tab to read, and on a phone
   they were ~400px of always-rendered chart below the ledger. Same animated
   grid-template-rows 0fr<->1fr collapse as .exp-card-details — see that
   block's comment for why the inner wrapper must carry no padding. */
.exp-disclosures {
  background: var(--df-panel);
  border: 2px solid var(--df-ink);
  border-radius: var(--df-radius-card);
  box-shadow: var(--df-press);
  overflow: hidden;
}

.exp-disclosure + .exp-disclosure {
  border-top: 2px dashed var(--df-hair);
}

.exp-disclosure-head {
  width: 100%;
  min-height: var(--tap-min);
  display: flex;
  align-items: center;
  gap: var(--space-lg);
  padding: var(--space-xl) var(--space-2xl);
  background: none;
  border: none;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
}

.exp-disclosure-head:hover {
  background: var(--df-chip-fill);
}

.exp-disclosure-title {
  flex: 1;
  font-family: var(--df-display);
  font-size: var(--fs-lg);
  font-weight: 800;
  letter-spacing: -0.02em;
  color: var(--df-ink);
}

.exp-disclosure-body {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows var(--dur-view) var(--ease-out);
}

.exp-disclosure.open .exp-disclosure-body {
  grid-template-rows: 1fr;
}

.exp-disclosure-inner {
  overflow: hidden;
  min-height: 0;
}

.exp-disclosure-inner > * {
  margin: 0 var(--space-2xl) var(--space-2xl);
}

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

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

/* #2237 — the Entries | Balances | Shared fund switch. Below 1024px only the
   chosen view's blocks show (the panel carries data-exp-view); at 1024px and
   up the two-column layout shows every block and the switch goes away. */
.exp-view-badge {
  display: inline-block;
  min-width: 1.25em;
  padding: 0 0.4em;
  margin-left: var(--space-xs);
  border-radius: 999px;
  background: var(--df-coral);
  color: #fff;
  font-size: var(--fs-xs);
  font-weight: 700;
  line-height: 1.4;
  text-align: center;
}
/* #2231 approved board: a full-width pill track with a 2px keyline; the
   chosen view is a solid ink pill. The buttons carry aria-pressed, not the
   shared toggle's .active class, so the pressed state is styled off that. */
.exp-view-switch {
  width: 100%;
  box-sizing: border-box;
  gap: 2px;
  padding: 3px;
  border: 2px solid var(--df-ink);
  background: var(--df-panel);
}
.exp-view-switch .itin-view-toggle-btn {
  flex: 1;
  min-height: var(--tap-min);
  padding: 0 var(--space-md);
  border: 0;
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--df-ink);
  font-size: var(--fs-lg);
  font-weight: 700;
  white-space: nowrap;
}
.exp-view-switch .itin-view-toggle-btn + .itin-view-toggle-btn { border-left: 0; }
.exp-view-switch .itin-view-toggle-btn[aria-pressed="true"] {
  background: var(--df-ink);
  color: var(--df-panel);
}
@media (max-width: 1023px) {
  .exp-panel[data-exp-view="entries"] [data-exp-view="balances"],
  .exp-panel[data-exp-view="entries"] [data-exp-view="fund"],
  .exp-panel[data-exp-view="balances"] [data-exp-view="entries"],
  .exp-panel[data-exp-view="balances"] [data-exp-view="fund"],
  .exp-panel[data-exp-view="fund"] [data-exp-view="entries"],
  .exp-panel[data-exp-view="fund"] [data-exp-view="balances"] {
    display: none;
  }
}
@media (min-width: 1024px) {
  /* #2244 — the right column switches Balances | Shared fund, and only when a
     fund exists (syncExpViewUI() adds .exp-has-fund). Entries is not an option
     here: the ledger is always on screen, so that button goes. */
  .exp-view-switch-wrap { display: none; }
  .exp-view-switch-wrap.exp-has-fund { display: block; }
  .exp-view-switch [data-exp-view-btn="entries"] { display: none; }
  .exp-panel[data-exp-view="fund"] #exp-balances-block { display: none; }
  .exp-panel:not([data-exp-view="fund"]) .exp-zone-fund { display: none; }
}

/* ==================== Shared fund view (#2244) ====================
   Departure Fever: panel stock, 2px ink keylines, hard offset shadow. No
   hairlines — every divider is a 2px ink keyline. */
.fund-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);
  margin-bottom: var(--space-lg);
  display: flex;
  flex-direction: column;
  gap: var(--space-lg);
}
.fund-left {
  display: flex;
  flex-direction: column;
  gap: var(--space-2xs);
}
.fund-left-label {
  font-family: var(--df-mono);
  font-size: var(--fs-xs);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--df-ink-soft);
}
.fund-left-amount {
  font-family: var(--df-display);
  font-size: var(--fs-2xl);
  font-weight: 800;
  letter-spacing: -0.02em;
  color: var(--df-ink);
}
.fund-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-md);
  font-size: var(--fs-sm);
  color: var(--df-ink-soft);
}
.fund-stats {
  display: flex;
  flex-direction: column;
  gap: var(--space-2xs);
  font-size: var(--fs-sm);
  color: var(--df-ink-soft);
}
.fund-stats > div,
.fund-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--space-sm);
}
.fund-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
}
.fund-row {
  font-size: var(--fs-sm);
  color: var(--df-ink-soft);
}
.fund-row-end {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
}
.fund-muted { color: var(--df-ink-soft); }
.fund-edit-btn {
  font-size: var(--fs-xs);
  padding: var(--space-2xs) var(--space-sm);
}
.fund-section {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  padding-top: var(--space-lg);
  border-top: 2px solid var(--df-ink);
}
.fund-actions,
.fund-form {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-md);
}
.fund-form-label {
  width: 100%;
  font-size: var(--fs-sm);
  color: var(--df-ink-soft);
}
.fund-link-btn {
  align-self: flex-start;
  min-height: var(--tap-min);
  padding: 0;
  border: none;
  background: none;
  color: var(--df-ink);
  font: inherit;
  font-size: var(--fs-sm);
  text-decoration: underline;
  cursor: pointer;
}

@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. */
  /* Issue #943 — the ledger track was minmax(0, 1fr), which grew with
     .exp-panel's own width. That was fine capped at 1180px (#941's interim
     measure); full-bleed on a wide monitor, an uncapped 1fr here stretches
     every ledger row's name/amount pair (.exp-card-left/-right, flex
     space-between) across 1000px+ of mostly empty middle — the exact "an
     expense row measured 1588px" complaint this section's own comment
     above already records, just re-earned a different way. Capped at
     720px instead: a real ledger reading width, not a container measure —
     same reasoning as --prose-measure on .section-sub and the 640px card
     measure on Bookings' route (#941). The grid's own justify-content
     stays default (start), so the standing rail sits right after the
     ledger column ends rather than being stranded far to the right — any
     leftover width just sits outside the whole two-column pair, on the
     panel's right edge, not between its two columns. */
  .exp-panel.active {
    display: grid;
    grid-template-columns: minmax(0, 720px) 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: 3;
    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.
     Both also DEFAULT to open here — the disclosure exists because a phone
     cannot afford ~400px of chart below the ledger, and this column has the
     room. That default is set in JS (syncExpBreakdownDefaults(),
     js/expenses.js), not forced by CSS: a rule that painted a closed section
     open would leave aria-expanded lying about what is on screen. */
  .exp-disclosures {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    column-gap: var(--space-5xl);
    align-items: start;
    background: none;
    border: none;
    box-shadow: none;
    overflow: visible;
  }
  .exp-disclosure {
    background: var(--df-panel);
    border: 2px solid var(--df-ink);
    border-radius: var(--df-radius-card);
    box-shadow: var(--df-press);
    overflow: hidden;
  }
  .exp-disclosure + .exp-disclosure {
    border-top: 2px solid var(--df-ink);
  }
  .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);
}

/* The ledger is ONE sheet, not one card per entry. Every expense used to
   carry its own 2px keyline and 4px hard offset shadow — the same chrome
   .stat-card, .settle-row and .balance-card wear — so forty entries painted
   forty identical objects and nothing on the screen could recede. The sheet
   keeps the keyline and the shadow once, at its own edge, and separates
   entries with the 2px dashed perforation DESIGN.md specifies for a torn
   stub: this screen is a receipt, and it never spent that vocabulary. */
.exp-sheet {
  background: var(--df-panel);
  border: 2px solid var(--df-ink);
  border-radius: var(--df-radius-card);
  box-shadow: var(--df-press);
  overflow: hidden;
}

/* A day rule replaces the day badge that used to repeat on every card:
   struck across the sheet in the Martian Mono ticket voice, carrying that
   day's own subtotal. Ground-tinted rather than filled, so it separates
   without becoming a third kind of object. */
.exp-day-rule {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--space-2xs) var(--space-lg);
  padding: var(--space-md) var(--space-2xl);
  background: var(--df-chip-fill);
  border-bottom: 2px solid var(--df-ink);
  font-family: var(--df-mono);
  font-size: var(--fs-xs);
  text-transform: uppercase;
  letter-spacing: 0.08em;
}
.exp-entry + .exp-day-rule {
  border-top: 2px solid var(--df-ink);
}
.exp-day-rule-label {
  font-weight: 700;
  color: var(--df-ink);
  /* "Day 2: 11 Mar 2026" breaking after "Mar" reads as two days. A
     multi-currency day's totals may take the second line instead. */
  white-space: nowrap;
}
.exp-day-rule-count {
  font-family: var(--font-sans);
  font-size: var(--fs-sm);
  text-transform: none;
  letter-spacing: normal;
  color: var(--df-ink-soft);
}
.exp-day-rule-total {
  margin-left: auto;
  font-weight: 500;
  color: var(--df-ink);
  font-variant-numeric: tabular-nums;
  text-align: right;
}

/* An entry inside the sheet. No border, no radius, no shadow of its own —
   the perforation and the category spine are the whole treatment. */
.exp-entry + .exp-entry {
  border-top: 2px dashed var(--df-hair);
}
.exp-entry {
  transition: background var(--dur-feedback) var(--df-ease);
}

/* The row's category meta, in the ticket voice. Replaces a colour pill, a
   pot badge and a day badge with one line; the hue moves to the spine. */
.exp-entry-meta {
  font-family: var(--df-mono);
  font-size: var(--fs-xs);
  font-weight: 500;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--df-ink-soft);
  margin-top: var(--space-xs);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.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);
}

/* Edit moved off the row and into the expanded detail (renderExpenses()):
   it was ~70px of permanently-visible chrome on every entry — a second
   outlined control per row, competing with the row's own amount and
   squeezing the name and meta line into a truncation at 390px — next to a
   Delete that already only appeared inside the fold. */
.exp-detail-actions-row {
  display: flex;
  justify-content: flex-end;
  gap: var(--space-md);
  border-bottom: none;
  padding-top: var(--space-lg);
}

.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);
}

/* #2235/#2319 — TREAT stamp: the kit's .xk-treat (24px, coral, -8deg). The
   strike plays once, on the treat action only (.is-striking, set by
   treatExpense() for that one render); a stamp already on the row just sits. */
.treat-stamp { pointer-events: none; }
.exp-card-toggle { position: relative; }
.exp-card-right { position: relative; }
/* Struck across the row, anchored to the left edge of the amount group so it
   can overlap the title area but never the figure. */
.treat-stamp-row {
  position: absolute;
  right: 100%;
  top: 8px;
  margin-right: 8px;
  white-space: nowrap;
}
.exp-card-left.has-treat-stamp { min-width: 0; }
.exp-card-left.has-treat-stamp .exp-entry-meta { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
@media (max-width: 680px) {
  /* #2539 — the absolutely-placed stamp sits left of the amount; keep the meta
     line clear of it (stamp ≈ 24px caps, rotated) and truncate instead. */
  .exp-card-left.has-treat-stamp { padding-right: 84px; }
}
.treat-stamp.is-striking { animation: treat-strike var(--dur-state) var(--ease-out) both; }
@keyframes treat-strike {
  from { transform: rotate(-8deg) scale(1.6); opacity: 0; }
  to   { transform: rotate(-8deg) scale(1); opacity: 1; }
}

/* Detail sheet */
.exp-sheet-head { display: flex; align-items: center; gap: var(--space-lg); margin-bottom: var(--space-lg); }
.exp-sheet-icon { font-size: var(--fs-xl); color: var(--df-ink-soft); }
.exp-sheet-sub { font-size: var(--fs-sm); color: var(--df-ink-soft); }
.exp-sheet-amount {
  font-size: var(--fs-4xl, 2rem);
  font-weight: 800;
  display: flex;
  align-items: center;
  gap: var(--space-lg);
}
.exp-sheet-paid { color: var(--df-ink-soft); margin-bottom: var(--space-lg); }
.exp-sheet-label {
  font-family: var(--df-mono);
  font-size: var(--fs-xs);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--df-ink-soft);
  margin: var(--space-lg) 0 var(--space-sm);
}
.exp-sheet-split { list-style: none; margin: 0; padding: 0; }
.exp-sheet-split li {
  display: flex;
  justify-content: space-between;
  padding: var(--space-sm) 0;
  border-bottom: 2px dashed var(--df-hair);
}
.exp-sheet-notes { font-style: italic; }
.exp-sheet-items { margin-top: var(--space-md); }
.exp-sheet-actions {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-md);
  margin-top: var(--space-xl);
}
.exp-sheet-actions .stop-edit-btn,
.exp-sheet-actions .btn-small,
.exp-sheet-more summary { min-height: var(--tap-min); min-width: var(--tap-min); }
.exp-sheet-more summary {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  list-style: none;
  font-size: var(--fs-lg);
}
.exp-sheet-more[open] { display: flex; flex-wrap: wrap; gap: var(--space-md); align-items: center; }

/* 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-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). */
.exp-entry {
  border-left: 5px solid var(--df-ink-soft);
}
.exp-entry.cmp-food      { border-left-color: var(--vn-amber); }
.exp-entry.cmp-transport { border-left-color: var(--vn-sky); }
.exp-entry.cmp-shopping  { border-left-color: var(--vn-coral); }
.exp-entry.cmp-hotel     { border-left-color: var(--vn-indigo); }
.exp-entry.cmp-activity  { border-left-color: var(--vn-green); }
.exp-entry.cmp-other     { border-left-color: 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);
}

/* Settlements are one sheet too, for the same reason the ledger is: three
   free-standing cards stacked above the entries pushed the ledger most of a
   screen down on a phone, and each one repeated the panel's own chrome. */
.settle-sheet {
  background: var(--df-panel);
  border: 2px solid var(--df-ink);
  border-radius: var(--df-radius-card);
  box-shadow: var(--df-press);
  overflow: hidden;
}

.settle-row {
  padding: var(--space-xl) var(--space-2xl);
}

.settle-row + .settle-row {
  border-top: 2px dashed var(--df-hair);
}

/* The sentence and the amount share a line. Apart they cost two lines per
   row for one fact, and the amount — set at --fs-2xl in the display face —
   became a second shout on a screen that already has one in the trip total. */
.settle-row-line {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-lg);
  flex-wrap: wrap;
}

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

/* 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);
  /* Was --fs-2xl. One shout per screen (DESIGN.md): the trip total in the
     standing stub is this tab's, and three settlement amounts at 22px in the
     display face were competing with it from directly above. Still the
     display face and still direction-coloured — just no longer louder than
     everything around it. */
  font-size: var(--fs-lg);
  font-weight: 800;
  letter-spacing: -0.02em;
  color: var(--df-ink);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
/* 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);
}

/* ==================== Check List ==================== */
.checklist-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 (renderChecklist(),
   js/checklist.js) instead of writing a width percentage. --dur-state, since
   this is a state change rather than a tap acknowledgement. */
.checklist-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);
}
.checklist-progress-label {
  font-size: var(--fs-xs);
  color: var(--df-ink-soft);
}

/* Issue #941 — the Check List tab's second axis. Single column below the
   breakpoint (matches every other panel's mobile/tablet behaviour, and
   .app-main is a 680px strip below 1024 anyway, so a two-column grid would
   have nowhere real to put a second column); a genuine two-up grid at
   >=1280px, the same breakpoint .itin-layout grows its own second column
   at. minmax(320px, 1fr) on both tracks, not a bare 1fr each, so neither
   column starves the other and this can't collapse an item name onto two
   lines the way a naive 50/50 split with a long assignee tag could. */
.checklist-columns {
  display: flex;
  flex-direction: column;
}
@media (min-width: 1280px) {
  .checklist-columns {
    display: grid;
    grid-template-columns: minmax(320px, 1fr) minmax(320px, 1fr);
    gap: var(--space-2xl);
    align-items: start;
  }
}
.checklist-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);
}
.checklist-item-top {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  flex-wrap: wrap;
}
.checklist-item-name {
  font-size: var(--fs-base);
  font-weight: 500;
  color: var(--df-ink);
}
/* #2362 — a flight's check-in item: the name opens the flight. */
button.checklist-item-link {
  background: none; border: 0; padding: 0; font: inherit; color: inherit;
  text-align: left; cursor: pointer; text-decoration: underline;
  min-height: var(--tap-min);
}
.checklist-item-chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
  margin-top: var(--space-md);
}
.checklist-item-actions {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  margin-left: auto;
}
.checklist-assignee-tag {
  font-size: var(--fs-xs);
  color: var(--df-ink-soft);
}

/* Issue #727 — "find this" merchant-link menu. A native <details> disclosure
   (no JS positioning machinery, unlike the itinerary "⋯" popover) — the
   dropdown just needs to sit under the icon, not escape a scroll container. */
.checklist-shop-menu {
  position: relative;
}
.checklist-shop-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: var(--tap-min);
  min-height: var(--tap-min);
  border: 2px solid var(--df-ink);
  border-radius: var(--df-radius-stub);
  background: var(--df-panel);
  color: var(--df-ink);
  cursor: pointer;
  list-style: none;
}
.checklist-shop-btn::-webkit-details-marker {
  display: none;
}
/* Issue #935 — "Log price". A quiet text action, not a second stub button: it
   appears on every ticked row (most have no price), so it must weigh less than
   the row's real controls (shop, Edit, delete). Still a full tap target. */
.checklist-log-price-btn {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  min-height: var(--tap-min);
  padding: 0 var(--space-xs);
  border: none;
  background: none;
  font: inherit;
  font-size: var(--fs-sm);
  color: var(--df-ink-soft);
  text-decoration: underline;
  cursor: pointer;
}
.checklist-shop-options {
  position: absolute;
  top: calc(100% + var(--space-2xs));
  right: 0;
  z-index: 5;
  display: flex;
  flex-direction: column;
  min-width: 120px;
  background: var(--df-panel);
  border: 2px solid var(--df-ink);
  border-radius: var(--df-radius-stub);
  box-shadow: var(--df-press);
  overflow: hidden;
}
.checklist-shop-options button {
  min-height: var(--tap-min);
  padding: 0 var(--space-md);
  border: none;
  background: transparent;
  color: var(--df-ink);
  text-align: left;
  font-size: var(--fs-sm);
  cursor: pointer;
}
.checklist-shop-options button:hover,
.checklist-shop-options button:focus-visible {
  background: var(--df-ground);
}

/* Issue #70 — per-item X/Y progress badge and lightweight reassign picker */
.checklist-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);
}
.checklist-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);
}

.checklist-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;
}
.checklist-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. Issue #873 gave this panel's
   own max-width: 1180px to the base .panel rule as var(--panel-measure).
   Issue #941 made full-bleed the *default* for the .panel layer and kept
   a 1180px interim cap on .set-panel itself (shared by #panel-people and
   #panel-settings), on the assumption neither panel had a real second
   axis yet — but both already did (.set-blocks and .set-stub-body's own
   auto-fit grids below, .people-top-grid further down), just not checked
   closely enough when #941 was scoped. Issue #943 deletes the interim cap
   outright: those grids' own containers now carry their own measures
   directly (see each one's comment), so the panel needs no cap of its
   own. */

/* 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 several different widths — the full phone
   column, the 680px cap, and the desktop panel above 1024px — and the
   answer at each is just "as many 280px columns as fit".
   Issue #943 — max-width added. auto-fit's minmax(280px, 1fr) caps a
   column's *floor*, not its ceiling: with only 1-2 real blocks in a given
   sub-tab (AI has one; Sharing has two) and no ceiling, auto-fit collapses
   unused tracks away entirely and stretches the ones that remain to fill
   whatever the container measures — a lone AI block would take the full
   panel width on a wide monitor, a plain radio-group card blown up to
   1500px+. 900px keeps two or three 280px+ columns close to their natural
   size regardless of how much wider the panel around them gets; any
   remaining width just sits outside the grid, not inside a stretched
   block. */
.set-blocks {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: var(--space-5xl);
  align-items: start;
  max-width: 900px;
}

/* 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-trip-name-status {
  display: block;
  min-height: 1.2em;
  margin-top: var(--space-xs);
  font-size: var(--fs-sm);
  color: var(--df-ink-soft);
}
.set-trip-name-status.is-error { color: var(--danger, #b3261e); }

.set-dest-chip-num {
  flex-shrink: 0;
  width: 22px;
  height: 22px;
  border-radius: var(--radius-pill);
  background: var(--df-ink);
  color: var(--df-panel);
  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);
}

/* Issue #1415 — per-leg date rows under the destination chips. */
.set-dest-leg { margin-bottom: var(--space-md); }
.set-dest-leg-name { font-weight: 600; margin-bottom: var(--space-xs); }
.set-dest-leg-note { font-size: var(--fs-sm); color: var(--df-ink-soft); margin-bottom: var(--space-xs); }
.set-dest-leg-range { font-size: var(--fs-base); color: var(--df-ink-soft); }
.set-dest-leg-inputs { display: flex; gap: 6px; align-items: center; }
.set-dest-leg-inputs input { flex: 1; min-width: 0; }
.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. */
/* Issue #943 — max-width added. Before this the stub had none, relying on
   .set-panel's now-deleted 1180px interim cap to keep it from stretching;
   full-bleed, its auto-fit .set-stub-body grid (below) behaves the same as
   .set-blocks' did (see that rule's #943 comment) — few enough fields that
   it can end up as one or two columns blown up to fill the whole panel
   rather than staying close to its own ~190px-per-field intent. Same 900px
   as .set-blocks, for the same reason: this is the trip's own ticket, not
   a full-width form. */
.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;
  max-width: 900px;
}

.set-stub-body {
  display: grid;
  /* Issue #2497 — explicit Name | Dates, Destinations full width below
     (the old fluid grid left Dates alone in a ~199px cell and the date inputs
     unreadably narrow). Dates sit before Destinations because a leg's
     "Outside the trip dates" warning is judged against them. */
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.4fr);
  gap: var(--space-4xl);
  padding: var(--space-4xl) var(--space-4xl) var(--space-3xl);
}
@media (max-width: 680px) {
  .set-stub-body {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* The start/end pair: two flex:1 inputs and a dash (CLAUDE.md date-range
   convention); no per-input font or height overrides. */
.set-stub-date-pair {
  display: flex;
  gap: var(--space-sm);
  align-items: center;
}
.set-stub-date-pair input {
  flex: 1;
  min-width: 0;
}
.set-stub-date-dash {
  color: var(--text-muted);
  font-size: var(--fs-base);
}

/* 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.
   Correction (issue #943): the line this replaced claimed .set-stub-body
   "collapses to a single 1fr column at 1024px+" as if that made this rule
   a no-op on desktop — untrue at this panel's actual desktop width (only
   true, if anywhere, of the pre-#680 sticky-rail layout this stub used to
   sit in, retired when the stub moved inline into General). grid-column:
   1 / -1 is live at every width the stub actually renders at. */
.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 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 — a lower breakpoint would
   flip the columns while the panel itself was still the narrow mobile
   column.
   Issue #943 — the roster track was a bare 1fr, which grew with the panel.
   That was fine capped at 1180px (#941's now-deleted interim measure, see
   .set-panel's own comment); full-bleed, an uncapped 1fr here stretches
   every .member-item row (flex, justify-content: space-between) across
   1000px+ of empty middle between the avatar/name and the row's actions —
   the exact stretched-row problem #941/#943 both exist to fix, just
   re-earned inside this grid instead of at the panel edge. minmax(0, 720px)
   caps the roster track directly, matching the ledger fix in .exp-panel
   (#943's own comment there has the full reasoning): capping the track,
   not a max-width on its contents, is what keeps Access & Permissions
   sitting right next to the roster instead of stranded near the panel's
   far edge — any leftover width sits outside the whole 2-column pair, not
   between its two columns. */
.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: minmax(0, 720px) 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 on --vn-teal's own
     light-dark() definition near the top of this file). */
  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 #943 — found in a real-browser check of the People tab, not in
   either issue's original scope: #rating-list-section sits below
   .people-top-grid (which #943 did account for), a bare block that was
   still stretching full panel width with nothing to say about the extra
   space — one .rating-card per member, no ordering relationship, same
   shape as Budget's .budget-others-grid. auto-fill so a lone card (a
   two-person trip) stays its own width instead of filling the row; the
   per-card margin-bottom above still applies within each grid cell,
   giving a wrapped row the same breathing room a stacked one gets. */
@media (min-width: 1024px) {
  #rating-list-section {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
    gap: var(--space-lg);
    align-items: start;
    max-width: 900px;
  }
}

/* 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 {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: var(--tap-min);
  min-height: var(--tap-min);
  font-size: var(--fs-2xl);
  cursor: pointer;
  color: var(--df-ink-soft);
  transition: color var(--dur-feedback) var(--ease-out), transform var(--dur-feedback) var(--ease-out);
}

.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);
}
/* #2231 — no idle caption above the scan button (it repeated the button's
   own label); the row only shows while a scan or upload reports progress. */
.exp-scan-row-header:has(#exp-scan-status:empty) { display: none; }
.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. */
/* Issue #1720 — the pinned footer overlays the bottom of the scroll area, so
   a chip scrolled/focused to the bottom edge sat underneath it and swallowed
   the click. scroll-padding-bottom is the footer's height (tap target + its
   own vertical padding + the divider), so scrollIntoView()/focus/Playwright
   scrolling stop with the target clear of it. */
.modal-content:has(.exp-modal-sticky-footer) {
  scroll-padding-bottom: calc(var(--tap-min) + 2 * var(--space-md) + 1px);
}
@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);
}

/* Issue #1458 — unequal split rows: name, input, and (shares mode) the
   derived amount. Inputs keep the shared 16px mobile input rule. */
.unequal-panel { margin-top: var(--space-md); display: flex; flex-direction: column; gap: var(--space-sm); }
.unequal-rows { display: flex; flex-direction: column; gap: var(--space-sm); }
.unequal-row { display: flex; align-items: center; gap: var(--space-md); }
.unequal-name { flex: 1; min-width: 0; overflow-wrap: anywhere; }
.unequal-row input { width: 7.5rem; min-height: var(--tap-min); }
.unequal-derived { min-width: 5rem; text-align: right; color: var(--text-muted, inherit); font-size: var(--fs-sm); }
.unequal-remainder { margin: 0; font-size: var(--fs-sm); }
.unequal-remainder.is-off { color: var(--danger, #b3261e); font-weight: 600; }

.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 .checklist-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/checklist.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-side-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/checklist.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 light-dark() pair rather than a new --vn-* token. */
.btn-transfer-host { color: light-dark(#7c3aed, #a78bfa); border-color: light-dark(#c4b5fd, #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 ==================== */
/* Settings (#settings-subtabs) is the only user of this bar now. Expenses'
   category row left it: a genuine sub-tab bar has few enough destinations to
   sit on one line, and Expenses had eight, which is why it needed the
   horizontal scroll and the fade-to-edge cue in the first place. Eight
   filters that all fit is a better answer than eight that scroll, so that row
   is now .exp-filters — wrapping outlined pills. The scroll machinery
   (.subtabs-scroll, its ::after fade, and updateSubtabsScrollFade()) went
   with it; nothing else ever used it. */
.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);
}

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

/* 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);
}

/* Issue #2501 — six uppercase, tracked mono tabs ("Currency & Expenses"
   among them) measure wider than a 390px phone and the scroll fade is gone,
   so the last tab ("Data", which holds export) sat off-screen with no cue.
   Below 680px drop the "& Expenses" tail and tighten padding/tracking so the
   whole bar fits, and wrap onto a second row rather than scroll if the
   real mono face still measures wider (AI on, long locale); the full label
   stays on wider screens. */
@media (max-width: 680px) {
  #settings-subtabs {
    flex-wrap: wrap;
    overflow-x: visible;
  }

  #settings-subtabs .subtab-extra {
    display: none;
  }

  #settings-subtabs .subtab {
    padding-left: var(--space-md);
    padding-right: var(--space-md);
    letter-spacing: 0.04em;
  }
}

/* ==================== 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;
}
/* Issue #1015 harden pass — a generic .status-error rule was never defined
   at this class name; only the ID-scoped rule above existed, so any other
   element carrying just the class (tripPassCheckoutStatus) rendered failure
   text in default ink instead of red. Kept alongside rather than replacing
   the ID-scoped rule to avoid any specificity/behavior change there. */
.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 #911 — the pre-itinerary planning hub (.planning-hub/-header/-cards/
   -destination-line, formerly here) was deleted along with
   renderPlanningHub() — destination/dates moved into the trip header
   instead. Despite the shared class-name prefix, everything below this
   point (.planning-hub-vote-link, .planning-hub-poll-*) belongs to the
   #312 decision board MODAL, which survives — kept. */
.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;
}

/* Issue #911 follow-up (code review) — was a bare 32px icon circle with no
   visible label at all, below the --tap-min floor and giving a first-time
   viewer no clue it was clickable. Now a labeled pill matching the 44px
   floor every other tappable control in this system holds. */
.planning-hub-poll-vote-btn {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  min-height: var(--tap-min);
  padding: 0 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;
  font-size: var(--fs-xs);
  font-weight: 700;
}

.planning-hub-poll-vote-btn i {
  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;
}

/* Issue #911 follow-up (code review) — the destination label and its
   suggester now sit on one row, out of the votes line they were previously
   clumped into ("You · suggested by You"). */
.planning-hub-poll-option-label-row {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--space-sm);
}

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

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

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

/* ==================== 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 {
  /* Issue #758 — background/color used to be force-pinned here, fighting a
     specificity war that no longer exists (originally literal white/black,
     #433 W1-5); no other rule targets .builder-stop-del's background/color,
     so removing the pin is a no-op today. */
  background: var(--df-panel);
  border: 2px solid var(--df-ink);
  box-shadow: var(--df-press);
  color: var(--df-ink);
  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;
  /* Anchors the ::before tap-expansion below. */
  position: relative;
}

/* Issue #758 — 20x20 fails both --tap-min (44px) and WCAG 2.2 AA's 24x24
   floor outright. Same expansion as .modal-close::before: the glyph stays
   20px, only the hit area grows. */
.builder-stop-del::before {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: max(var(--tap-min), 100%);
  height: max(var(--tap-min), 100%);
  transform: translate(-50%, -50%);
}

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

.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 .checklist-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 {
  min-width: var(--tap-min);
  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); }

/* Issue #1000 — System/Light/Dark theme control, styled like a smaller
   .itin-view-toggle (same segmented-pill idiom, --df-brand/-brand-fg being
   the dropdown's own alias of the --vn-teal/--surface-2 pair that toggle
   uses directly) rather than a --home-profile-item row, since it isn't a
   navigation action. Stacked (label above, full-width pill below) rather
   than a single row — the dropdown's own min-width (190px) has no room for
   a "Theme" label plus three 44px tap targets side by side. */
.theme-toggle-row {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  padding: var(--space-sm) var(--space-lg) var(--space-md);
  border-bottom: 2px solid var(--df-hair);
  margin-bottom: var(--space-xs);
}
.theme-toggle-label {
  font-size: var(--fs-xs);
  color: var(--df-ink-soft);
}
.theme-toggle {
  display: flex;
  gap: var(--space-2xs);
  background: var(--df-chip-fill);
  border-radius: var(--radius-pill);
  padding: var(--space-2xs);
}
/* flex:1 rather than a fixed width — three equal shares of the dropdown's
   190px floor (minus its own padding and this row's) come out around 50px
   each, comfortably above --tap-min without risking overflow on a
   narrower future dropdown. */
.theme-toggle-btn {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: var(--tap-min);
  background: none;
  border: none;
  border-radius: var(--radius-pill);
  color: var(--df-ink-soft);
  font-size: var(--fs-lg);
  cursor: pointer;
}
.theme-toggle-btn:hover { background: var(--df-chip-fill-hover); }
.theme-toggle-btn.sel {
  background: var(--df-brand);
  color: var(--df-brand-fg);
}

/* 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 .home-trip-section-list {
    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-trip-section { margin-bottom: var(--space-4xl); }
.home-section-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-xl);
  margin-bottom: var(--space-xl);
}
.home-section-head .home-section-label { margin-bottom: 0; }
.home-section-new,
.home-section-toggle {
  min-height: var(--tap-min);
  padding: 0 var(--space-xl);
  background: none;
  border: 0;
  font-family: var(--df-mono);
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--df-brand);
  cursor: pointer;
}
.home-section-toggle { display: block; margin-top: var(--space-md); }
.home-trip-card[hidden] { display: none; }

.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);
}

/* Issue #298 — Create/Join actions inline in the empty state itself, not
   just a pointer down at the always-visible footer cards. */
.home-empty-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-md);
  margin-top: var(--space-md);
}

/* 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);
}

/* Issue #1560 — hero card: the first Ongoing/Upcoming stub, ~3x a compact
   card. Content sits at the foot so the photo (when it lands) owns the top.
   No travelling entrance of its own: it rides the existing .home-trips-trace
   fade, which already resolves to its finished state under
   prefers-reduced-motion. */
.home-trip-card-hero { min-height: 240px; }
.home-trip-card-hero .home-trip-card-open {
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  gap: var(--space-md);
}
.home-trip-card-hero .home-trip-name { font-size: var(--fs-xl, 1.5rem); }
.home-hero-kicker {
  font-family: var(--df-mono);
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--df-ink-soft);
}
.home-trip-card-photo-visible.home-trip-card-photo .home-hero-kicker { color: #ffeeda; }
.home-trip-card-create { border-style: dashed; }
.home-trip-card-create .home-trip-card-open { justify-content: center; }
@media (min-width: 680px) {
  #my-trips-list .home-trip-card-hero { grid-column: 1 / -1; }
}

/* 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;
}
/* Issue #1562 — hero prep checklist; same stub voice as the settle line. */
.home-trip-prep {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
}
.home-trip-prep-item {
  display: block;
  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);
}
.home-trip-prep-item i {
  font-size: var(--fs-sm);
  margin-right: var(--space-xs);
  vertical-align: -2px;
}
/* Issue #1563 — Past card's open-balance line; same stub voice as the meta row. */
.home-trip-settle {
  display: block;
  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);
}
.home-trip-settle 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-settle,
.home-trip-card-photo-visible.home-trip-card-photo .home-trip-prep-item,
.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);
}

/* shareTripInvite() (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 .home-trip-section-list {
    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] [icon-only category chips] | [zoom -/+]
   (divider, issue #902 — the zoom pair is a separate group, not a sixth
   category filter).
   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);
}

/* Issue #902: separates the Filter-by-category group (the "All" pill +
   category chips) from the unrelated Timeline zoom group, which otherwise
   sits flush against them and reads as one row of five identical filters.
   margin-left: auto pushes the divider — and the zoom group after it — to
   the far end of the rail whenever the chips don't already fill the width;
   once they do (a long trip with every category present), it just follows
   immediately after the last chip, still visually broken by the rule. */
.itin-filter-divider {
  flex-shrink: 0;
  align-self: stretch;
  min-height: var(--tap-min);
  width: 2px;
  margin-left: auto;
  background: var(--df-hair);
}

.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 #952 grew it 32 → 44px
     for the two-line date header (calDayHeaderLabelHtml()); grow both
     together or not at all. Issue #154 Phase 3's plan badge lives inside
     this row too — gap only, height/centering untouched. */
  height: 44px;
  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 #952 — a dated column stacks the date over "Sat (Day 2)". Each
   line ellipsises on its own at the 72px column floor. */
.itin-cal-daycol-label-dated {
  display: flex;
  flex-direction: column;
  line-height: 1.2;
}
.itin-cal-daycol-date,
.itin-cal-daycol-sub {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}
.itin-cal-daycol-sub {
  font-size: var(--fs-xs);
  font-weight: 500;
  color: var(--text-muted);
}
/* Issue #154 Phase 3 — compact clickable A/B flip control. A bare letter,
   not planLabel()'s "Main Trip"/"Sub-Trip" text (used only for title/aria-label):
   the header has no room for it, especially at the 72px column floor. */
/* Visible box stays 16px to fit the header row 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-side-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-side-badge::before {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: max(24px, 100%);
  height: max(24px, 100%);
  transform: translate(-50%, -50%);
}
.itin-cal-side-badge.trip-main { color: var(--trip-main-accent); background: var(--trip-main-bg); }
.itin-cal-side-badge.trip-sub { 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;
}

/* Issue #218 — the travel leg between two consecutive calendar blocks: a
   small pill centred in the gap (or straddling the boundary when the gap is
   zero). Never interactive — the blocks either side own drag/double-click. */
.itin-cal-leg {
  position: absolute;
  left: 0;
  right: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
  z-index: 1;
}
.itin-cal-leg-pill {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  max-width: calc(100% - var(--space-xs));
  padding: 0 var(--space-xs);
  border: 1px solid var(--border);
  border-radius: var(--df-radius-pill, 999px);
  background: var(--surface-2);
  color: var(--df-ink-soft);
  font-size: var(--fs-xs);
  line-height: 16px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.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.trip-main::before,
.itin-cal-block.trip-sub::before,
.itin-cal-chip.trip-main::before,
.itin-cal-chip.trip-sub::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 3px;
  border-radius: 4px 0 0 4px;
  pointer-events: none;
}
.itin-cal-block.trip-main::before, .itin-cal-chip.trip-main::before { background: var(--trip-main-accent); }
.itin-cal-block.trip-sub::before, .itin-cal-chip.trip-sub::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);
}

/* ── Ideas panel: motion ───────────────────────────────────────────────
   The holding pen fills and empties — a suggestion is struck onto the board
   (css/features/idea-pool.css), then it either files into this list or is
   flicked away. These three cover the panel frame itself: it opening, its
   count changing, and a kept idea arriving here taped down. Each is a
   one-shot armed by a real interaction in js/itinerary.js / js/features/
   idea-pool.js and consumed on the next render — never replayed by the
   re-render a stop edit triggers. All answered in the prefers-reduced-motion
   block at the end of this file. */

/* Body unfold — the panel opens from its header edge on a genuine expand
   toggle (_ideasJustToggled). clip-path is composited, so no layout thrash,
   and a height tween is impossible here anyway (content-driven height). The
   hidden start lives only in the keyframe: with the animation gone the body
   is simply shown. */
@keyframes ideas-sidebar-unfold {
  from { opacity: 0; clip-path: inset(0 0 100% 0); }
  to   { opacity: 1; clip-path: inset(0 0 0 0); }
}
.ideas-sidebar-body-reveal {
  animation: ideas-sidebar-unfold var(--dur-view) var(--ease-out);
}

/* Count badge — pops when the number of parked ideas actually changes (kept,
   added, dragged onto a day, deleted). Same curve as .notification-bell-badge-pop. */
@keyframes ideas-count-pop {
  from { transform: scale(0.4); }
}
.ideas-sidebar-count-pop {
  animation: ideas-count-pop var(--dur-feedback) var(--ease-out);
}

/* A kept suggestion lands in this list taped down: the card settles from a
   slight lift while its tape strip (the ::before at ~line 9530) wipes across
   from the left. The wipe keyframe ends on the strip's own resting transform,
   so `both` through the 80ms lead-in resolves to the normal taped look. */
@keyframes idea-card-land {
  from { opacity: 0; transform: translateY(-8px); }
}
@keyframes idea-card-tape-wipe {
  from { transform: rotate(-3deg) scaleX(0); }
  to   { transform: rotate(-3deg) scaleX(1); }
}
.itin-stop-card.idea-card.idea-card-landed {
  animation: idea-card-land var(--dur-state) var(--ease-out);
}
.itin-stop-card.idea-card.idea-card-landed::before {
  transform-origin: left center;
  animation: idea-card-tape-wipe var(--dur-state) var(--ease-out) 80ms both;
}

/* ── Ideas: filters + user-defined groups (issue #734 Parts B, C) ───── */
.ideas-filter-row {
  display: flex;
  gap: var(--space-sm);
  align-items: center;
  padding: var(--space-xs) var(--space-lg) 0;
}
.ideas-filter-select {
  flex: 1;
  min-width: 0;
  min-height: var(--tap-min);
  border: 2px solid var(--df-ink);
  border-radius: var(--radius-md);
  background: var(--df-panel);
  color: var(--df-ink);
  padding: var(--space-xs) var(--space-sm);
  font: inherit;
  font-size: var(--fs-xs);
}
.ideas-filter-clear {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: var(--tap-min);
  min-height: var(--tap-min);
  border: 2px solid var(--df-ink);
  border-radius: var(--radius-md);
  background: var(--df-panel);
  color: var(--df-ink);
  cursor: pointer;
}
.ideas-filter-clear:hover { background: var(--vn-teal-light); }

/* A group subheading inside the single #itin-ideas-sidebar .day-box-stops
   container — a plain divider row, not its own drop target (drag-between-groups
   is deliberately out of scope; groups are set via the modal / "⋯" menu). */
.ideas-group-header {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  margin-top: var(--space-sm);
  padding-bottom: var(--space-2xs);
  border-bottom: 2px solid var(--df-ink);
  font-family: var(--df-mono);
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--df-ink-soft);
}
.ideas-group-header:first-child { margin-top: 0; }
.ideas-group-name { flex: 1; min-width: 0; }
.ideas-group-count {
  flex-shrink: 0;
  color: var(--df-ink);
  background: var(--vn-teal-light);
  border-radius: var(--radius-pill);
  padding: 0 var(--space-sm);
}
.ideas-group-rename {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: var(--tap-min);
  min-height: var(--tap-min);
  border: none;
  background: none;
  color: var(--df-ink-soft);
  cursor: pointer;
}
.ideas-group-rename:hover { color: var(--vn-teal); }

/* The idea's group, shown on its expanded card. */
.stop-idea-group {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  font-size: var(--fs-xs);
  color: var(--df-ink-soft);
  margin-top: var(--space-xs);
}

/* ── Idea "About" disclosure (issue #734 Part E) ───────────────────── */
.idea-about { margin-top: var(--space-sm); }
.idea-about-toggle {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  min-height: var(--tap-min);
  border: none;
  background: none;
  color: var(--vn-teal);
  font: inherit;
  font-size: var(--fs-xs);
  cursor: pointer;
  padding: 0;
}
.idea-about-body {
  margin-top: var(--space-2xs);
  font-size: var(--fs-sm);
  line-height: 1.5;
  color: var(--df-ink);
}

/* ── Add-from-ideas picker (issue #734 Part F) ─────────────────────── */
.idea-picker-open-btn {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  margin-top: var(--space-xs);
  border: none;
  background: none;
  color: var(--vn-teal);
  font: inherit;
  font-size: var(--fs-xs);
  cursor: pointer;
  padding: 0;
  min-height: var(--tap-min);
}
.idea-picker-open-btn:hover { text-decoration: underline; }

.idea-picker-section-label {
  font-family: var(--df-mono);
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--df-ink-soft);
  margin: var(--space-md) 0 var(--space-xs);
}
.idea-picker-section-label:first-child { margin-top: 0; }
.idea-picker-group {
  font-size: var(--fs-xs);
  color: var(--df-ink-soft);
  margin: var(--space-sm) 0 var(--space-2xs);
}
.idea-picker-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  width: 100%;
  min-height: var(--tap-min);
  padding: var(--space-sm) var(--space-md);
  margin-bottom: var(--space-xs);
  border: 2px solid var(--df-ink);
  border-radius: var(--radius-md);
  background: var(--df-panel);
  color: var(--df-ink);
  font: inherit;
  text-align: left;
  cursor: pointer;
}
/* A suggestion is a COPY (dashed, matching .idea-suggestion-card); an own idea
   MOVES (solid). The two must not look identical (PRD §7). */
.idea-picker-suggestion { border-style: dashed; }
.idea-picker-row:hover { background: var(--vn-teal-light); }
.idea-picker-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.idea-picker-tag {
  flex-shrink: 0;
  font-size: var(--fs-xs);
  color: var(--df-ink-soft);
}

/* ── 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 #951 — a day whose "Day N" now exceeds the trip's own date range
   (after a shrink, or a stop added past the trip's end). Plain fact, not a
   warning — no icon, no color, no tap target — sitting below the whole
   header row rather than beside the title, so it never competes with the
   already-crowded button cluster on the right. */
.day-box-outside-range {
  font-size: var(--fs-sm);
  color: var(--text-muted);
  margin-bottom: var(--space-xs);
}

/* Issue #952 Phase 4b — the day's destination, directly under the header.
   Read-only it's a stub destination, so it takes the Martian Mono label
   voice (DESIGN.md: "stub destinations"); a pin, not a colour, marks it —
   green stays for things you press. A one-word place name longer than the
   box wraps rather than being cut mid-word. */
.day-box-dest,
.day-box-dest-field {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  padding: var(--space-md) var(--space-2xl) 0;
  color: var(--text-muted);
}
.day-stay-chip-slot:empty { display: none; }
.day-stay-chip-slot { padding: 0 var(--space-md) var(--space-sm); }
.day-stay-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  min-height: var(--tap-min);
  padding: 0 var(--space-md);
  border: 1px dashed var(--border);
  border-radius: 999px;
  background: transparent;
  color: var(--text-muted);
  font-size: var(--fs-sm);
  cursor: pointer;
}
.day-stay-chip-deciding { border-style: solid; color: var(--text); }
.day-stay-chip:hover { color: var(--text); }

.day-box-dest {
  font-family: var(--df-mono);
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  overflow-wrap: anywhere;
}
/* The host's control is the same stub, made editable — not the shared
   input/select look (the retired Field Journal grey: 0.5px #e0e0e0 border,
   #f5f5f5 fill, which also failed WCAG 1.4.11's 3:1 field-boundary
   contrast). Departure Fever's field vocabulary instead: a 2px ink keyline
   on the stub's 12px radius, transparent so the card shows through, sized
   to its content rather than the full row. Font-size and height are NOT
   overridden here: those stay on the shared rule (16px / --tap-min under
   680px, so no iOS zoom), per CLAUDE.md's one-shared-rule convention. */
.day-box-dest-field .day-dest-select {
  flex: 0 1 auto;
  width: auto;
  min-width: 0;
  max-width: min(100%, 22rem);
  min-height: var(--tap-min);
  font-family: var(--df-mono);
  font-weight: 500; /* quieter than the label's 700: at the shared 16px it must not outweigh the day title */
  letter-spacing: 0.06em;
  text-transform: uppercase;
  text-overflow: ellipsis;
  color: var(--df-ink);
  background: transparent;
  border: 2px solid var(--df-ink);
  border-radius: var(--df-radius-stub);
}
/* An untagged day: the same stub, ghosted — dashed keyline, secondary ink
   — so untagged days scan as different from tagged ones at a glance. */
.day-box-dest-field .day-dest-select-unset {
  border-style: dashed;
  color: var(--text-muted);
}

/* The day destination sheet (#dayDestModal). */
.day-dest-sheet-sub {
  font-size: var(--fs-base);
  color: var(--text-muted);
  margin-bottom: var(--space-xl);
}
/* The browser's default placeholder grey measured 4.34:1 (light) / 3.74:1
   (dark) here; the secondary-text tier clears 4.5:1 in both themes. */
#dayDestName::placeholder {
  color: var(--text-muted);
  opacity: 1;
}
.day-dest-sheet-hint {
  margin-top: var(--space-sm);
  font-size: var(--fs-sm);
  color: var(--text-muted);
}
.day-dest-sheet-submit {
  width: 100%;
  margin-top: var(--space-xl);
}

/* 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 Sub-Trip'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 / Sub-Trip 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 — "Sub-Trip" 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-split-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-split-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-split-btn:hover { background: var(--df-chip-fill-hover); }

/* Issue #952 Phase 3 — "Remove day" sits in the dense day header beside
   "Split Day", so it borrows that pill and its expanded hit area. Quiet on
   purpose: it only ever removes an empty last day. */
.day-box-remove-day-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 var(--dur-feedback) var(--ease-out);
  position: relative;
}
.day-box-remove-day-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-remove-day-btn:hover { background: var(--df-chip-fill-hover); }

/* Issue #952 Phase 3 — "+ Add day" closes the day list on a dateless trip:
   a dashed "next day" slot the width of a day box, not a loud button — the
   blank day boxes are the surface. */
.itin-add-day-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-sm);
  width: 100%;
  min-height: var(--tap-min);
  margin-top: var(--space-md);
  padding: var(--space-lg);
  background: transparent;
  border: 2px dashed var(--df-hair);
  border-radius: var(--df-radius-card);
  color: var(--df-ink);
  font-family: inherit;
  font-size: var(--fs-sm);
  font-weight: 700;
  cursor: pointer;
  transition: border-color var(--dur-feedback) var(--ease-out);
}
.itin-add-day-btn:hover { border-color: var(--df-ink); }

/* 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;
}

/* Issue #952 Phase 5 — "Autofill with <city> ideas", under the empty-day
   hint. A secondary sticker (Cut Stock fill, 2px ink keyline, pill), not
   green: the filled "Add stop" below stays the day's one primary action.
   Dense scale per DESIGN.md's migration posture — the offset steps down to
   2px and presses into it. */
.day-box-autofill-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-sm);
  margin: 0 auto var(--space-xl);
  min-height: var(--tap-min);
  padding: 0 var(--space-4xl);
  font-family: inherit;
  font-size: var(--fs-sm);
  font-weight: 700;
  color: var(--df-ink);
  background: var(--df-panel);
  border: 2px solid var(--df-ink);
  border-radius: var(--radius-pill);
  box-shadow: 2px 2px 0 var(--df-ink);
  cursor: pointer;
  transition: transform var(--dur-feedback) var(--ease-out), box-shadow var(--dur-feedback) var(--ease-out);
}
.day-box-autofill-btn:hover { transform: translate(1px, 1px); box-shadow: 1px 1px 0 var(--df-ink); }
.day-box-autofill-btn:active { transform: translate(2px, 2px); box-shadow: 0 0 0 var(--df-ink); }
.day-box-autofill-btn:disabled { opacity: 0.6; cursor: progress; transform: none; box-shadow: 2px 2px 0 var(--df-ink); }

.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-redo-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,
.day-box-redo-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,
.day-box-redo-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-redo-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 gave "+ Add stop" a solid teal fill (the .btn-primary
   treatment) so it wouldn't read as equal-weight with the neutral "Open day
   route in maps" beside it. Issue #903 dials that back: on a day with few or
   no stops, a full-bleed solid-green bar repeated once per card outweighed
   the actual stops — quiet text rows — so the empty scaffolding read as more
   prominent than the plan itself. A tinted ghost keeps just enough color to
   still out-rank the plain-grey .day-box-route-btn next to it (without that,
   the two collapse back into #447's original "read as equals" problem) while
   no longer competing with real content for attention.
   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 pick up this tint either. */
.day-box-actions-row .day-box-add-btn {
  background: var(--vn-teal-light);
  color: var(--vn-teal);
  font-weight: 600;
  border-top-color: var(--vn-teal);
}
.day-box-actions-row .day-box-add-btn:hover { background: var(--vn-teal-mid); color: var(--surface-2); }

/* ── 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;
  /* Issue #803 — the Ideas column's own width, dragged via #ideas-col-resizer
     the same way --day-map-width is dragged via .day-map-resizer. 340px is
     today's old fixed ceiling, kept as the default so a user who never drags
     sees an unchanged layout. IDEAS_COL_WIDTH_MIN/MAX (js/itinerary.js) is
     the clamp the drag itself enforces; the minmax() at this var's one call
     site below is a floor only, not a second clamp. */
  --ideas-col-width: 340px;
}

/* 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; }

/* Issue #803 — same reasoning, one breakpoint over: below 1280px the Ideas
   sidebar isn't a grid column at all (it's a stacked block, see .itin-layout
   below), so its resize handle plays no role either. */
#ideas-col-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 every track, 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 (--ideas-col-width's default) 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.
       Issue #803 — the middle "." / "resizer" column carries #ideas-col-resizer,
       the drag handle for --ideas-col-width; 260px here is a floor
       independent of IDEAS_COL_WIDTH_MIN (js/itinerary.js) — the var is
       already clamped there, this is a backstop for a var CSS can't trust. */
    grid-template-columns:
      minmax(calc(var(--day-stops-min) + var(--day-map-width) + 6px), 1fr)
      6px
      minmax(260px, var(--ideas-col-width));
    grid-template-areas:
      "filter  .        ."
      "daylist resizer  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;
    /* Issue #803 — the base rule's single `gap` (below the .itin-layout
       declaration up top) would otherwise apply as column-gap on *both*
       sides of the new 6px resizer column, doubling what used to be one
       14px gutter into 34px and eating into the day list's own floor for no
       visual gain. The 6px resizer column is the gutter here, the same way
       .day-box:has(.day-map-section-has-route) uses its own resizer column
       as the only spacing between stops and the map, with no separate gap
       at all.
       row-gap keeps the filter row's own breathing room untouched. */
    column-gap: 0;
  }
  #itin-ideas-sidebar {
    position: sticky;
    top: 80px;
    max-height: calc(100vh - 100px);
    overflow-y: auto;
  }
  /* Issue #803 — spans the same sticky viewport slice as the sidebar it
     resizes, not the day list's full (often much taller) height, so the
     drag handle only ever covers the visible panel. */
  #ideas-col-resizer {
    grid-area: resizer;
    display: block;
    position: sticky;
    top: 80px;
    height: calc(100vh - 100px);
    cursor: col-resize;
  }
  #ideas-col-resizer::after {
    content: '';
    position: absolute;
    top: 0;
    bottom: 0;
    left: 2px;
    width: 2px;
    border-radius: 1px;
    background: var(--df-hair);
    transition: background 0.15s;
  }
  #ideas-col-resizer:hover::after,
  #ideas-col-resizer.dragging::after {
    background: var(--vn-teal);
  }
  /* Widen the tappable/draggable zone to --tap-min without widening the
     visible 2px line — same convention as .stop-menu-btn (this file). */
  #ideas-col-resizer::before {
    content: '';
    position: absolute;
    top: 0;
    bottom: 0;
    left: 50%;
    width: max(var(--tap-min), 100%);
    transform: translateX(-50%);
  }
  #ideas-col-resizer:focus-visible {
    outline: 2px solid var(--vn-teal);
    outline-offset: 2px;
  }

  /* Issue #311 — hidden (see the base-layout rule above), the column
     itself needs to go too, or the day list gets stranded in a narrower 1fr
     track with a bare empty gap beside it. grid-template-areas must be
     overridden here too, not just grid-template-columns — the desktop base
     rule above declares a 3-column "daylist resizer ideas" area, and leaving
     that in place implicitly adds back the missing columns (auto-sized) the
     moment grid-template-columns lists fewer, since the areas imply more
     columns than the explicit track list provides. Those implicit columns
     still pick 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. #ideas-col-resizer itself is
     display:none'd below rather than left an orphaned resizer grid item with
     nothing declaring its area. */
  .itin-layout:has(#itin-ideas-sidebar.itin-ideas-hidden) {
    grid-template-columns: 1fr;
    grid-template-areas:
      "filter"
      "daylist";
  }
  .itin-layout:has(#itin-ideas-sidebar.itin-ideas-hidden) #ideas-col-resizer,
  .itin-layout:has(#itin-ideas-sidebar.collapsed) #ideas-col-resizer {
    display: none;
  }

  /* 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 width 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.
     grid-template-areas is redeclared here too, same reason as the
     itin-ideas-hidden override just above — the resizer column drops out
     entirely (and #ideas-col-resizer is display:none'd above) rather than
     leaving a draggable handle beside a 44px strip there's nothing left to
     widen. */
  .itin-layout:has(#itin-ideas-sidebar.collapsed) {
    grid-template-columns: 1fr 44px;
    grid-template-areas:
      "filter  ."
      "daylist ideas";
  }
  #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 Main Trip/Sub-Trip 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 Main Trip / I'm on Sub-Trip" 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. */
  /* Issue #903 — scoped to .day-map-section-has-route (set in JS only when
     buildDayMapUrl() resolves, i.e. 2+ locatable stops), not bare
     .day-map-section. The section itself always renders (its delete-zone is
     load-bearing at any stop count — see the comment on renderDayMapBody()),
     so keying off its mere presence gave every day box this two-column
     layout and a permanently-visible "Add at least 2 stops to see a route"
     panel eating a third of the card's width, empty day or not. A day
     without a route yet now stays single-column and reclaims that width;
     the section still exists in the DOM (for the delete-zone) but falls
     back to the base rule's collapsed state below, with nothing to expand it
     since .day-box-route-btn (the only control that sets .expanded) is
     itself only rendered when a route exists. */
  .day-box:has(.day-map-section-has-route) {
    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-split-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-box:has(.day-map-section-has-route) .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-has-route {
    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-has-route::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-section-has-route .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-has-route) grid override above) — filling
     it avoids a mismatched short zone floating in a tall column. Scoped the
     same way: a routeless day's delete-zone isn't part of that grid, so it
     falls back to the base rule's min-height:60px instead. */
  .day-map-section-has-route.delete-armed::before { display: none; }
  .day-box:has(.day-map-section-has-route) .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 — Main Trip/Sub-Trip 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.trip-main { background: var(--trip-main-bg); box-shadow: inset 3px 0 0 var(--trip-main-accent); }
.itin-stop-card.trip-sub { 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(.trip-main):not(.trip-sub) because a Main Trip/Sub-Trip 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(.trip-main):not(.trip-sub) {
  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-split-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-split-badge {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  color: var(--df-ink-soft);
  font-weight: 600;
}
.day-split-flip-row.trip-main .day-split-badge { color: var(--trip-main-accent); }
.day-split-flip-row.trip-sub .day-split-badge { color: var(--vn-sky-text); }
.day-split-flip-btn.trip-main { color: var(--trip-main-accent); border-color: var(--trip-main-accent); }
.day-split-flip-btn.trip-sub { color: var(--vn-sky-text); border-color: var(--vn-sky); }

/* Issue #154 Phase 2 — self-declared roster controls in the flip row. */
.day-split-roster-declare { display: flex; align-items: center; gap: var(--space-sm); flex-wrap: wrap; }
.day-split-declare-btn.trip-main.active { background: var(--trip-main-accent); color: #fff; border-color: var(--trip-main-accent); }
.day-split-declare-btn.trip-sub.active { background: var(--vn-sky); color: #fff; border-color: var(--vn-sky); }
.day-split-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; }

/* The label half of a category pill — now only the Discover card's (#2478
   made the itinerary stop card's pill icon-only, a 30px circle, see
   .itin-stop-card .cat-pill).
   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. */
.itin-stop-fixed .stop-drag-grip { display: none; }
.itin-cal-block.itin-stop-fixed, .itin-cal-chip.itin-stop-fixed { cursor: pointer; }
.itin-cal-block-lock { font-size: var(--fs-xs); margin-right: 2px; }
.stop-open-booking { min-height: var(--tap-min); }
.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%);
}

/* Issue #693 Part A — same non-interactive shape as .stop-booking-signal
   above (tapping the row already expands the card). --vn-indigo is the
   Category Compass's real hotel hue (.cat-hotel), unlike .stop-booking-signal's
   own use of it, which is generic to any tagged booking — reused here on
   purpose since this chip specifically IS about accommodation. */
.stop-accommodation-signal {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2xs);
  color: var(--vn-indigo);
  font-weight: 600;
  white-space: nowrap;
  position: relative;
}
.stop-accommodation-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;
}

/* Issue #901 — first line of a stop's note, shown on the collapsed row so a
   group doesn't have to expand the card to see a meeting point or other
   detail someone just typed. Unlike .stop-name-subtitle above, this is NOT
   gated behind the 1024px breakpoint: the whole point is "visible without
   reopening the row", at any width. */
.stop-note-preview {
  font-size: var(--fs-xs);
  color: var(--df-ink-soft);
  font-style: italic;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Issue #734 Part A — a small per-card marker so a Quick Idea
   (.itin-stop-card.idea-card, this trip's own unscheduled stop) and an
   idea-pool suggestion (.idea-suggestion-card, css/features/idea-pool.css)
   identify themselves independent of whichever section header they happen
   to render under. One shared base + a modifier per side, not two
   unrelated classes, since both are answering the exact same question. */
.idea-type-badge {
  display: inline-block;
  align-self: flex-start;
  padding: 1px var(--space-sm);
  border-radius: var(--radius-pill);
  font-size: var(--fs-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  line-height: 1.5;
}
.idea-type-badge-quick {
  background: var(--vn-teal-light);
  color: var(--vn-teal);
}

/* 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 .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; }

/* #2039 — community diet labels + report control on a food stop. */
.stop-community-diet { font-size: var(--fs-xs); margin-bottom: var(--space-xs); display: flex; flex-direction: column; gap: var(--space-xs); }
.community-diet-line { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-xs); }
.community-diet-line a { color: var(--vn-teal); }
.community-diet-title { font-weight: 600; }
.community-diet-row { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-xs); }
.community-diet-q { min-width: 7.5rem; }
.community-diet-btn { min-height: var(--tap-min); min-width: var(--tap-min); padding: 0 var(--space-sm); border: 1px solid var(--border-strong); border-radius: var(--radius-sm, 6px); background: transparent; color: inherit; cursor: pointer; }
.community-diet-btn.active { background: var(--vn-teal); color: var(--surface-2); border-color: var(--vn-teal); }
.community-diet-reason { min-height: var(--tap-min); max-width: 100%; }
.community-diet-note { color: var(--text-muted); }

/* 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 var(--dur-feedback) var(--ease-out);
}
.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;
}

/* Issue #825 — the 12Go affiliate link-out card in the Add Transport sheet.
   Deliberately plain (no icon/photo — D1 has no image to show, #823 §5.5),
   and no "best price"/"deals" framing per #823 §5.4/§5.9's disclosure rule. */
.affiliate-offer-card {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  padding: var(--space-md);
  margin-bottom: var(--space-md);
  border: 2px solid var(--df-hair);
  border-radius: var(--df-radius-card);
  background: var(--df-panel);
}
.affiliate-offer-title {
  font-weight: 600;
  font-size: var(--fs-sm);
}

/* Issue #824 — the sandboxed Klook widget's container. margin-top, not
   margin-bottom on the sidebar above it: #itin-ideas-klook-widget is a
   sibling of #itin-ideas-sidebar (js/itinerary.js's own render function
   controls this element independently — see the comment there), and the
   sidebar's own bottom margin/border already exists regardless of whether
   this section is showing. */
.affiliate-widget-section {
  margin-top: var(--space-md);
  padding: var(--space-md);
  border: 2px solid var(--df-hair);
  border-radius: var(--df-radius-card);
  background: var(--df-panel);
}
.affiliate-widget-label {
  font-weight: 600;
  font-size: var(--fs-sm);
  margin-bottom: var(--space-sm);
}
/* #1099 — commission disclosure line (affiliateDisclosureHtml(), js/utils.js),
   on every affiliate widget, the 12Go card and the checklist shop menu.
   Muted like the accommodation sheet's copy of the same sentence. */
.affiliate-disclosure {
  font-size: var(--fs-xs);
  color: var(--text-muted);
  margin: 0 0 var(--space-sm);
}
.checklist-shop-options .affiliate-disclosure {
  width: 200px;
  margin: 0;
  padding: var(--space-xs) var(--space-md) var(--space-sm);
  border-top: 1px solid var(--df-hair);
}

/* Issue [critique pass] — was .settle-status-confirmed (--vn-teal, the
   brand's own Pressable Green under a legacy variable name — DESIGN.md:
   "Don't use green for anything that isn't pressable"). This span is a
   fact, not a control, so it gets a neutral ink badge instead; the shared
   .settle-status-confirmed class itself is untouched (js/expenses.js's
   "Settled" badge and accommodations.js's pre-booking "Confirmed" thread
   status are their own call, outside this pass's scope). */
.booking-badge-booked {
  background: var(--df-panel);
  color: var(--df-ink);
  border: 1px solid var(--df-hair);
}

/* The break block. Dashed rather than solid all the way round: the one
   container in this system that is deliberately not a cut edge, because
   what it marks is an absence. */
.bk-break {
  border: 2px dashed var(--vn-coral);
  border-radius: var(--df-radius-stub);
  background: var(--vn-coral-light);
  padding: var(--space-xl);
}
.bk-break-head {
  font-family: var(--df-display);
  font-weight: 800;
  font-size: var(--fs-xl);
  line-height: 1.15;
  letter-spacing: -0.02em;
  color: var(--vn-coral-text);
}
.bk-break-sub {
  font-size: var(--fs-md);
  color: var(--df-ink-soft);
  margin-top: var(--space-sm);
}
.bk-break-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-md);
  margin-top: var(--space-lg);
}
/* Departure Fever buttons: pill, 2px keyline, hard offset ink, and the
   press moves the control INTO its own shadow. Local to the route rather
   than another .btn-* variant — #panel-bookings already scopes its own
   button conversion (below) and this is the same tab's vocabulary. */
.bk-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: var(--tap-min);
  padding: 0 var(--space-4xl);
  border: 2px solid var(--df-ink);
  border-radius: var(--radius-pill);
  background: var(--df-panel);
  color: var(--df-ink);
  box-shadow: var(--df-press);
  font-family: inherit;
  font-size: var(--fs-md);
  font-weight: 600;
  cursor: pointer;
  transition: transform var(--dur-feedback) var(--ease-out), box-shadow var(--dur-feedback) var(--ease-out);
}
.bk-btn:hover { transform: translate(2px, 2px); box-shadow: var(--df-press-hover); }
.bk-btn:active { transform: translate(4px, 4px); box-shadow: var(--df-press-active); }
/* --df-brand-fg, never a literal white: --vn-teal is a bright mint after
   dark and white on it is 1.31:1 (issue #432). */
.bk-btn-primary { background: var(--df-brand); color: var(--df-brand-fg); }

/* Reduced motion: the buttons still press, they just don't animate. */
@media (prefers-reduced-motion: reduce) {
  .bk-btn { transition: none; }
}

/* ── Boarding-pass stub (issue [critique pass]) ───────────────────
   Applied only on the Bookings tab's own copy of a card (renderTransportBookingCard()/
   renderAccommodationBooked()/renderNewBookingCard()'s `opts.stub`) — the
   Itinerary tab's identical planning-surface card stays the flat list row
   it already was. A confirmed booking's job here is to PROVE itself; DESIGN.md
   hands this tab the boarding-pass-stub vocabulary (components.stub,
   --df-radius-stub, the torn-ticket dashed rule) for exactly that and it
   went unused until this pass. Each card becomes its own physical object —
   own keyline, own hard-offset shadow, own category-hued tab bleeding off
   the top edge (reusing --stop-cat, the same custom property
   renderStopCard() already publishes via its stop-cat-* class, itinerary.js) —
   instead of a hairline-separated row in a flat list. */
.itin-stop-card.booking-stub {
  border: 2px solid var(--df-ink);
  border-radius: var(--df-radius-stub);
  box-shadow: var(--df-press);
  margin-bottom: var(--space-2xl);
  position: relative;
}
/* border-bottom repeated explicitly, not left to cascade order against
   .itin-stop-card:last-child's border-bottom:none (equal specificity,
   0 classes apart) — a stub keeps its own full border on every side even
   as the trip's last booking, unlike a flat-list row's hairline. */
.itin-stop-card.booking-stub:last-child { margin-bottom: 0; border-bottom: 2px solid var(--df-ink); }
.booking-stub::before {
  content: '';
  position: absolute;
  top: -8px;
  left: var(--space-xl);
  width: 36px;
  height: 8px;
  background: var(--stop-cat, var(--df-ink-soft));
  border-radius: var(--df-radius-print) var(--df-radius-print) 0 0;
}
/* The torn-ticket internal rule (DESIGN.md Shapes) — same technique
   .wizard-recap-row already borrows it for (see that rule's own comment) —
   marks where a stub's tearable detail section begins, in place of the flat
   list's plain hairline. Lives on .stop-expanded-body itself (the grid
   container the collapse animation sizes, not .stop-expanded-inner, which
   clips to zero height inside it) — a real perforation is always there,
   folded or not, which is exactly right here: the line stays visible on a
   collapsed stub too, not just an expanded one. */
.booking-stub .stop-expanded-body {
  border-top: 2px dashed var(--df-hair);
}
/* .itin-stop-card.expanded:not(.trip-main):not(.trip-sub) (above, issue #449)
   sets its own box-shadow — an inset left rail — which would otherwise WIN
   over .booking-stub's hard-offset shadow the instant a stub expands (four
   class-level selectors there vs two here). A stub is never a Main Trip/Sub-Trip row
   (bookings.js/accommodations.js never emit those classes), so repeating
   the same :not() pair here is safe and makes this unambiguously the more
   specific rule regardless of source order — the card keeps its own
   press-offset shadow expanded or not; the category-hued tab is signal
   enough without also borrowing the itinerary rail. */
.itin-stop-card.expanded.booking-stub:not(.trip-main):not(.trip-sub) {
  background: var(--df-panel);
  box-shadow: var(--df-press);
}
/* A confirmation number is what someone reads off this card at a check-in
   counter or a gate — sized and set to actually be read at a glance, not
   filed alongside every other --fs-xs metadata line. */
.booking-stub-confno {
  font-family: var(--df-mono);
  font-weight: 700;
  font-size: var(--fs-lg);
  letter-spacing: 0.03em;
  color: var(--df-ink);
}

/* Issue #799/#822 decision 8 — one section per TripDestination leg above
   its own accommodation cards; .settings-section-title already gives the
   label the right small-caps treatment, this just adds breathing room
   between sections and above each section's card stack. */
.accommodation-leg-section {
  margin-bottom: var(--space-xl);
}
.accommodation-leg-section .settings-section-title {
  margin-bottom: var(--space-sm);
}

.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 #874 — the Bookings tab itself as a drop target, extending
   .booking-dropzone's own dragover vocabulary rather than duplicating it.
   The overlay stays hidden until an actual drag enters the panel
   (#panel-bookings.dragover, toggled by js/bookings.js's
   onBookingsTabDragOver/Leave) — a permanently visible dropzone would
   compete with the route the tab exists to show. */
#panel-bookings { position: relative; }
.bookings-tab-drop-overlay {
  display: none;
  position: absolute;
  inset: 0;
  z-index: 5;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-sm);
  padding: var(--space-lg);
  background: var(--vn-teal-light);
  border: 2px dashed var(--vn-teal);
  border-radius: var(--df-radius-card);
  color: var(--vn-teal);
  font-size: var(--fs-lg);
  font-weight: 600;
  text-align: center;
  pointer-events: none;
}
.bookings-tab-drop-overlay i { font-size: var(--fs-2xl); }
#panel-bookings.dragover .bookings-tab-drop-overlay { display: flex; }

/* 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. An indigo-tinted box. */
.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); }

/* Issue [audit 2026-08-24] — the Bookings tab's cards (.itin-stop-card,
   and now the .bk-* route) 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);
}

/* Issues #873 / #941 — the panel stays full-bleed (its left edge must match
   every other tab's); only the card content inside an opened row gets a
   measure, sized for a card instead of a paragraph. */
.bk-detail-body .itin-stop-card.booking-stub {
  max-width: 640px;
}

/* Audit 2026-09-02 — an expanded booking card's "View link" / "View
   document" / "View receipt" rows are ~78x15 anchors stacked 4px apart:
   under the 44px floor, and their 24px hit circles overlap each other
   (WCAG 2.5.8). Scoped to the Bookings tab's expanded card body only — the
   Itinerary tab's identical planning-surface card stays the flat, dense list
   row it's meant to be. Growing the anchor's own box (not a ::before) is
   what keeps the stacked rows from colliding. */
#panel-bookings .stop-expanded-inner .stop-addr a {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-min);
}

/* 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). */
/* .booking-menu-btn (js/bookings.js, issue [critique pass]) joins the same
   combined group — a THIRD "⋯" trigger, same reasoning as .member-menu-btn's
   own comment: nothing card-specific about the styling, own class name so
   closeBookingCardMenu() can never cross-wire with the other two. Replaces
   the summary row's old adjacent Edit+Delete buttons, whose --tap-min hit
   zones (each individually correct) overlapped once packed next to each
   other and the expand chevron. */
.stop-menu-btn,
.member-menu-btn,
.booking-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,
.booking-menu-btn:hover { background: var(--df-hair); }
.stop-menu-btn::before,
.member-menu-btn::before,
.booking-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,
.booking-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,
.booking-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,
.booking-menu-item:hover { background: var(--vn-teal-light); }
.stop-menu-item-danger,
.member-menu-item-danger,
.booking-menu-item-danger { color: var(--vn-coral); }
.stop-menu-item-danger:hover,
.member-menu-item-danger:hover,
.booking-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);
}

/* Issue #1015 Phase 3 — "3 photos left" beside the Photo button. Muted, not
   an alert: it is information until the cap is hit, and the 402 modal owns
   the blocking moment. Empty until the free cap is close, so it takes no space. */
/* #1030 critique — up from --fs-xs (11px): these hints carry a sentence and a
   decision, not a timestamp, so they take the helper-text tier. */
.memory-quota-hint {
  font-size: var(--fs-sm);
  color: var(--text-muted);
}

/* Issue #1030 — the same muted hint for the AI caps: directly above Generate in
   the agent modal (.ai-quota-hint, css/features/itinerary-agent.css places it)
   and directly above Take Photo/Upload in the expense form, so each hint reads
   before the action it describes. Unlike the photo hint these sit on their own
   line, so when empty they take no space at all. */
.quota-hint {
  font-size: var(--fs-sm);
  color: var(--text-muted);
}

.quota-hint:empty {
  display: none;
}

.receipt-quota-hint {
  margin-bottom: var(--space-sm);
  text-align: center;
}

/* The hint stays in the flex row even when empty (flex:1 inline) — that is what
   keeps the Photo button right-aligned; hiding it would pull the button left. */
.memory-quota-buy {
  background: none;
  border: none;
  padding: 0 0 0 var(--space-xs);
  font: inherit;
  color: var(--vn-teal);
  text-decoration: underline;
  cursor: pointer;
  min-height: var(--tap-min);
}

.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;
}

/* Issue #1361 — the shared Report flag (js/report.js). A small glyph whose
   tappable area is expanded to --tap-min with ::before, like .stop-menu-btn. */
.report-btn {
  background: none;
  border: none;
  cursor: pointer;
  position: relative;
  padding: var(--space-2xs);
  color: var(--df-ink-soft, var(--text-muted));
  font-size: var(--fs-md);
  line-height: 1;
}
.report-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%);
}
.report-btn:hover, .report-btn:focus-visible { color: var(--error); }
.report-btn-inline { margin-left: var(--space-xs); vertical-align: middle; font-size: var(--fs-sm); }
.memory-photo-report {
  position: absolute;
  bottom: 2px;
  right: 2px;
  z-index: 2;
  background: var(--df-panel);
  border-radius: 50%;
  font-size: var(--fs-xs);
}

/* ── 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); }
/* #1369 — keeps the report flag beside the delete ×, not centered between
   name and × by the row's space-between. */
.comment-meta .report-btn { margin-left: auto; }

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

.comment-add-row textarea {
  /* No own font-size (issue #446) — see .checklist-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) {
  /* #1884 — boot mark renders fully drawn and static (the inline block in
     index.html carries its own copy so it holds before this file loads). */
  #boot-screen { animation: none; opacity: 1; }
  #boot-screen .rm-trace { animation: none; stroke-dashoffset: 0; }
  #boot-screen .rm-dot { animation: none; opacity: 1; transform: none; }
  .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,
  .ideas-sidebar-generate-btn.loading .ti-sparkles,
  /* Ideas panel — the body is shown, the badge sits at full size, and a
     just-kept card rests in place with its tape strip on. Only the travel goes. */
  .ideas-sidebar-body-reveal,
  .ideas-sidebar-count-pop,
  .itin-stop-card.idea-card.idea-card-landed,
  .itin-stop-card.idea-card.idea-card-landed::before,
  /* The Recap scroll replay (css/recap.css) is already guarded behind
     no-preference, so this is belt and braces: the route stays drawn and
     every field and bar stays at its real value. */
  .recap-stamp.is-struck,
  .recap-route-stop::before,
  .recap-route-name,
  .recap-route-name::before,
  .recap-num-n,
  .recap-cats .cat-bar-fill {
    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,
  .treat-stamp.is-striking,
  .exp-disclosure-body,
  .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.) */
  .checklist-progress-fill,
  .wizard-progress-segment-fill,
  /* Issue #993 — the map stop-list overlay's own collapse/expand
     (css/features/itinerary-map.css). Removing the transition still ends at
     the correct max-height for whichever state .collapsed is in — never
     stuck mid-collapse — same as every other disclosure in this list. */
  .map-stop-list-body,
  .map-stop-list-toggle i,
  /* Issue #1140 — the consent switch knob. Without the transition it still
     lands on the correct side for its state. */
  .consent-switch::after {
    transition: none;
  }

  /* #2315 — the treat stamp lands already struck (its finished state), and
     the disclosure's grid-template-rows still ends at 0fr/1fr, just unslid. */
  .treat-stamp.is-striking {
    animation: 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, .scan-btn,
     .settle-action-btn, .x-close-btn) — same rule applies. The expense rows
     left this list when they stopped being cards: an entry inside .exp-sheet
     has no offset shadow and does not travel on hover, so there is no
     motion here left to answer. */
  .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,
  .settle-action-btn,
  .x-close-btn,
  /* Issue #911 follow-up — the itinerary toolbar's own Departure Fever
     press (Create Itinerary/Proposals/Decide/member-count), same mechanic. */
  .itin-toolbar .itin-action-btn,
  .itin-toolbar .itin-member-count-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,
  .settle-action-btn:hover,
  .x-close-btn:hover,
  .itin-toolbar .itin-action-btn:hover,
  .itin-toolbar .itin-member-count-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,
  .itin-toolbar .itin-action-btn:active,
  .itin-toolbar .itin-member-count-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);
  }
}

/* Issue #1561 — home Trip requests section + invite nudge */
.home-invite-nudge {
  display: block; width: 100%; text-align: left; min-height: var(--tap-min);
  padding: var(--space-sm) var(--space-md); margin-bottom: var(--space-md);
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: var(--radius-md); color: var(--text); font: inherit;
  font-size: var(--fs-sm); cursor: pointer;
}
.home-request-group-label { font-size: var(--fs-xs); color: var(--text-muted); margin: var(--space-sm) 0; }
.home-request-card {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
  gap: var(--space-md); padding: var(--space-lg); margin-bottom: var(--space-md);
  background: var(--surface-2); border-radius: var(--radius-md);
}
.home-request-text { min-width: 0; flex: 1 1 12rem; }
.home-request-name { font-size: var(--fs-base); font-weight: 600; }
.home-request-sub { font-size: var(--fs-sm); color: var(--text-muted); }
.home-request-actions { display: flex; flex-wrap: wrap; gap: var(--space-sm); }
.home-request-actions .btn-small { min-height: var(--tap-min); }

/* ── Where you want to go (#1565) ──
   Six photo cards. Same scrim recipe as .home-trip-card-photo (0.64 → 0.90
   keeps white text at AA over any image), so the label is legible whether or
   not the photo ever loads: until then the card is plain Cut Stock with ink
   text, and the text flips to lamplight only once the photo is visible. */
.home-city-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-xl);
}
@media (min-width: 680px) {
  .home-city-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
.home-city-card {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  gap: 2px;
  min-height: max(var(--tap-min), 112px);
  padding: var(--space-xl);
  text-align: left;
  font: inherit;
  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);
  overflow: hidden;
  cursor: pointer;
  transition: transform var(--dur-feedback) var(--df-ease),
              box-shadow var(--dur-feedback) var(--df-ease);
}
.home-city-card:hover { transform: translate(2px, 2px); box-shadow: var(--df-press-hover); }
.home-city-name { position: relative; z-index: 1; font-weight: 800; line-height: 1.15; }
.home-city-country {
  position: relative;
  z-index: 1;
  font-family: var(--df-mono);
  font-size: var(--fs-xs);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--df-ink-soft);
}
.home-city-card-photo::before {
  content: '';
  position: absolute;
  inset: 0;
  background-image:
    linear-gradient(180deg, rgba(16, 26, 20, 0.64), rgba(16, 26, 20, 0.90)),
    var(--home-city-photo);
  background-size: auto, cover;
  background-position: center;
  opacity: 0;
  transition: opacity var(--dur-view) var(--ease-out);
}
.home-city-card-photo-visible::before { opacity: 1; }
.home-city-card-photo-visible .home-city-name,
.home-city-card-photo-visible .home-city-country { color: #ffeeda; }

.home-city-view-photo {
  position: relative;
  height: 160px;
  margin-bottom: var(--space-xl);
  border-radius: var(--df-radius-stub);
  background: var(--df-panel);
  overflow: hidden;
}
.home-city-view-photo.home-city-card-photo::before {
  background-image: var(--home-city-photo);
  background-size: cover;
}
.home-city-view-title { margin: 0; }
.home-city-view-country {
  margin: 0 0 var(--space-xl);
  font-family: var(--df-mono);
  font-size: var(--fs-xs);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--df-ink-soft);
}
.home-city-view-empty { margin: 0 0 var(--space-xl); }
.home-city-view-plan { width: 100%; min-height: var(--tap-min); }

/* Placed after the main block above on purpose: same specificity, so it has to
   come later to win. The press travels; the photo fade is state feedback and
   stays. */
@media (prefers-reduced-motion: reduce) {
  .home-city-card { transition: none; }
  .home-city-card:hover { transform: none; box-shadow: var(--df-press-hover); }
  /* Issue #952 Phase 5 — the autofill button keeps its shadow change as
     motionless feedback; only the 1–2px press travel goes. */
  .day-box-autofill-btn:hover,
  .day-box-autofill-btn:active { transform: none; }
}

/* #1666/#1919 — the stamp strike resolves to its finished (struck) state. */
@media (prefers-reduced-motion: reduce) {
  .stamp-card-struck.just-stamped,
  /* #2668 — the detail strike is static (no animation); it already sits finished. */
  .stamps-complete.just-stamped { animation: none; }
  .stamps-trip-strip-btn:hover { transform: none; }
}

/* #2021 — per-viewer food and rhythm notes on a stop. Informational only: no
   positive badge exists. The avatar button gets its tap area from ::before
   (--tap-min) while the glyph stays small. */
.stop-notes { display: flex; flex-wrap: wrap; gap: var(--space-xs); margin-top: var(--space-2xs); }
.stop-note-chip {
  display: inline-flex; align-items: center; gap: var(--space-2xs);
  padding: 2px var(--space-xs); border-radius: 999px;
  font-size: var(--fs-xs); font-weight: 600;
  background: var(--surface-2, rgba(0, 0, 0, 0.05)); color: var(--text-muted, inherit);
}
.stop-note-not_halal, .stop-note-not_vegetarian { color: var(--vn-coral); }
.member-avatar-wrap { position: relative; display: inline-flex; }
.member-avatar {
  position: relative; width: 20px; height: 20px; padding: 0; border: 0; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center; overflow: hidden;
  font-size: 10px; font-weight: 700; background: var(--vn-indigo); color: #fff; cursor: default;
}
.member-avatar img { width: 100%; height: 100%; object-fit: cover; }
.member-avatar::before { content: ''; position: absolute; top: 50%; left: 50%; width: max(44px, 100%); height: max(44px, 100%); transform: translate(-50%, -50%); }
.member-needs-card {
  display: none; position: absolute; z-index: 20; top: 100%; left: 0; margin-top: var(--space-2xs);
  min-width: 140px; padding: var(--space-xs) var(--space-sm); border-radius: var(--radius-md, 8px);
  background: var(--surface, #fff); color: var(--text, inherit); border: 1px solid var(--border, rgba(0, 0, 0, 0.12));
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); font-weight: 400; white-space: nowrap; flex-direction: column;
}
.member-avatar-wrap:hover .member-needs-card,
.member-avatar-wrap:focus-within .member-needs-card { display: flex; }
.member-needs-name { font-weight: 600; }
.member-needs-list { color: var(--text-muted, inherit); }
.itin-food-footer { margin: var(--space-md) 0 0; font-size: var(--fs-sm); color: var(--text-muted, inherit); }

/* Issue #2086 — the join QR. Dark on light in every theme (System/Light/Dark):
   the white ground is part of the code, not a themed surface, and a dark-mode
   inversion would not scan. */
.invite-qr-box {
  display: flex;
  justify-content: center;
  margin: var(--space-md) 0;
}
.invite-qr-box:empty {
  display: none;
}
.invite-qr-svg {
  width: 160px;
  height: 160px;
  background: #fff;
  border-radius: var(--radius-sm);
}
.invite-qr-box-large .invite-qr-svg {
  width: min(320px, 100%);
  height: auto;
  aspect-ratio: 1;
}

/* ==================== Expenses zero state (#2240) ==================== */
.exp-empty-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-md);
  text-align: center;
  padding: var(--space-3xl) var(--space-xl);
  border: 2px solid var(--df-ink);
  border-radius: var(--radius-lg, 12px);
}
.exp-empty-card h3 { margin: 0; }
.exp-empty-card p { margin: 0; color: var(--text-muted, inherit); }
.exp-empty-icon { font-size: var(--fs-2xl); color: var(--vn-teal); }
.exp-empty-actions { display: flex; gap: var(--space-md); flex-wrap: wrap; justify-content: center; }
.exp-empty-actions .btn-primary,
.exp-empty-actions .btn-secondary { min-height: var(--tap-min); }
/* The card stands alone: everything else in the panel steps aside, whichever
   address or view is active. */
.exp-panel[data-exp-empty="true"] .exp-zone,
.exp-panel[data-exp-empty="true"] .exp-add-pill { display: none !important; }


/* #2241 — Balances groups and the Settle up / Mark received sheet. */
.settle-group + .settle-others,
.settle-group + .settle-start-fund { margin-top: var(--space-xl); }

/* #2424 — Balances as one card from the Expenses board kit. */
.xk-bal-switch { display: flex; justify-content: flex-end; margin-bottom: var(--space-md); }
.xk-bal-switch[hidden] { display: none; }
.settle-mine .settle-group-title { margin: 0 0 var(--space-sm); }
.settle-mine .settle-row {
  min-height: 44px;
  padding: var(--space-sm) 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
}
.settle-mine .settle-row + .settle-row { border-top: 1.5px dashed var(--df-hair); }
.settle-mine .settle-row-line { align-items: center; flex-wrap: nowrap; }
.settle-mine .settle-sentence { line-height: 1.3; }
.settle-mine .settle-amount { font-family: var(--df-sans); font-size: 15px; font-weight: 700; letter-spacing: 0; color: var(--df-ink); }
.settle-row-meta.xk-row-meta { font-size: 10px; }
.settle-row-meta-end.xk-row-meta .settle-status { color: var(--df-brand); font-weight: 700; }
.settle-mine .settle-group-cta { width: 100%; margin-top: var(--space-lg); }
.settle-mine .settle-group + .settle-group { border-top: 2px solid var(--df-ink); }
.settle-others > summary.xk-disclosure { list-style: none; margin: 0; }
.settle-others > summary.xk-disclosure::-webkit-details-marker { display: none; }
.settle-others[open] > summary .settle-others-chev { transform: rotate(180deg); }
/* Per person: a disclosure row with no shadow. */
.exp-disclosures { box-shadow: none; }
.exp-disclosure-head.xk-disclosure { border: 0; border-radius: 0; min-height: 48px; padding: 0 var(--space-2xl); }
.exp-disclosure-head.xk-disclosure .exp-disclosure-title { font-family: inherit; font-size: inherit; letter-spacing: 0; }
.settle-mine .settle-group { padding: var(--space-xl) var(--space-2xl); }
.settle-mine .settle-group + .settle-group { border-top: 2px solid var(--df-ink); }
.settle-mine .settle-row { padding: var(--space-md) 0; }
.settle-mine .settle-group-cta { width: 100%; justify-content: center; }
.settle-row-who { min-width: 0; }
.settle-row-amt { text-align: right; }
.settle-row-meta { margin-top: var(--space-2xs); font-size: var(--fs-xs); color: var(--df-ink-soft); }
.settle-row-meta .settle-status { background: none; padding: 0; color: inherit; border: 0; }
.settle-row-meta-end .settle-status { color: var(--vn-teal); font-weight: 600; }
.settle-group-title {
  margin: 0 0 var(--space-md);
  font-size: var(--fs-sm);
  font-weight: 700;
  color: var(--df-ink-soft);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.settle-group-cta { margin-top: var(--space-lg); }
.settle-others { margin-top: var(--space-xl); }
.settle-others > summary {
  cursor: pointer;
  min-height: var(--tap-min);
  display: flex;
  align-items: center;
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--df-ink-soft);
}
.settle-others .settle-sheet { margin-top: var(--space-md); }
.settle-start-fund { margin-top: var(--space-xl); }

/* #2425 — Settle up sheet, from the approved board (#2231): the xk- sheet
   chrome, one bordered payee list with dashed dividers, only the chosen row's
   header ink, its expansion on cream. The native radio stays in the DOM
   (visually hidden) for keyboard and screen-reader use. */
.settle-scrim { align-items: flex-end; padding: 0; }
.settle-sheet-chrome { padding: 0; gap: 0; box-shadow: none; overflow: hidden; }
@media (max-width: 680px) {
  .settle-sheet-chrome { height: calc(100vh - 24px); height: calc(100dvh - 24px); max-height: none; }
}
@media (min-width: 681px) {
  .settle-scrim { align-items: center; padding: 1rem; }
  .settle-sheet-chrome { border: 2px solid var(--df-ink); border-radius: 18px; max-height: 90dvh; }
}
.settle-sheet-body { flex: 1; min-height: 0; display: flex; flex-direction: column; }
.settle-sheet-scroll { flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain; padding: 0 16px 14px; display: flex; flex-direction: column; gap: 14px; }
.settle-sheet-foot { padding: 12px 16px 18px; border-top: 2px solid var(--df-ink); background: var(--df-panel); }

.settle-payees { border: 2px solid var(--df-ink); border-radius: var(--radius-sm); overflow: hidden; background: var(--df-panel); }
.settle-payee-block { border-bottom: 1.5px dashed var(--df-hair); }
.settle-payee-block:last-child { border-bottom: 0; }
.settle-payee {
  position: relative;
  display: flex;
  align-items: center;
  gap: 12px;
  min-height: 52px;
  padding: 0 14px;
  background: var(--df-panel);
  color: var(--df-ink);
  font-size: 16px;
  cursor: pointer;
}
.settle-payee.selected { background: var(--df-ink); color: var(--df-panel); }
.settle-payee input[type="radio"] {
  position: absolute;
  opacity: 0;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  cursor: pointer;
}
.settle-dot {
  flex: none;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  border: 2px solid currentColor;
  background: transparent;
  box-sizing: border-box;
}
.settle-payee.selected .settle-dot { background: var(--vn-teal); border-color: var(--df-panel); }
.settle-payee input[type="radio"]:focus-visible + .settle-dot { outline: 2px solid var(--vn-teal); outline-offset: 2px; }
.settle-payee-name { flex: 1; min-width: 0; overflow-wrap: anywhere; }
.settle-payee-amt { font-weight: 700; }
.settle-payee-detail {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 12px 14px 14px;
  background: var(--df-panel);
  color: var(--df-ink);
  font-size: 14px;
}
.settle-method { display: flex; gap: 12px; align-items: center; }
.settle-method + .settle-method { margin-top: 4px; }
.settle-method-text { display: flex; flex-direction: column; align-items: flex-start; gap: 4px; min-width: 0; }
.settle-method-id { overflow-wrap: anywhere; }
.settle-method-qr { flex: none; width: 96px; height: 96px; object-fit: contain; border: 2px solid var(--df-ink); border-radius: 3px; background: var(--df-panel); }
.settle-copy-btn { min-height: 36px; padding: 0 12px; font-size: 13px; font-weight: 600; background: var(--df-ground); }
.settle-tiles { display: flex; gap: 8px; }
.settle-tile {
  flex: 1;
  min-width: 0;
  min-height: 52px;
  padding: 0 8px;
  font-size: 14px;
  gap: 6px;
}
.settle-tile--cash {
  display: flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  border: 2px solid var(--df-ink);
  border-radius: var(--radius-sm);
  background: var(--df-panel);
  color: var(--df-ink);
  font-family: var(--df-sans);
  font-weight: 700;
  cursor: pointer;
}
.settle-tile:first-child { flex: 1.4; }
.settle-tile.is-on { background: var(--df-ink); color: var(--df-panel); }
.settle-tile-thumb { flex: none; width: 32px; height: 32px; object-fit: cover; border-radius: 4px; }
.settle-evidence { display: flex; align-items: center; gap: 10px; margin: 0; color: var(--df-ink); text-decoration: none; font-weight: 600; }
.settle-evidence--cash { gap: 6px; }
.settle-evidence-thumb { width: 48px; height: 48px; }
.settle-tile-change { text-decoration: underline; text-underline-offset: 3px; font-weight: 600; }
.settle-fx-link {
  align-self: flex-start;
  min-height: var(--tap-min);
  padding: 0;
  border: 0;
  background: none;
  color: var(--df-ink);
  font: inherit;
  font-size: 14px;
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
}
.settle-sheet-fx { display: flex; gap: var(--space-sm); flex-wrap: wrap; align-items: center; }
.settle-sheet-fx input { width: 90px; }
.settle-sheet-note { font-size: 14px; margin: 0; }
.settle-sheet-submit { width: 100%; min-height: 52px; font-size: 16px; }
.settle-sheet-submit:disabled { background: var(--df-hair); color: var(--df-ink-soft); box-shadow: none; cursor: not-allowed; }
.settle-sheet-hint { margin: 6px 0 0; font-size: 13px; color: var(--df-ink-soft); text-align: center; }

/* #2296 — Fund board: Contributions head, pencil edit, one Close and refund. */
.fund-meta-text { flex: 1; min-width: 0; }
.fund-section-head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-md); }
.fund-section-title {
  margin: 0;
  font-family: var(--df-mono);
  font-size: var(--fs-xs);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--df-ink-soft);
}
.fund-row-name { min-width: 0; overflow-wrap: anywhere; color: var(--df-ink); }
.fund-row-amount { font-variant-numeric: tabular-nums; color: var(--df-ink); font-weight: 600; }
.fund-edit-btn {
  position: relative;
  background: none;
  border: none;
  padding: var(--space-xs);
  color: var(--df-ink-soft);
  cursor: pointer;
}
.fund-edit-btn::before { content: ""; position: absolute; inset: 50% auto auto 50%; width: max(var(--tap-min), 100%); height: max(var(--tap-min), 100%); transform: translate(-50%, -50%); }
.fund-close-btn { align-self: flex-start; }

/* #2308 (#2192 step 5) — "You're in" arrival moment. The trip name lands as a
   Stamp Word (DESIGN.md: coral ink, double keyline, Trail-Green ghost second
   pass) over the trip that is already opening. Inline and non-blocking. */
.arrival-moment {
  position: fixed;
  inset: 0;
  z-index: 1500;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-lg);
  pointer-events: none;
}
.arrival-stamp {
  max-width: 100%;
  overflow-wrap: anywhere;
  text-align: center;
  font-family: var(--df-mono);
  font-size: var(--fs-2xl, 1.5rem);
  font-weight: 800;
  line-height: 1.15;
  letter-spacing: 0.04em;
  padding: 0.04em 0.2em 0.09em;
  color: var(--vn-coral);
  /* Over live trip content, so the ground is opaque; the die stays the stamp. */
  background: var(--surface-2, var(--bg));
  border: 0.055em solid currentColor;
  border-radius: 0.14em;
  box-shadow: inset 0 0 0 0.022em currentColor;
  text-shadow: 0.028em 0.036em 0 var(--vn-green, currentColor);
  transform: rotate(-2.6deg);
  animation: arrival-strike var(--dur-state) var(--ease-out) both;
}
@keyframes arrival-strike {
  from { transform: rotate(-11deg) scale(1.85); opacity: 0; }
  to   { transform: rotate(-2.6deg) scale(1); opacity: 1; }
}
/* Finished state, never hidden: the stamp shows already landed. */
@media (prefers-reduced-motion: reduce) {
  .arrival-stamp { animation: none; }
}

/* #2309 (#2192 step 4) — "What brings you here?" intent step, laid over My Trips
   right after sign-up. Three large choices plus Skip, all >= --tap-min. */
.intent-step {
  position: fixed;
  inset: 0;
  z-index: 1400;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-lg);
  background: var(--bg);
  overflow-y: auto;
}
.intent-step-card {
  width: 100%;
  max-width: 420px;
  display: flex;
  flex-direction: column;
  gap: var(--space-md, 12px);
  animation: intent-rise var(--dur-view) var(--ease-out) both;
}
.intent-step-title { margin: 0 0 var(--space-sm); font-size: var(--fs-2xl, 1.5rem); outline: none; }
.intent-choice {
  display: flex;
  align-items: center;
  gap: var(--space-md, 12px);
  width: 100%;
  min-height: var(--tap-min);
  padding: var(--space-md, 12px) var(--space-lg);
  text-align: left;
  color: inherit;
  background: var(--surface-2, var(--bg));
  border: 1px solid var(--border, currentColor);
  border-radius: 12px;
  cursor: pointer;
  transition: border-color var(--dur-feedback) var(--ease-out);
}
.intent-choice:hover, .intent-choice:focus-visible { border-color: var(--vn-coral); }
.intent-choice .ti { font-size: 1.5rem; flex: none; color: var(--vn-coral); }
.intent-choice-text { display: flex; flex-direction: column; gap: 2px; }
.intent-skip {
  min-height: var(--tap-min);
  align-self: center;
  background: none;
  border: 0;
  color: inherit;
  text-decoration: underline;
  cursor: pointer;
}
@keyframes intent-rise {
  from { transform: translateY(12px); opacity: 0; }
  to   { transform: none; opacity: 1; }
}
/* Finished state, never hidden. */
@media (prefers-reduced-motion: reduce) {
  .intent-step-card { animation: none; }
}

/* Issue #2310 — sign-in motion resolves to its finished state, never hidden. */
@media (prefers-reduced-motion: reduce) {
  .lp-nav-brand img.route-mark-draw,
  .lp-nav-brand img.route-mark-land,
  .auth-form-swap,
  .field-error-in { animation: none; }
}

/* ==========================================================================
   Expenses board kit (#2316, part of #2231)
   The approved boards' visual language as shared classes, `xk-` prefixed so
   it never collides with the legacy Expenses classes it replaces. Tokens
   only (dark mode flips through --df-*); no screen uses these yet, the
   per-screen issues apply them. Static motionless states only: nothing here
   travels, so the prefers-reduced-motion block has nothing to answer.
   ========================================================================== */

/* Eyebrow */
.xk-eyebrow {
  font-family: var(--df-mono);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--df-ink);
}
.xk-eyebrow--green { color: var(--df-brand); }
.xk-eyebrow--coral { color: var(--vn-coral-text); }
.xk-eyebrow--ink { color: var(--df-ink); }

/* Display figures */
.xk-figure {
  font-family: var(--df-display);
  font-weight: 800;
  letter-spacing: -0.03em;
  line-height: 1;
  color: var(--df-ink);
}
.xk-figure--total { font-size: 40px; }
.xk-figure--fund { font-size: 32px; }
.xk-figure--sheet { font-size: 26px; text-transform: uppercase; }
.xk-figure--compact { font-size: 18px; }

/* Day band */
.xk-day-band {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 8px 14px;
  background: var(--df-ink);
  color: var(--df-panel);
  font-family: var(--df-mono);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

/* Ledger row */
.xk-row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 11px 14px 11px 12px;
  background: var(--df-panel);
  border-bottom: 1.5px dashed var(--df-hair);
  color: var(--df-ink);
  text-decoration: none;
}
.xk-row-spine {
  flex: none;
  width: 5px;
  align-self: stretch;
  border-radius: 3px;
  background: var(--xk-spine, var(--df-ink-soft));
}
.xk-row-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.xk-row-title { font-family: var(--df-sans); font-size: 15px; font-weight: 700; }
.xk-row-meta {
  font-family: var(--df-mono);
  font-size: 10px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--df-ink-soft);
}
.xk-row-amount { font-family: var(--df-sans); font-size: 15px; font-weight: 700; }

/* Segmented control (Entries/Balances/Fund, Split between, Not-equal modes) */
.xk-seg {
  display: flex;
  padding: 3px;
  background: var(--df-panel);
  border: 2px solid var(--df-ink);
  border-radius: var(--radius-pill);
}
.xk-seg-btn {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: var(--tap-min);
  padding: 0 12px;
  border: 0;
  border-radius: var(--radius-pill);
  background: none;
  color: var(--df-ink);
  font-family: var(--df-sans);
  font-size: 15px;
  font-weight: 700;
  white-space: nowrap;
  text-decoration: none;
  cursor: pointer;
}
.xk-seg-btn[aria-checked="true"],
.xk-seg-btn[aria-selected="true"],
.xk-seg-btn[aria-pressed="true"],
.xk-seg-btn.is-on {
  background: var(--df-ink);
  color: var(--df-panel);
}

/* Field */
.xk-field-label {
  display: block;
  margin-bottom: 6px;
  font-family: var(--df-sans);
  font-size: 14px;
  font-weight: 700;
  text-transform: none;
  letter-spacing: 0;
  color: var(--df-ink);
}
.xk-field {
  box-sizing: border-box;
  width: 100%;
  height: 48px;
  padding: 0 14px;
  border: 2px solid var(--df-ink);
  border-radius: var(--radius-sm);
  background: var(--df-panel);
  color: var(--df-ink);
  font-family: var(--df-sans);
}

/* Icon tile (category picker) */
.xk-tile {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  width: 52px;
  height: 48px;
  border: 2px solid var(--df-ink);
  border-radius: var(--radius-pill);
  background: var(--df-panel);
  color: var(--xk-tile-ink, var(--df-ink));
  cursor: pointer;
}
.xk-tile[aria-checked="true"],
.xk-tile.is-on {
  background: var(--xk-tile-fill, var(--df-amber));
  color: var(--df-amber-fg);
}

/* Dashed tile (Scan receipt, Upload payment receipt) */
.xk-dashed {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  box-sizing: border-box;
  width: 100%;
  min-height: 48px;
  border: 2px dashed var(--df-ink);
  border-radius: var(--radius-sm);
  background: var(--df-panel);
  color: var(--df-ink);
  font-family: var(--df-sans);
  font-size: 15px;
  font-weight: 700;
  cursor: pointer;
}

/* Pill buttons */
.xk-pill {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  box-sizing: border-box;
  min-height: 48px;
  padding: 0 22px;
  border: 2px solid var(--df-ink);
  border-radius: var(--radius-pill);
  background: var(--df-panel);
  color: var(--df-ink);
  font-family: var(--df-sans);
  font-size: 16px;
  font-weight: 700;
  cursor: pointer;
}
.xk-pill--primary {
  background: var(--df-brand);
  color: var(--df-brand-fg);
  box-shadow: 3px 3px 0 var(--df-ink);
}
.xk-pill--primary:active { box-shadow: var(--df-press-active); }
.xk-pill--coral {
  border-color: var(--vn-coral-text);
  color: var(--vn-coral-text);
}

/* Disclosure row */
.xk-disclosure {
  display: flex;
  align-items: center;
  justify-content: space-between;
  box-sizing: border-box;
  width: 100%;
  min-height: 48px;
  padding: 0 14px;
  border: 2px solid var(--df-ink);
  border-radius: var(--radius-sm);
  background: var(--df-panel);
  color: var(--df-ink);
  font-family: var(--df-sans);
  font-size: 15px;
  font-weight: 700;
  text-align: left;
  cursor: pointer;
}

/* TREAT stamp (a known exception to DESIGN.md's One Shout rule) */
.xk-treat {
  display: inline-block;
  padding: 0 8px;
  border: 3px solid var(--vn-coral-text);
  border-radius: 0.14em;
  color: var(--vn-coral-text);
  font-family: var(--df-display);
  font-size: 24px;
  font-weight: 800;
  line-height: 1.1;
  letter-spacing: -0.03em;
  text-transform: uppercase;
  transform: rotate(-8deg);
}

/* Sheet chrome */
.xk-scrim { background: var(--df-scrim-soft); }
.xk-sheet {
  display: flex;
  flex-direction: column;
  background: var(--df-ground);
  border-top: 2px solid var(--df-ink);
  border-radius: 18px 18px 0 0;
  color: var(--df-ink);
}
.xk-sheet-handle {
  align-self: center;
  width: 40px;
  height: 4px;
  margin: 8px 0 0;
  border-radius: var(--radius-pill);
  background: var(--df-ink);
}
.xk-sheet-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 14px 16px 10px;
}
.xk-sheet-title {
  margin: 0;
  font-family: var(--df-display);
  font-size: 26px;
  font-weight: 800;
  letter-spacing: -0.03em;
  text-transform: uppercase;
}
.xk-sheet-close {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border: 2px solid var(--df-ink);
  border-radius: var(--radius-pill);
  background: var(--df-panel);
  color: var(--df-ink);
  cursor: pointer;
}

/* Progress bar (fund left vs in) */
.xk-progress {
  height: 10px;
  overflow: hidden;
  border: 2px solid var(--df-ink);
  border-radius: var(--radius-pill);
  background: var(--df-ground);
}
.xk-progress-fill {
  height: 100%;
  width: var(--xk-progress, 0%);
  background: var(--df-brand);
}

/* ==========================================================================
   Expenses 21 (#2317): Entries, summary card, empty state and desktop, from
   the approved boards (#2231). Applies the xk- kit; tokens only. These sit
   last so they win over the earlier .exp-* rules they replace.
   ========================================================================== */

/* Summary card: 40px green-eyebrow total, 4px shadow, no rule between columns */
.exp-stub { box-shadow: 4px 4px 0 var(--df-ink); }
.exp-stub-head { display: flex; flex-direction: column; gap: 2px; padding: 12px 16px; }
.exp-stub-head .stat-value { font-family: var(--df-display); font-size: 40px; font-weight: 800; letter-spacing: -0.03em; line-height: 1; color: var(--df-ink); }
.exp-stub-field + .exp-stub-field { border-left: 0; }
.exp-stub-fields { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 4px 16px; padding: 10px 16px 12px; }
.exp-stub-field { padding: 0; }
.exp-stub-field .stat-label { font-family: var(--df-mono); font-size: 10px; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; color: var(--vn-coral-text); }
.exp-stub-field + .exp-stub-field .stat-label { color: var(--df-brand); }
.exp-stub-line { font-size: 14px; }
.exp-stub-amt, .exp-stub-amt.settle-amount-owe, .exp-stub-amt.settle-amount-owed { font-weight: 700; color: var(--df-ink); }
.exp-stub-square { padding: 10px 16px 12px; }
.exp-stub-desk { display: none; }
.exp-stub-compact { display: none; }
@media (max-width: 1023px) {
  .exp-panel[data-exp-view="balances"] .exp-stub-compact,
  .exp-panel[data-exp-view="fund"] .exp-stub-compact {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 10px 16px;
    font-weight: 400;
  }
}

/* Phone: the "All entries · N entries" heading gives way to the chips */
.exp-ledger-head { display: none; }
.exp-ledger-head .exp-count { display: none; }

/* Chips first, then a borderless fund line */
.exp-fund-line {
  display: flex;
  align-items: center;
  justify-content: space-between;
  min-height: 36px;
  margin-bottom: var(--space-lg);
  padding: 0;
  border: 0;
  border-radius: 0;
  font-size: 14px;
}
.exp-fund-line:hover { border-color: transparent; }
.exp-fund-line-text { display: inline-flex; align-items: center; gap: 8px; }
.exp-fund-line-text i { color: var(--df-brand); font-size: 18px; }
.exp-fund-line b { font-weight: 700; }

/* Day band, ledger rows */
.exp-sheet { border-radius: 12px; box-shadow: none; }
.exp-day-rule { justify-content: space-between; align-items: center; flex-wrap: nowrap; gap: var(--space-lg); padding: 8px 14px; border-bottom: 0; }
.exp-day-rule-label, .exp-day-rule-total { font-size: 10px; font-weight: 700; letter-spacing: 0.1em; color: var(--df-panel); }
.exp-day-rule-total { margin-left: 0; }
.exp-entry + .exp-entry { border-top: 0; }
.exp-entry + .exp-day-rule { border-top: 0; }
.exp-entry { border-bottom: 1.5px dashed var(--df-hair); }
.exp-entry .exp-card-toggle { gap: 12px; padding: 11px 14px 11px 12px; }
.exp-entry.cmp-food { --xk-spine: var(--vn-amber); }
.exp-entry.cmp-transport { --xk-spine: var(--vn-sky); }
.exp-entry.cmp-shopping { --xk-spine: var(--vn-coral); }
.exp-entry.cmp-hotel { --xk-spine: var(--vn-indigo); }
.exp-entry.cmp-activity { --xk-spine: var(--vn-green); }
.exp-entry .exp-card-toggle { border-left: 0; }
.exp-entry-meta { margin-top: 0; }
.expense-amount.xk-row-amount { font-weight: 700; }
/* the category word: spoken everywhere, seen on desktop only */
.exp-entry-cat { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }

/* Fixed Add pill: 52px, 2px ink, 4px shadow, 16/700 */
@media (max-width: 1023px) {
  .exp-add-pill {
    min-height: 52px;
    border: 2px solid var(--df-ink);
    box-shadow: 4px 4px 0 var(--df-ink);
    font-size: 16px;
    font-weight: 700;
  }
}

/* Empty state: cream poster card, left-aligned, fund link below it */
.exp-empty-card { align-items: stretch; gap: 22px; padding: 18px 6px 0; border: 0; border-radius: 0; text-align: left; }
.exp-empty-poster {
  display: flex;
  flex-direction: column;
  gap: 14px;
  padding: 28px 22px 26px;
  background: var(--df-panel);
  border: 2px solid var(--df-ink);
  border-radius: 14px;
  box-shadow: 6px 6px 0 var(--df-ink);
  transform: rotate(-1deg);
}
.exp-empty-headline { margin: 0; font-family: var(--df-display); font-size: 38px; font-weight: 800; line-height: 0.95; letter-spacing: -0.03em; text-transform: uppercase; color: var(--df-ink); }
.exp-empty-poster p { margin: 0; font-size: 16px; line-height: 1.55; color: var(--df-ink); }
.exp-empty-actions { flex-direction: column; flex-wrap: nowrap; gap: 14px; justify-content: flex-start; margin-top: 6px; }
.exp-empty-actions .xk-pill { width: 100%; gap: 8px; }
.exp-empty-actions .xk-pill i { font-size: 20px; }
.exp-empty-fund-link { align-self: center; font-size: 15px; font-weight: 700; color: var(--df-ink); text-decoration: underline; text-underline-offset: 3px; }

/* Desktop >=1024px: summary card is total + mono counts; the Balances column
   already shows To pay / To receive. */
@media (min-width: 1024px) {
  .exp-stub-fields { display: none; }
  .exp-stub-desk { display: block; margin-top: 4px; }
  .exp-ledger-head { display: flex; }
  .exp-ledger-head h3 { margin: 0; font-family: var(--df-display); font-size: 30px; font-weight: 800; letter-spacing: -0.03em; text-transform: uppercase; }
  .exp-entry-cat { position: static; width: auto; height: auto; overflow: visible; clip: auto; }
}

/* ==========================================================================
   Expenses 23 (#2319): detail sheet from the Row board. Kit classes plus
   these; tokens only. Last in the file so they win over the #2235 sheet rules.
   ========================================================================== */
.exp-detail-scrim { background: var(--df-scrim-60); align-items: flex-end; padding: 0; }
@media (min-width: 681px) { .exp-detail-scrim { align-items: center; padding: 1rem; } }
.exp-detail-sheet { padding: 10px 16px 18px; gap: 0; box-shadow: none; border-radius: 18px 18px 0 0; }
@media (min-width: 681px) { .exp-detail-sheet { border-radius: 18px; border: 2px solid var(--df-ink); } }
#expense-detail-body { display: flex; flex-direction: column; gap: 16px; }
.exp-sheet-head { display: flex; align-items: flex-start; gap: 12px; margin: 0; }
.exp-sheet-icon {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 48px;
  height: 48px;
  box-sizing: border-box;
  border: 2px solid var(--df-ink);
  border-radius: var(--radius-pill);
  background: var(--xk-spine, var(--df-ink-soft));
  color: var(--df-amber-fg);
  font-size: 22px;
}
.exp-sheet-icon.cmp-food { --xk-spine: var(--vn-amber); }
.exp-sheet-icon.cmp-transport { --xk-spine: var(--vn-sky); }
.exp-sheet-icon.cmp-shopping { --xk-spine: var(--vn-coral); }
.exp-sheet-icon.cmp-hotel { --xk-spine: var(--vn-indigo); }
.exp-sheet-icon.cmp-activity { --xk-spine: var(--vn-green); }
.exp-sheet-headtext { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.exp-sheet-title { margin: 0; font-family: var(--df-sans); font-size: 18px; font-weight: 700; letter-spacing: 0; text-transform: none; }
.exp-sheet-meta { font-family: var(--df-mono); font-size: 10px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--df-ink-soft); }
.exp-sheet-figure { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: flex-end; gap: 4px 12px; }
.exp-sheet-amount { font-size: 40px; font-weight: 800; display: block; white-space: nowrap; max-width: 100%; }
.exp-sheet-paid { margin: 0; font-size: 14px; color: var(--df-ink); text-align: right; }
.exp-sheet-label { margin: 0; font-family: var(--df-mono); font-size: 11px; font-weight: 700; letter-spacing: 0.1em; color: var(--df-ink); }
.exp-sheet-split { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 200px), 1fr)); column-gap: 20px; margin-top: 4px; }
.exp-sheet-split li { font-size: 15px; padding: 6px 0; border-bottom: 1.5px dashed var(--df-hair); gap: 12px; min-width: 0; }
.exp-sheet-split li span:first-child { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.exp-sheet-split li span:last-child { font-weight: 700; white-space: nowrap; flex: none; }
.exp-receipt-link { display: flex; align-items: center; gap: 12px; min-height: 44px; font-size: 15px; font-weight: 600; color: var(--df-ink); text-decoration: none; }
.exp-receipt-tile {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  box-sizing: border-box;
  border: 2px solid var(--df-ink);
  border-radius: 3px;
  background: var(--df-panel);
  color: var(--df-ink-soft);
  font-size: 20px;
}
.exp-sheet-actions { display: flex; gap: 8px; margin: 0; position: relative; }
.exp-sheet-act { flex: 1; padding: 0 12px; font-size: 15px; }
.exp-sheet-more { flex: none; position: static; }
.exp-sheet-more-btn { width: 48px; padding: 0; }
.exp-sheet-menu {
  position: absolute;
  right: 0;
  bottom: calc(100% + 8px);
  min-width: 180px;
  display: flex;
  flex-direction: column;
  padding: 4px;
  border: 2px solid var(--df-ink);
  border-radius: var(--radius-sm);
  background: var(--df-panel);
  box-shadow: 3px 3px 0 var(--df-ink);
}
.exp-sheet-menu[hidden] { display: none; }
.exp-sheet-menu-item {
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: var(--tap-min);
  padding: 0 12px;
  border: 0;
  background: none;
  color: var(--df-ink);
  font-family: var(--df-sans);
  font-size: 15px;
  font-weight: 600;
  text-align: left;
  cursor: pointer;
}
.exp-sheet-menu-item.exp-delete-link { color: var(--vn-coral-text); }

/* ==========================================================================
   Expenses 25 (#2321): Shared fund view from the Fund board (#2231). Kit
   classes plus these; tokens only. Last in the file so it wins over the
   earlier .fund-* rules (#2244/#2296) it replaces.
   ========================================================================== */
.fund-block { display: flex; flex-direction: column; gap: 12px; margin-bottom: var(--space-lg); }
.fund-card { margin-bottom: 0; padding: 0; gap: 0; border-radius: 12px; box-shadow: none; overflow: hidden; }
.fund-card--form { padding: 14px; gap: var(--space-md); }
.fund-card-top { display: flex; justify-content: space-between; align-items: flex-end; gap: 12px; padding: 14px 16px; }
.fund-left { gap: 2px; min-width: 0; }
.fund-left-label { font-size: 11px; letter-spacing: 0.1em; color: var(--df-ink); }
.fund-left-amount { font-size: 32px; letter-spacing: -0.03em; line-height: 1; }
.fund-meta-text {
  flex: none;
  font-family: var(--df-mono);
  font-size: 10px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--df-ink-soft);
}
.fund-card > .fund-form { padding: 0 16px 12px; }
.fund-progress { margin: 0 16px 14px; background: var(--df-ground); }
.fund-section-head { padding-top: 2px; }
.fund-section-title { margin: 0; }
.fund-pill {
  min-height: var(--tap-min);
  padding: 0 14px;
  border: 2px solid var(--df-ink);
  border-radius: var(--radius-pill);
  background: var(--df-panel);
  color: var(--df-ink);
  font-family: var(--df-sans);
  font-size: 14px;
  font-weight: 700;
  cursor: pointer;
}
.fund-pill--add { background: var(--df-brand); color: var(--df-brand-fg); }
.fund-pill--coral { border-color: var(--vn-coral-text); color: var(--vn-coral-text); }
.fund-card--list .fund-list { gap: 0; }
.fund-row {
  box-sizing: border-box;
  min-height: var(--tap-min);
  padding: 10px 14px;
  border-bottom: 1.5px dashed var(--df-hair);
  font-size: 15px;
  color: var(--df-ink);
}
.fund-list > .fund-row:last-child { border-bottom: 0; }
.fund-row-amount { font-weight: 700; }
.fund-row-end { gap: 4px; }
.fund-edit-btn { width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; padding: 0; color: var(--df-ink); }
.fund-empty { padding: 14px; font-size: 15px; color: var(--df-ink-soft); }
.fund-entries-link {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  min-height: var(--tap-min);
  padding: 0;
  border: 0;
  background: none;
  color: var(--df-ink);
  font-family: var(--df-sans);
  font-size: 15px;
  cursor: pointer;
}
.fund-another { align-self: flex-start; font-weight: 700; }
.fund-field { width: auto; flex: 1; min-width: 120px; }
.fund-block .fund-actions .fund-field { flex: none; }
.fund-block .fund-section { border-top: 0; padding-top: 0; }

/* Issue #2311 — welcome card entrance and dismiss resolve to the finished state. */
@media (prefers-reduced-motion: reduce) {
  .onboarding-card--enter,
  .onboarding-card--enter .onboarding-card-points li,
  .onboarding-card--leave { animation: none; }
}

/* ── Bookings list (Bookings redesign M1, #2349) ──────────────────
   Three groups, one plain row per stay, leg or item, one status word at the
   right. No buttons on a row: every action lives in the sheet a row opens. */
.bk-bar {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--space-md);
  margin-bottom: var(--space-lg);
}
.bk-people-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--tap-min);
  height: var(--tap-min);
  padding: 0;
  border: 2px solid var(--df-ink);
  border-radius: 50%;
  background: var(--df-panel);
  color: var(--df-ink);
  font-size: var(--fs-lg);
  cursor: pointer;
}
.bk-add-btn { min-height: var(--tap-min); }
@media (max-width: 1023px) {
  /* Fixed above the trip tab bar, like Expenses' Add (#2239). */
  .bk-bar {
    position: fixed;
    /* .app-main is a centred 680px strip below 1024px, so between 681px and
       1023px (a desktop window at 200% zoom) the viewport edge is not the
       column edge. Percentages keep the pill on the column. */
    right: max(var(--space-lg), env(safe-area-inset-right), calc((100% - 680px) / 2 + var(--space-lg)));
    bottom: calc(76px + env(safe-area-inset-bottom) + var(--space-lg));
    z-index: 30;
    margin: 0;
  }
  #panel-bookings { padding-bottom: 6rem; }
}
.bk-first-run {
  border: 2px solid var(--df-ink);
  border-radius: var(--radius-lg, 12px);
  background: var(--df-panel);
  padding: var(--space-xl);
}
.bk-first-run-title { margin: 0 0 var(--space-lg); font-weight: 700; font-size: var(--fs-md); color: var(--df-ink); }
.bk-first-run-actions { display: flex; flex-direction: column; gap: var(--space-md); }
.bk-first-run-actions .bk-btn { min-height: var(--tap-min); }
.bk-summary { display: flex; margin-bottom: var(--space-lg); }
.bk-open-chip {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-min);
  padding: 0 var(--space-xl);
  border: 2px solid var(--df-ink);
  border-radius: var(--radius-pill);
  background: var(--df-panel);
  color: var(--df-ink);
  font-family: inherit;
  font-size: var(--fs-md);
  font-weight: 600;
  cursor: pointer;
}
.bk-open-chip[aria-pressed="true"] { background: var(--df-ink); color: var(--df-panel); }
.bk-group { margin-bottom: var(--space-3xl); }
.bk-group-title {
  font-family: var(--df-mono);
  font-weight: 700;
  font-size: var(--fs-xs);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--df-ink-soft);
  margin: 0 0 var(--space-md);
}
.bk-rows { list-style: none; margin: 0; padding: 0; }
.bk-row {
  display: flex;
  align-items: center;
  gap: var(--space-lg);
  width: 100%;
  min-height: var(--tap-min);
  padding: var(--space-lg) var(--space-xl);
  border: 0;
  border-bottom: 1px solid var(--df-hair);
  background: transparent;
  color: var(--df-ink);
  font-family: inherit;
  text-align: left;
  cursor: pointer;
}
.bk-row:hover { background: var(--df-chip-fill); }
.bk-row-icon { flex-shrink: 0; font-size: var(--fs-xl); color: var(--df-ink-soft); }
.bk-row-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: var(--space-2xs); }
.bk-row-title { font-weight: 700; font-size: var(--fs-md); overflow-wrap: anywhere; }
.bk-row-sub { font-size: var(--fs-sm); color: var(--df-ink-soft); overflow-wrap: anywhere; }
.bk-row-warn { font-size: var(--fs-sm); color: var(--vn-coral-text); }
.bk-row-status {
  flex-shrink: 0;
  font-size: var(--fs-sm);
  font-weight: 600;
  padding: var(--space-2xs) var(--space-md);
  border: 1px solid var(--df-ink);
  border-radius: var(--radius-pill);
  white-space: nowrap;
}
.bk-row-status.is-open { border-style: dashed; color: var(--vn-coral-text); border-color: var(--vn-coral); }
.bk-row-status.is-booked { background: var(--df-panel); }
.bk-skeleton-row { padding: var(--space-lg) var(--space-xl); }

/* Bookings Today view (#2363) */
.bk-today-heading { margin: 0 0 var(--space-lg); font-size: var(--fs-lg); font-weight: 700; color: var(--df-ink); }
.bk-today-cards { list-style: none; margin: 0 0 var(--space-xl); padding: 0; display: flex; flex-direction: column; gap: var(--space-lg); }
.bk-today-card { border: 2px solid var(--df-ink); border-radius: var(--radius-lg, 12px); background: var(--df-panel); padding: var(--space-xl); }
.bk-today-card-main { display: flex; align-items: flex-start; gap: var(--space-lg); }
.bk-today-time { font-family: var(--df-mono); font-size: var(--fs-sm); color: var(--df-ink-soft); }
.bk-today-card-actions { display: flex; gap: var(--space-md); margin-top: var(--space-lg); }
.bk-today-card-actions .bk-btn { min-height: var(--tap-min); min-width: var(--tap-min); display: inline-flex; align-items: center; justify-content: center; text-decoration: none; }
.bk-today-card-actions .bk-btn-primary { flex: 1; }
.bk-today-strip, .bk-today-earlier {
  display: block; width: 100%; min-height: var(--tap-min); margin: 0 0 var(--space-xl); padding: 0 var(--space-xl);
  border: 2px dashed var(--vn-coral); border-radius: var(--radius-lg, 12px); background: transparent;
  color: var(--vn-coral-text); font-family: inherit; font-size: var(--fs-md); font-weight: 600; text-align: left; cursor: pointer;
}
.bk-today-earlier { border: 0; border-bottom: 1px solid var(--df-hair); border-radius: 0; color: var(--df-ink-soft); margin: 0; }

/* Issue #2408 — Trip Settings → Preferences reuses the wizard's chips around
   real inputs: the input is visually hidden but stays focusable, and the chip
   reads :checked / :focus-visible / :disabled off it. */
.wizard-chip-input { position: absolute; opacity: 0; width: 1px; height: 1px; margin: 0; pointer-events: none; }
.wizard-chip { position: relative; gap: var(--space-sm); }
.wizard-chip:has(> .wizard-chip-input:checked) {
  background: var(--vn-teal-light);
  border-color: var(--vn-teal);
  color: var(--vn-teal);
  font-weight: 600;
}
.wizard-chip:has(> .wizard-chip-input:focus-visible) { outline: 3px solid var(--vn-teal); outline-offset: 2px; }
.wizard-chip:has(> .wizard-chip-input:disabled) { cursor: default; opacity: 0.7; }
.wizard-chip:has(> .wizard-chip-input:disabled):hover,
.wizard-chip:has(> .wizard-chip-input:disabled):active { transform: none; box-shadow: var(--df-press); }
.wizard-chip-grid--inline { flex-direction: row; flex-wrap: wrap; }
.wizard-chip-grid--inline .wizard-chip { width: auto; }
.trip-pref-group { border: 0; padding: 0; margin: 0 0 var(--space-xl); min-width: 0; }
.trip-pref-summary { margin: 0 0 var(--space-lg); display: grid; gap: var(--space-sm); }
.trip-pref-summary > div { display: flex; flex-wrap: wrap; gap: var(--space-xs) var(--space-md); }
.trip-pref-summary dt { min-width: 7rem; font-weight: 600; color: var(--df-ink); }
.trip-pref-summary dd { margin: 0; flex: 1 1 12rem; color: var(--df-ink-soft); overflow-wrap: anywhere; }
.trip-pref-section { margin: 0 0 var(--space-xl); padding-top: var(--space-md); border-top: 1px solid var(--border); }
.trip-pref-section-title { margin: 0 0 var(--space-md); font-size: var(--fs-lg); font-weight: 700; color: var(--df-ink); }
.trip-pref-section .set-block-hint { max-width: none; }
.trip-pref-pair { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-md); }
@media (max-width: 680px) { .trip-pref-pair { grid-template-columns: 1fr; } }
.trip-pref-interest-row { margin: 0 0 var(--space-md); }
.trip-pref-row-title { margin: 0 0 var(--space-xs); font-size: var(--fs-sm); color: var(--df-ink-soft); }
.trip-pref-places { margin: 0 0 var(--space-md); padding-left: var(--space-lg); color: var(--df-ink-soft); }
.trip-prefs-saved { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-sm) var(--space-md); }
.trip-prefs-saved[hidden] { display: none; }
.trip-pref-legend { padding: 0; font-size: var(--fs-md); font-weight: 600; color: var(--df-ink); }

/* Trip Preferences (#2409): Save stays in reach on a long form; the sharing
   block is its own card because, unlike the form, it saves on change. */
.trip-prefs-actions { position: sticky; bottom: 0; z-index: 2; align-items: center; padding: var(--space-md) 0; background: var(--surface-2); }
.trip-prefs-actions .pf-unsaved { margin: 0; }
.trip-sharing-card { border: 1px dashed var(--border-strong); }
.trip-sharing-auto { margin: 0 0 var(--space-md); font-size: var(--fs-sm); font-weight: 600; color: var(--df-ink-soft); }

/* Deciding a stay in Bookings (E4-2, #2358): options reuse the
   .accommodation-option-row anatomy; these are the vote control and the rows
   around the list. 44px targets; status is always in words, not colour alone. */
.bk-stay-deciding { display: flex; flex-direction: column; gap: var(--space-md); }
.bk-vote-btn {
  min-height: var(--tap-min);
  padding: 0 var(--space-lg);
  border: 2px solid var(--df-ink);
  border-radius: var(--radius-pill);
  background: var(--df-panel);
  color: var(--df-ink);
  font-family: inherit;
  font-size: var(--fs-sm);
  font-weight: 600;
  cursor: pointer;
}
.bk-vote-btn.sel { background: var(--df-brand); color: var(--df-brand-fg); }
.bk-vote-closed { font-size: var(--fs-sm); color: var(--df-ink-soft); }
.bk-stay-add {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  width: 100%;
  min-height: var(--tap-min);
  padding: 0;
  border: none;
  border-top: 2px solid var(--df-hair);
  background: none;
  color: var(--df-ink);
  font-family: inherit;
  font-size: var(--fs-md);
  font-weight: 600;
  cursor: pointer;
}
.bk-stay-pick { align-self: stretch; }
.bk-stay-host summary { min-height: var(--tap-min); display: flex; align-items: center; cursor: pointer; font-weight: 600; }
.bk-stay-host-actions { display: flex; flex-wrap: wrap; gap: var(--space-md); padding: var(--space-md) 0; }
.bk-stay-picker { margin: 0; color: var(--df-ink-soft); font-size: var(--fs-md); }
.bk-stay-picked { display: flex; flex-direction: column; gap: var(--space-md); }
.bk-stay-picked-name { margin: 0; font-size: var(--fs-lg); }

/* ==========================================================================
   Expenses 22A (#2421, part of #2318): the Add/Edit expense sheets on the
   xk- kit. Apricot ground, cream fields, sentence-case labels, a dashed scan
   tile and a cream footer bar. Tokens only; nothing here travels, so the
   prefers-reduced-motion block has nothing to answer.
   ========================================================================== */
.exp-form-sheet.xk-sheet { display: block; background: var(--df-ground); }
.exp-form-sheet .xk-sheet-head { padding-left: 0; padding-right: 0; }
.exp-form-sheet .xk-sheet-close { flex: none; min-width: 44px; }

/* Cream fields at the kit's 48px; the ID rules above would otherwise win */
#addExpenseModal .xk-field,
#edit-expense-modal .xk-field {
  height: 48px;
  min-height: 48px;
  padding: 0 14px;
  border: 2px solid var(--df-ink);
  border-radius: var(--radius-sm);
  background-color: var(--df-panel);
  color: var(--df-ink);
  font-family: var(--df-sans);
}
.exp-form-sheet .xk-field-label { margin-bottom: 6px; }
.exp-form-sheet .exp-form-section-legend.xk-field-label { margin-bottom: 6px; }

/* Currency + Amount: two separate fields side by side */
.exp-form-sheet .exp-amount-wrap { gap: 10px; align-items: flex-start; }
.exp-form-sheet .exp-currency-picker { flex: none; width: 100px; min-width: 100px; }
.exp-form-sheet .exp-amount-col { flex: 1; min-width: 0; }
.exp-form-sheet .exp-currency-sel,
.exp-form-sheet .exp-amount-inp {
  width: 100%;
  border-radius: var(--radius-sm) !important;
  border-width: 2px !important;
}
#addExpenseModal .exp-amount-inp,
#edit-expense-modal .exp-amount-inp { font-size: 18px; font-weight: 700; }

/* Scan: one full-width dashed tile, no hairline under it */
.exp-form-sheet .exp-scan-row { border-bottom: 0; padding-bottom: 0; }
.exp-form-sheet .exp-scan-row .scan-btns { display: flex; flex-direction: column; gap: var(--space-sm); }

/* Footer: cream bar, 2px ink top rule, a 52px primary pill. #2537 — the sheet
   padding is one token (--exp-sheet-pad, 2rem, 1.5rem at <=480px) and the
   footer's negative margins and sticky offset reuse it, so the bar runs flush
   to the sheet's left/right/bottom edges and cannot drift from the padding. */
.exp-form-sheet {
  --exp-sheet-pad: 2rem;
  padding: var(--exp-sheet-pad);
}
@media (max-width: 480px) {
  .exp-form-sheet { --exp-sheet-pad: 1.5rem; }
}
.exp-form-sheet .exp-modal-sticky-footer {
  background: var(--df-panel);
  border-top: 2px solid var(--df-ink);
  bottom: calc(-1 * var(--exp-sheet-pad));
  padding: var(--space-md) var(--exp-sheet-pad) calc(var(--space-md) + env(safe-area-inset-bottom));
  margin: var(--space-md) calc(-1 * var(--exp-sheet-pad)) calc(-1 * var(--exp-sheet-pad));
}
.exp-form-sheet .exp-footer-pill { width: 100%; min-height: 52px; }

/* ==========================================================================
   Expenses 22B (#2422, part of #2318): category tiles, Split between,
   Not equal? and More details on the Add/Edit expense sheets. Tokens only;
   nothing here travels, so the prefers-reduced-motion block has nothing to
   answer (the legacy chip/chevron transitions are already covered there).
   ========================================================================== */
/* Category: xk-tile, selected = solid category fill with an ink icon */
.exp-form-sheet .exp-cat-chip-row { gap: 8px; padding-bottom: 2px; }
.exp-form-sheet .exp-cat-chip.xk-tile {
  width: 52px;
  min-width: 52px;
  height: 48px;
  min-height: var(--tap-min);
  padding: 0;
  border: 2px solid var(--df-ink);
  background: var(--df-panel);
}
.exp-form-sheet .exp-cat-chip.xk-tile.sel { color: var(--df-ink); border-color: var(--df-ink); }
.exp-form-sheet .exp-cat-chip.xk-tile.sel i { color: var(--df-ink); }
.exp-form-sheet .exp-cat-chip.xk-tile.sel.cat-food { background: var(--vn-amber); }
.exp-form-sheet .exp-cat-chip.xk-tile.sel.cat-transport { background: var(--vn-sky); }
.exp-form-sheet .exp-cat-chip.xk-tile.sel.cat-shopping { background: var(--vn-coral); }
.exp-form-sheet .exp-cat-chip.xk-tile.sel.cat-hotel { background: var(--vn-indigo); }
.exp-form-sheet .exp-cat-chip.xk-tile.sel.cat-activity { background: var(--vn-green); }
.exp-form-sheet .exp-cat-chip.xk-tile.sel.cat-other { background: var(--df-amber); }

/* Split between: ink-selected segments; member pills show their state */
.exp-form-sheet .xk-seg-btn { flex: 1; }
.exp-form-sheet .pax-toggle .pax-btn.sel {
  background: var(--df-ink);
  border-color: var(--df-ink);
  color: var(--df-panel);
}
.exp-form-sheet .split-summary-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  margin-top: var(--space-sm);
}
.exp-form-sheet .split-summary-row .split-summary { margin: 0; flex: 1; min-width: 0; }
.exp-form-sheet .split-notequal-link {
  margin-left: auto;
  color: var(--df-brand);
  font-size: 15px;
  font-weight: 700;
  text-decoration: underline;
}
.exp-form-sheet .split-modes { margin-top: var(--space-sm); }

/* AI-scan toggle sits under the scan tile */
.exp-form-sheet .exp-scan-ai { display: flex; margin-top: var(--space-sm); }
.exp-form-sheet .exp-scan-ai .exp-ai-scan-toggle { margin-left: 0; }

/* More details: kit disclosure row under a 2px ink top rule */
.exp-form-sheet .form-more-toggle.xk-disclosure {
  margin-top: var(--space-md);
  padding: 0;
  border: 0;
  border-top: 2px solid var(--df-ink);
  border-radius: 0;
  background: transparent;
  color: var(--df-ink);
}
.exp-form-sheet .exp-day-chip-wrap { display: block; }
.exp-form-sheet .exp-day-chip-sel { width: 100%; height: 48px; border-radius: var(--radius-sm); }

/* ── Destinations: read-first stub + editor ─────────────────── */
.set-dest-summary { font-size: var(--fs-sm); color: var(--df-ink-soft); margin-bottom: var(--space-xs); }
.set-dest-read-list { list-style: none; margin: 0 0 var(--space-md); padding: 0; display: flex; flex-direction: column; gap: var(--space-xs); }
.set-dest-read-row { display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-sm); font-size: var(--fs-base); }
.set-dest-read-row .set-dest-leg-range { font-size: var(--fs-sm); }
.set-dest-edit-btn { min-height: var(--tap-min); margin-bottom: var(--space-md); }
.set-dest-leg { padding-bottom: var(--space-md); border-bottom: 1px solid var(--df-hair); }
.set-dest-leg-head { display: flex; align-items: center; gap: var(--space-sm); margin-bottom: var(--space-xs); }
.set-dest-leg-head .set-dest-leg-name { flex: 1; min-width: 0; margin-bottom: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.set-dest-leg-reorder { display: flex; gap: var(--space-sm); }
.set-dest-btn { min-width: var(--tap-min); min-height: var(--tap-min); display: flex; align-items: center; justify-content: center; background: var(--df-hair); border: none; border-radius: var(--radius-pill); color: var(--df-ink); cursor: pointer; }
.set-dest-btn:disabled { opacity: 0.3; cursor: not-allowed; }
.set-dest-remove { margin-top: var(--space-md); min-height: var(--tap-min); display: inline-flex; align-items: center; gap: var(--space-xs); background: none; border: none; color: var(--vn-coral-text); font-family: inherit; font-size: var(--fs-sm); cursor: pointer; }
.set-dest-editor-actions { display: flex; gap: var(--space-sm); margin: var(--space-md) 0; }
.set-dest-editor-actions .btn-small { min-height: var(--tap-min); }

/* Bookings partner entry (M9, issue 2366) — one reserved place, with its disclosure. */
.bk-partner { margin-top: var(--space-lg); }
.bk-partner .bk-btn { min-height: var(--tap-min); }
.bk-partner-note { margin: var(--space-sm) 0 0; font-size: var(--fs-xs); color: var(--text-muted); }

/* ==========================================================================
   Expenses 22C (issue 2423): the Stop field is a button that opens a picker sheet.
   Tokens only; nothing travels, so the reduced-motion block has nothing to add.
   ========================================================================== */
.exp-link-field { position: relative; }
.exp-link-btn {
  display: flex;
  align-items: center;
  gap: 8px;
  padding-right: 52px;
  text-align: left;
  cursor: pointer;
  overflow: hidden;
}
.exp-link-btn-text { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.exp-link-btn-empty { color: var(--df-ink-soft, var(--text-muted)); }
.exp-link-clear {
  position: absolute;
  top: 2px;
  right: 2px;
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--tap-min);
  height: 44px;
  border: 0;
  background: transparent;
  color: var(--df-ink);
  cursor: pointer;
}
.exp-link-clear[hidden] { display: none; }
.exp-link-sheet { max-height: 85vh; max-height: 85dvh; overflow: hidden; }
.exp-link-search { padding: 0 16px 8px; }
.exp-link-count { margin: 6px 0 0; font-size: 13px; color: var(--df-ink); }
.exp-link-list { flex: 1; min-height: 0; overflow-y: auto; padding: 0 16px 16px; }
.exp-link-group {
  margin: 14px 0 6px;
  font-family: var(--df-sans);
  font-size: 12px;
  font-weight: 800;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--df-ink);
}
.exp-link-opt {
  display: flex;
  align-items: center;
  gap: 10px;
  box-sizing: border-box;
  width: 100%;
  min-height: var(--tap-min);
  margin-bottom: 6px;
  padding: 8px 12px;
  border: 2px solid var(--df-ink);
  border-radius: var(--radius-sm);
  background: var(--df-panel);
  color: var(--df-ink);
  font-family: var(--df-sans);
  text-align: left;
  cursor: pointer;
}
.exp-link-opt.is-on { background: var(--df-amber); color: var(--df-amber-fg); }
.exp-link-empty { margin: 16px 0; color: var(--df-ink); }

/* Expenses: former inline styles in js/expenses.js. Same values, now classes. */
.unequal-mismatch[hidden] { display: none; }
.exp-item-adjust { margin-top: var(--space-xs); }
.exp-item-adjust-dates { display: flex; align-items: center; gap: var(--space-sm); margin-top: var(--space-xs); }
.exp-scan-progress { font-size: var(--fs-base); color: var(--vn-teal); }
.exp-item-taxable { display: flex; align-items: center; gap: var(--space-sm); font-size: var(--fs-xs); color: var(--text-muted); margin-top: var(--space-xs); }
.exp-items-footer { display: flex; gap: var(--space-md); align-items: center; margin-top: var(--space-sm); }
.exp-items-total-note { font-size: var(--fs-xs); color: var(--text-muted); }
.btn-small.exp-show-all-btn { width: 100%; }
.exp-modal-empty { padding: 1rem 0; }
.exp-muted { color: var(--text-muted); }
.exp-coral { color: var(--vn-coral); }
.exp-teal { color: var(--vn-teal); }
.exp-strong { font-weight: 600; }
.exp-share-heading { font-weight: 600; font-size: var(--fs-base); margin-bottom: var(--space-sm); }
.exp-share-hint { font-size: var(--fs-sm); color: var(--text-muted); }
.btn-small.exp-add-item-btn { margin-top: var(--space-lg); }
.balance-name-row { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: var(--space-xs); }
.balance-badges { display: flex; gap: var(--space-xs); flex-wrap: wrap; }
.settle-currency-picker { margin-top: var(--space-sm); display: flex; gap: var(--space-sm); flex-wrap: wrap; align-items: center; }
.settle-rate-label { font-size: var(--fs-sm); color: var(--text-muted); }
.settle-rate-input { width: 90px; }
.empty.exp-settled-empty { padding: 0.75rem 0; font-size: var(--fs-base); }
