/* ============================================================
   FONT: Inter — the same typeface notion.com uses, and the standard
   free stand-in for Apple's SF Pro. We SELF-HOST one small file
   (public/fonts/inter.woff2) so it works offline and in places where
   Google Fonts is blocked (e.g. mainland China / the Shanghai trip).
   It's a "variable" font: one file covers every weight 100–900.
   ============================================================ */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 100 900;            /* the whole weight range, from one file */
  font-display: swap;              /* show text immediately, swap in Inter when ready */
  src: url('fonts/inter.woff2') format('woff2');
}

/* Reset: remove default spacing and make sizing predictable */
* { margin: 0; padding: 0; box-sizing: border-box; }
/* Buttons/inputs don't inherit the page font by default — make them */
button, input, select { font: inherit; color: inherit; }
button { background: none; border: none; cursor: pointer; border-radius: var(--r-btn); }

/* ============================================================
   SIMPLE GLASS — the one look every box in the app shares.
   A box is just four things:
     1) a dark SEE-THROUGH tint (you can still see the globe through it)
     2) a soft BLUR of whatever sits behind it, so text stays readable
     3) a thin light OUTLINE
     4) ROUND corners
   That's the whole material. Change the knobs below and every box
   in the app follows.
   ============================================================ */
:root {
  color-scheme: dark;                /* native date/time pickers render dark */

  /* The notch/Dynamic Island, the home indicator and a landscape phone's
     rounded corners, published as plain numbers. The browser hands these to
     CSS only, and JS needs them too (placeMenuAnchored keeps a context menu
     out of them), so they live here once and are read back with
     getComputedStyle rather than guessed at in two places. */
  --sa-top: env(safe-area-inset-top, 0px);
  --sa-right: env(safe-area-inset-right, 0px);
  --sa-bottom: env(safe-area-inset-bottom, 0px);
  --sa-left: env(safe-area-inset-left, 0px);

  /* The app's base page color — every full-bleed background (html, the
     portrait-lock overlay, the cost panel, the AI menu's card and desktop
     sidebar, the mobile AI menu's fullscreen state) reads this ONE value
     rather than repeating a literal black, so they can never drift out of
     sync with each other. Started as Kalshi's own dark (not pure black)
     background, matched exactly via getComputedStyle on their <body>
     (rgb(10, 12, 15)) — picked because a true #000 base reads as a hole in
     the screen next to the globe's real photographic colors. Nudged a bit
     darker than that exact match, per the traveler's own live call once he
     saw it running (rgb(7, 8, 11) — same warm-black hue, ~30% less light). */
  --bg: #07080b;

  --glass-bg:     rgba(10, 10, 14, 0.55);     /* the dark see-through tint */
  --glass-blur:   blur(14px);                 /* how soft the background gets behind a box */
  --glass-border: rgba(var(--fg-rgb), 0.14);  /* the thin light outline */
  --glass-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);  /* soft shadow so boxes float */
  --press:        rgba(var(--fg-rgb), 0.16);  /* any box while your finger is on it */
  --hover:        rgba(var(--fg-rgb), 0.09);  /* mouse-over preview, one notch lighter
                                                  than resting, one notch quieter than --press —
                                                  see it before you commit to the click */
  /* Rows that live INSIDE an already-glass card (menu rows, the ideas "add" button)
     skip the full floating --glass-shadow (double-blur reads muddy) but still want
     SOME depth so they don't feel like a flatter, cheaper material than everything
     else in the app — a quiet fraction of the real thing. */
  --flat-shadow:  0 2px 10px rgba(0, 0, 0, 0.28);

  /* Text/border/hover colors that need to flip between dark and light mode.
     --fg / --fg-rgb are the app's plain text color (rgba() tints of text,
     hairlines and hover washes are almost always this same white at some
     opacity, hence --fg-rgb — see the [data-theme="light"] block below).
     --fg2 is the slightly warm off-white text used by the Notion-matched AI
     chat/sidebar surfaces (see #ai-menu below); --card-bg is the near-black
     raised card those surfaces sit on. --invert-bg/--invert-fg are the
     handful of "solid, this-is-the-one-that-pops" controls (Save button,
     an active/checked pill) that are a solid white chip in dark mode and
     need to become a solid DARK chip in light mode to keep popping. */
  --fg: #fff;
  --fg-rgb: 255, 255, 255;
  --fg2: #f0efed;
  --fg2-dim: #9b9a97;
  --fg2-faint: #6f6e69;
  --card-bg: #141414;
  --card-bg-rgb: 20, 20, 20;   /* same raised card, as r,g,b for translucent chips over it */
  --invert-bg: #fff;
  --invert-fg: #000;
  /* The globe/space pane's own background. Deliberately NOT --bg — the
     globe is "space", so it stays this same near-black in both themes
     while --bg itself flips. See #globe below, where this is applied. */
  --space-bg: #07080b;

  /* corner roundness
     --r-chip is the calendar day box (square + soft corner). Buttons use the
     same language (--r-btn) so chrome feels like the grid, not a Grok pill. */
  --r-pill: 999px;
  --r-card: 24px;
  --r-chip: 14px;
  --r-btn: 12px;

  /* Motion — snappy, eased, never bouncy */
  --ease-out: cubic-bezier(0.22, 1, 0.36, 1);
  --ease-snap: cubic-bezier(0.32, 0.72, 0, 1);
  /* True ease-in-ease-out (slow start, fast middle, slow end) — DESKTOP day-turn only
     (nav arrows + trackpad/mouse drag). --ease-snap above starts almost instantly
     (no ease-in), which is right for a finger already moving on a touchscreen but
     reads as an abrupt jolt on desktop, where the "swipe" is a released drag with
     no ongoing finger contact. Must match DESKTOP_DAY_TURN_MS in app.js. */
  --ease-in-out-snap: cubic-bezier(0.45, 0, 0.2, 1);
  --dur-view: 0.28s;
  --dur-item: 0.24s;
  --dur-tap: 0.16s;
  /* Was 0.11s — enough for the old ~22px fade, too short for a full pane-width
     slide (see the day-turn JS: a live drag now moves the ENTIRE day, not a
     sliver of it). Must match DAY_TURN_MS in app.js. */
  --dur-day-turn: 0.22s;
  /* Desktop-only day turn: faster than the touch version above — a mouse/trackpad
     release has no finger still touching the glass to "carry" the motion, so it
     needs to snap over noticeably less time to still read as quick. */
  --dur-day-turn-desktop: 0.15s;

  /* Desktop Grok-style left sidebar. Collapsed = icon rail; expanded = labels. */
  --sidebar-w: 56px;
}
body.sidebar-expanded { --sidebar-w: 256px; }

/* LIGHT MODE. Toggled in Settings (see #theme-toggle in app.js), which sets
   data-theme="light" on <html> and remembers it in localStorage. Off-white,
   not pure white — matched to what Stripe/Notion/Linear use for a light
   background (a hint of warmth so it doesn't glare next to the globe's dark
   half). Every value here is one of the flipping variables defined in :root
   above; nothing else in this file should need a [data-theme="light"] rule
   of its own. --space-bg is deliberately absent — the globe stays dark in
   every theme. */
[data-theme="light"] {
  color-scheme: light;               /* native date/time pickers render light */
  --bg: #f4f2ee;
  /* Cards read as near-solid white (not hazy translucent glass — that read
     "washed out" rather than "polished" on a light page) with a crisp hairline
     and a real two-layer shadow: a tight near shadow for edge definition plus
     a soft ambient one for lift, the same recipe Stripe/Linear-style light
     UIs use instead of one big blurred shadow. */
  --glass-bg:     rgba(255, 255, 255, 0.82);
  --glass-border: rgba(20, 20, 22, 0.09);
  --glass-shadow: 0 1px 2px rgba(20, 20, 22, 0.04), 0 8px 24px rgba(20, 20, 22, 0.07);
  --press:        rgba(20, 20, 22, 0.10);
  --hover:        rgba(20, 20, 22, 0.05);
  --flat-shadow:  0 1px 4px rgba(20, 20, 22, 0.06);
  --fg: #17161a;
  --fg-rgb: 23, 22, 26;
  --fg2: #2b2a27;
  --fg2-dim: #5c5a54;
  --fg2-faint: #8a8883;
  --card-bg: #ffffff;
  --card-bg-rgb: 255, 255, 255;
  --invert-bg: #17161a;
  --invert-fg: #ffffff;
}

/* PORTRAIT LOCK (phone only). iOS Safari has no real orientation-lock API for
   a plain web page, so we can't stop the phone from physically rotating —
   instead, the moment a PHONE (touch, and narrower than the smallest iPad
   landscape width of 1024px) goes landscape, this covers the whole screen
   solid black and tells the user to rotate back. The app underneath keeps
   running (nothing to save/restore) — it's just hidden. iPad and desktop
   landscape are deliberately supported layouts elsewhere in this file and
   must NOT match this rule. */
#portrait-lock {
  display: none;
}
@media (orientation: landscape) and (pointer: coarse) and (max-width: 926px) {
  #portrait-lock {
    display: flex;
    position: fixed;
    inset: 0;
    z-index: 9999;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 20px;
    padding: 40px;
    background: var(--bg);
    text-align: center;
  }
  #portrait-lock svg {
    width: 48px;
    height: 48px;
    color: rgba(var(--fg-rgb), 0.7);
    transform: rotate(90deg);
  }
  #portrait-lock p {
    max-width: 280px;
    color: rgba(var(--fg-rgb), 0.85);
    font-family: 'Inter', -apple-system, sans-serif;
    font-size: 15px;
    line-height: 1.5;
  }
}

/* Whole-word text — wrap at spaces; only break a word if it alone is wider
   than the box (long URLs / codes). Never force mid-word chops for normal text. */
.wrap-words {
  white-space: pre-line;
  overflow-wrap: break-word;
  word-break: normal;
  hyphens: none;
  max-width: 100%;
  min-width: 0;
}

html {
  height: 100%;
  background: var(--bg);             /* the base layer of the page — see --bg */
  -webkit-tap-highlight-color: transparent;  /* no grey flash on iPhone taps */
  overflow: hidden;                 /* no page scroll — keyboard must not pan the shell */
  overflow-x: hidden;               /* no sideways page scroll — vertical only */
  overscroll-behavior: none;        /* kill rubber-band / keyboard-driven overscroll */
  /* iOS Safari (and Chrome-on-iOS, same WebKit engine underneath) silently
     boosts font sizes in narrow text columns to help readability — a real-
     device-only behavior Chrome DevTools' desktop device simulation never
     reproduces, since that's plain Blink with no text-autosizer at all. That
     mismatch is exactly why fonts look right in DevTools but oversized on a
     real iPhone. Pinning this to 100% turns the boost off so our own sizes
     are what actually render. */
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
}
body {
  height: 100%;
  color: var(--fg);
  /* Inter first; fall back to Apple's own SF Pro if the file ever fails to load */
  font-family: 'Inter', -apple-system, system-ui, sans-serif;
  letter-spacing: -0.011em;         /* the slightly tight tracking that reads as "Apple" */
  -webkit-font-smoothing: antialiased;       /* crisp, thin text on Mac/iPhone */
  -moz-osx-font-smoothing: grayscale;
  overflow: hidden;                 /* no scrolling — it fills the screen like an app */
  overflow-x: hidden;
  overscroll-behavior: none;
  /* Behave like an app, not a web page: long-pressing never highlights text or
     pops up the iOS copy/lookup bubble. We turn selection back ON for inputs below. */
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
}
/* The only place you SHOULD be able to select/edit text is a form field */
input { -webkit-user-select: text; user-select: text; }

/* The starfield canvas sits behind the globe map, clipped to the globe half only
   (#globe has overflow:hidden) — it used to be position:fixed across the WHOLE
   viewport, which on the stacked mobile layout meant it kept redrawing, every
   frame, forever, an entire invisible copy of itself under the opaque calendar
   half. That's real, constant GPU fill-rate cost for zero visible benefit — a
   big part of why animations felt clunky on a real phone despite looking fine
   in a desktop browser (desktop GPUs don't notice; mobile GPUs do). */
#stars {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 0;                       /* behind #map, which paints on top */
  display: block;
}

/* The app fills the screen and stacks the two halves vertically.
   FIXED to the layout viewport (inset: 0) — NOT height: 100dvh.
   Why: 100dvh shrinks when the mobile keyboard opens, so the whole
   globe/calendar flex layout compressed and "jumped up" with the keyboard.
   A fixed shell stays put; the keyboard overlays on top (see also the
   viewport meta interactive-widget=overlays-content). */
#app {
  position: fixed;
  inset: 0;
  width: 100%;
  max-width: 100%;
  height: auto;                     /* height comes from inset: 0, not dvh */
  display: flex;
  flex-direction: column;
  z-index: 1;                       /* ...and the app sits above the stars */
  overflow: hidden;
  overflow-x: hidden;               /* vertical layout only — never pan sideways */
}

/* Two fixed halves. Portrait = stacked 50/50 (globe top, calendar bottom).
   Landscape = side-by-side 50/50 width (globe left, calendar right).
   Calendar always paints ABOVE the globe (higher z-index) so the map canvas
   can never cover the back button or crop the calendar header at the seam. */
#globe, #calendar {
  flex: 0 0 50%;
  min-height: 0;                    /* lets the halves shrink correctly */
  min-width: 0;                     /* flex children can shrink so text wraps, not scroll-x */
  max-width: 100%;
  overflow-x: hidden;
  position: relative;
}
#globe {
  z-index: 1;
  overflow: hidden;                 /* hard-clip map/menus to this half only */
  background: var(--space-bg);      /* space stays dark in light mode too — see --space-bg */
}
#calendar {
  z-index: 2;                       /* never sit under the globe at the split */
  overflow-x: hidden;
  overflow-y: hidden;               /* scroll lives inside day/month views, not here */
}

/* Portrait (phone/tablet upright): exact half-and-half height. */
@media (orientation: portrait) {
  #app { flex-direction: column; }
  #globe {
    flex: 0 0 50%;
    max-height: 50%;
    height: 50%;
  }
  #calendar {
    flex: 0 0 50%;
    max-height: 50%;
    height: 50%;
  }
}

/* The app splits LEFT/RIGHT (globe left, calendar right) whenever the screen is
   WIDER than it is tall (landscape). When it's TALLER than wide (portrait) the two
   halves stay STACKED (globe on top, calendar below). One simple rule, no gaps:
   orientation: landscape = wider-than-tall, portrait = taller-than-wide. */
@media (orientation: landscape) {
  #app { flex-direction: row; }
  #globe {
    flex: 1 1 50%;                   /* left half — can grow if calendar is fixed 50% */
    max-height: none;
    height: auto;
  }
  #calendar {
    flex: 0 0 50%;                   /* right half — fixed half of the screen width */
    max-height: none;
    height: auto;
    /* Breathe down from the top edge so the day-view back button and month
       title never sit flush against the seam (where the globe used to clip them).
       Also clear the iPhone notch on the RIGHT edge when the phone is rotated
       that way — calendar is always the right half, so only the right inset
       matters (the left notch sits over the globe, not over this panel). */
    padding-top: 20px;
    padding-right: max(16px, env(safe-area-inset-right, 0px));
  }
}

/* ============================================================
   THE GLASS RECIPE — every box in the app, in one place.
   ============================================================ */
#globe-account-btn, .globe-trip-bubble, #back-to-month, #detail-back, .day-nav-bubble, .item,
#add-card, .sheet-card, #item-menu-card button, #trip-menu-card button, #chat-menu-card button, #cost-row-menu-card button,
#cost-type-menu-card,
#add-cancel, .btn-cancel, .airport-list, .nav-btn, .contact-btn, .copy-bubble, .res-tile,
.tr-hotel-suggest, .res-accordion, .res-traveler-flat, #detail-header-action {
  background: var(--glass-bg);
  -webkit-backdrop-filter: var(--glass-blur);   /* Safari spells it with -webkit- */
  backdrop-filter: var(--glass-blur);
  border: 1px solid var(--glass-border);
  box-shadow: var(--glass-shadow);
}

/* ============================================================
   HOVER — a mouse gets a preview of "this is pressable" before it
   commits to a click; touch never sees this (every :active press
   state above already covers touch, and pointer:fine keeps :hover
   from getting stuck after a tap on a hybrid device). One rule per
   material family, same lighten-on-hover language everywhere so a
   button in the calendar and a button in the menu read as the same
   kind of thing, not two different apps stitched together.
   ============================================================ */
@media (pointer: fine) {
  /* Floating glass (the recipe above) — the deep shadow reads as "closer" on hover. */
  #globe-account-btn:hover, .globe-trip-bubble:hover,
  #back-to-month:hover, #detail-back:hover, .day-nav-bubble:hover:not(:disabled),
  .item:hover, #item-menu-card button:hover, #trip-menu-card button:hover, #chat-menu-card button:hover, #cost-row-menu-card button:hover,
  #add-cancel:hover, .btn-cancel:hover, .nav-btn:hover, .contact-btn:hover,
  .copy-bubble:hover, .res-tile-copy:hover, .res-tile-link:hover,
  .tr-hotel-suggest:hover, .airport-option:hover, .res-acc-head:hover,
  #google-signin:hover, .globe-menu-row:hover, .globe-menu-trip:hover,
  #ai-menu-mic:hover, #detail-header-action:hover {
    background: var(--hover);
    box-shadow: 0 14px 34px rgba(0, 0, 0, 0.55);
  }
  /* Flat rows nested in an already-glass card — same lighten, no shadow escalation
     (they were never meant to look like they're floating above their own card). */
  .sidetrip-add-btn:hover, .fill-ideas-btn:hover,
  .ai-icon-btn:hover, .ai-history-item:hover {
    background: rgba(var(--fg-rgb), 0.12);
  }
  /* The one solid, high-contrast primary action (Add / Save / Log in) — a hair
     dimmer on hover, the same quiet tell every white pill on the web uses. */
  #add-save:hover, .btn-save:hover {
    background: rgba(var(--fg-rgb), 0.88);
  }
}

/* The edit box's Cancel/Save reuse the app-wide glass-cancel + solid-save
   pair (see "THE GLASS RECIPE" above and the pop-up buttons below), just
   sized down to sit inline in a chat bubble instead of a full-height pop-up. */
/* Shared motion curves — views, cards, list items */
@keyframes view-in {
  from { opacity: 0; transform: scale(0.97) translateY(6px); }
  to   { opacity: 1; transform: none; }
}
@keyframes item-in {
  from { opacity: 0; transform: scale(0.96) translateY(5px); }
  to   { opacity: 1; transform: none; }
}
@keyframes pop-in {
  from { opacity: 0; transform: scale(0.97) translateY(8px); }
  to   { opacity: 1; transform: scale(1)    translateY(0); }
}
@keyframes overlay-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* Calendar panels ease in whenever they become visible */
#month-view:not([hidden]),
#day-view:not([hidden]),
#detail-view:not([hidden]) {
  animation: view-in var(--dur-view) var(--ease-out) both;
}

/* Pop-ups: dimmed backdrop + card settle */
#add-overlay:not([hidden]),
.sheet-overlay:not([hidden]),
#item-menu:not([hidden]),
#trip-menu:not([hidden]),
#chat-menu:not([hidden]),
#cost-row-menu:not([hidden]),
#cost-type-menu:not([hidden]) {
  animation: overlay-in 0.2s ease both;
}
#add-card, .sheet-card, #cost-type-menu-card {
  animation: pop-in 0.26s var(--ease-out) both;
}
#item-menu-card button, #trip-menu-card button, #chat-menu-card button, #cost-row-menu-card button,
#cost-type-menu-card .cost-type-option {
  animation: pop-in 0.24s var(--ease-out) both;
}
#item-menu-card button:nth-child(1), #trip-menu-card button:nth-child(1), #chat-menu-card button:nth-child(1), #cost-row-menu-card button:nth-child(1) { animation-delay: 0.02s; }
#item-menu-card button:nth-child(2), #trip-menu-card button:nth-child(2), #chat-menu-card button:nth-child(2), #cost-row-menu-card button:nth-child(2) { animation-delay: 0.05s; }
#item-menu-card button:nth-child(3), #trip-menu-card button:nth-child(3), #chat-menu-card button:nth-child(3) { animation-delay: 0.08s; }
#chat-menu-card button:nth-child(4) { animation-delay: 0.11s; }
#cost-type-menu-card .cost-type-option:nth-child(1) { animation-delay: 0.02s; }
#cost-type-menu-card .cost-type-option:nth-child(2) { animation-delay: 0.04s; }
#cost-type-menu-card .cost-type-option:nth-child(3) { animation-delay: 0.06s; }
#cost-type-menu-card .cost-type-option:nth-child(4) { animation-delay: 0.08s; }
#cost-type-menu-card .cost-type-option:nth-child(5) { animation-delay: 0.10s; }
#cost-type-menu-card .cost-type-option:nth-child(6) { animation-delay: 0.12s; }

/* Anyone who asked their phone for less motion gets stillness, full stop */
@media (prefers-reduced-motion: reduce) {
  #month-view:not([hidden]), #day-view:not([hidden]), #detail-view:not([hidden]),
  #add-overlay:not([hidden]), .sheet-overlay:not([hidden]), #item-menu:not([hidden]), #trip-menu:not([hidden]), #chat-menu:not([hidden]), #cost-row-menu:not([hidden]), #cost-type-menu:not([hidden]),
  #add-card, .sheet-card, #cost-type-menu-card, #item-menu-card button, #trip-menu-card button, #chat-menu-card button, #cost-row-menu-card button, #cost-type-menu-card .cost-type-option,
  .day-flow .item, #detail-view:not([hidden]) .res-section-panel,
  #detail-view:not([hidden]) .detail-nav { animation: none; }
  #day-stage, .day-stage-ghost { transition: none !important; }
  .res-acc-body { transition: none !important; }
  .res-acc-chev { transition: none !important; }
}

/* ---------- TOP HALF: the real globe (MapLibre map) ---------- */
/* #globe overflow/z-index live with the shared #globe,#calendar rules above
   so the map can never paint over the calendar half (back button crop bug). */
#map {
  position: absolute;
  inset: 0;                        /* fill the whole top half */
  width: 100%;
  height: 100%;
  max-width: 100%;
  max-height: 100%;
  background: transparent;         /* let the starry background show around the globe */
  overflow: hidden;
}
/* MapLibre injects a canvas sized to the container — keep it clipped to #map. */
#map .maplibregl-canvas-container,
#map .maplibregl-canvas {
  max-width: 100% !important;
  max-height: 100% !important;
}
/* MapLibre can inject its own attribution — keep it off the globe */
.maplibregl-ctrl-attrib,
.maplibregl-ctrl-logo {
  display: none !important;
}
/* A hovered/selected pin's OWN marker wrapper (MapLibre's positioned
   `.maplibregl-marker` div, the sibling that sits right after the map's
   canvas — see #map .maplibregl-canvas-container above) needs its own lift
   above the route line, not just above the canvas. A transfer's pin sits
   directly ON its own A→B line (placeRidesOnRoute centres it there on
   purpose), and the teardrop morph translates the ring UP off that spot on
   select — the tip that's supposed to end up marking the spot, and (for a
   confirmed transfer, whose ring is its only outline, see .day-loc-icon::before
   below) the ring's outermost edge, both cross back over the line's own
   path as they rise. The canvas itself never wins that fight (DOM order
   already keeps every marker above it), but two elements at the SAME
   z-index paint in DOM order — and only .sel (a JS-driven click/hover-card
   selection) was ever given an explicit boost; a bare CSS :hover directly
   on the pin was not, so it could still lose to whichever sibling marker
   the day happened to draw right after it. This covers both triggers, on
   the marker itself rather than deep inside its pseudo-elements, so it's
   one rule for "hovered, however that hover was triggered." */
.maplibregl-marker:has(.day-loc-icon:hover) {
  z-index: 5;
}

/* A reservation's spot within the city. DEFAULT is a round black BUBBLE with a white
   ring, holding that entry's own monochrome glyph. SELECTED (you hover its card in the
   day view, or the pin itself) morphs the circle into a teardrop PINPOINT, pops the
   name underneath and lifts it above its neighbours.
   The shape is drawn by ::before rather than on the element, so the pin body can spin
   45° into a teardrop while the glyph on top of it stays upright. */
.day-loc-icon {
  display: block;
  position: relative;
  width: 32px;
  height: 32px;
  box-sizing: border-box;
  color: var(--fg);
  opacity: 0;
  transform: scale(0.3);
  /* The marker is anchored by its CENTRE, so the middle of this box is the exact spot
     on the map. A circle is happy there. A teardrop is not: its tip is what marks the
     place, so on selection the whole shape lifts by --pin-tip-lift and the tip comes to
     rest on the centre — i.e. on the spot, and on the route line running through it.
     Rotating a 32px square 45° puts its sharp corner 16 × √2 ≈ 22.63px below the middle,
     which is precisely how far it has to rise. Scaling then pivots about the tip too. */
  --pin-tip-lift: 22.63px;
  transform-origin: 50% 50%;
  transition: opacity 0.26s var(--ease-out), transform 0.26s var(--ease-out);
  cursor: pointer;
  pointer-events: auto;
}
/* The bubble itself. Rounding one corner and rotating -45° turns the circle into a
   teardrop whose point sits straight down — both are transitions of the SAME shape,
   which is what makes the change read as a morph rather than a swap. */
.day-loc-icon::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  width: 32px;
  height: 32px;
  box-sizing: border-box;
  background: #000000;
  border: 3px solid #ffffff;
  border-radius: 50%;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
  transition: border-radius 0.26s var(--ease-out), transform 0.26s var(--ease-out),
              background-color 0.26s var(--ease-out), border-color 0.26s var(--ease-out);
}
.day-loc-icon.in { opacity: 1; transform: scale(1); }
.day-loc-icon.sel::before,
.day-loc-icon:hover::before {
  border-radius: 50% 50% 50% 0;
  transform: translateY(calc(-1 * var(--pin-tip-lift))) rotate(-45deg);
}
/* The glyph rides up with the shape — otherwise the pin lifts off and leaves its own
   icon behind on the spot. It only ever TRANSLATES: the bubble turns 45° under it, and
   an icon turned with it would sit on its ear. */
.day-loc-icon.sel svg,
.day-loc-icon:hover svg {
  transform: translateY(calc(-1 * var(--pin-tip-lift)));
}
/* A TENTATIVE pin's dashed ring has to become a teardrop too.
   It used to take only the lift above, so the bubble morphed into a teardrop and the
   ring rose with it still a circle — a pointed pin sitting inside a round outline, with
   the point pushing out through it. The ring shares the bubble's centre (inset is
   symmetric, so both pseudo-elements pivot about the same point), so handing it the
   very same rotation keeps the two locked together through the whole morph.

   WHY THE CORNER IS 4px AND NOT 0 LIKE THE BUBBLE'S. The ring sits 4px outside a 32px
   bubble, so its own box is 40px. Square its corner off like the bubble's and that
   corner lands 20√2 ≈ 28.28px from the centre while the bubble's is at 16√2 ≈ 22.63px
   — a 5.66px gap at the tip against 4px everywhere else, which reads as an outline that
   doesn't fit its pin, stretched and drifting off the point. Rounding the corner pulls
   it back in: for radius r the corner reaches (20 − r)√2 + r, and 4px puts it at
   26.63px — exactly 4px beyond the bubble's tip, the same gap the ring holds all the
   way round. */
.day-loc-icon.sel::after,
.day-loc-icon:hover::after {
  border-radius: 50% 50% 50% 4px;
  transform: translateY(calc(-1 * var(--pin-tip-lift))) rotate(-45deg);
}
/* A quick overshoot on a springy curve for the select bounce. Our marker already exists
   (unlike a remounted-on-select marker, which would need to start at opacity 0), so it
   stays visible throughout — same motion, no flicker. */
.day-loc-icon.sel {
  animation: pin-bounce-select 0.36s cubic-bezier(0.34, 1.56, 0.64, 1) both;
}
@keyframes pin-bounce-select {
  0% { transform: scale(0.6) translateY(6px); }
  60% { transform: scale(1.15) translateY(-4px); }
  100% { transform: scale(1) translateY(0); }
}
/* The name pill, tucked just under the pin's TIP — measured from the centre (the spot),
   not the bottom of the box, because the shape has lifted off it. */
.day-pin-label-wrap {
  position: absolute;
  top: calc(50% + 7px);
  left: 50%;
  transform: translateX(-50%);
  opacity: 0;
  z-index: 1;
  pointer-events: none;
  transition: opacity 0.2s ease-in-out;
}
.day-loc-icon.sel .day-pin-label-wrap,
.day-loc-icon:hover .day-pin-label-wrap { opacity: 1; }
.day-pin-label {
  display: block;
  max-width: 170px;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  background: #ffffff;
  color: #000000;
  font-size: 12.5px;
  line-height: 18px;
  font-weight: 500;
  letter-spacing: 0.05px;
  text-align: center;
  border-radius: 9999px;
  padding: 2px 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.35);
}
/* An APPROXIMATE pin: the venue itself isn't on any map we can reach, so this
   sits at the city's centre instead (see repinLostSpots). Faded, so it reads at
   a glance as "somewhere in this city" rather than as a located address — the
   event still appears on the day, which is the point, without the map making a
   claim it can't back. Needs .in in the selector to outrank the reveal rule
   that sets opacity to 1. */
.day-loc-icon-approx.in { opacity: 0.55; }

/* An idea's pin: same bubble, plus a bright dashed ring (matches trip-flag idea).
   Lives on ::after because ::before is the bubble. */
.day-loc-icon-idea::after {
  content: '';
  position: absolute;
  inset: -4px;
  border-radius: 50%;
  border: 1.5px dashed rgba(0, 0, 0, 0.92);
  pointer-events: none;
  /* border-radius rides the same curve and duration as the bubble's own morph
     (see .day-loc-icon::before), so the outline and the shape inside it round
     off together instead of the ring snapping to its teardrop a beat early. */
  transition: transform 0.26s var(--ease-out), border-radius 0.26s var(--ease-out);
}
.day-loc-icon svg {
  display: block;
  position: absolute;
  top: 7px;
  left: 7px;
  width: 18px;
  height: 18px;
  z-index: 1;
  transition: color 0.26s var(--ease-out), filter 0.26s var(--ease-out),
              transform 0.26s var(--ease-out);
}
@media (prefers-reduced-motion: reduce) {
  .day-loc-icon { transition: none; }
  .day-loc-icon::before { transition: none; }
  .day-loc-icon svg { transition: none; }
  .day-loc-icon-idea::after { transition: none; }
  .day-loc-icon.sel { animation: none; }
  .day-pin-label-wrap { transition: none; }
}
/* YOUR OWN LOCATION — "Find me on the globe" (account menu). Two pieces:
   the real GPS-accuracy circle is its own map layer in true geographic
   space (grows/shrinks with zoom — see map.on('load')'s my-location-accuracy
   source and updateMyLocationAccuracy), drawn UNDER this fixed-size puck
   marking "you, precisely here" — the same split Google Maps' blue dot uses.
   The puck's own material is deliberately the SAME one .day-loc-icon uses
   for a reservation (black disc, crisp white ring, soft drop shadow) — this
   IS a pin on this globe, just for a place that isn't a booking, so it earns
   its place by looking like it belongs to the same family, not a mismatched
   sticker on top. The halo behind it is a slow, soft breathing pulse — the
   quieter idle language Apple's own blue dot uses, in place of a harder
   Google-style radar ping, matching this app's motion elsewhere. */
.my-location-pin {
  display: block;
  position: relative;
  width: 22px;
  height: 22px;
  pointer-events: none;
  opacity: 0;
  transform: scale(0.4);
  transition: opacity 0.3s var(--ease-out), transform 0.3s var(--ease-out);
}
.my-location-pin.in { opacity: 1; transform: scale(1); }
.my-location-halo {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(var(--fg-rgb), 0.4) 0%, rgba(var(--fg-rgb), 0) 72%);
  animation: my-location-breathe 2.6s ease-in-out infinite;
}
@keyframes my-location-breathe {
  0%, 100% { transform: scale(0.85); opacity: 0.6; }
  50%      { transform: scale(1.7); opacity: 0.15; }
}
.my-location-dot {
  position: absolute;
  inset: 6px;
  border-radius: 50%;
  background: #000000;
  border: 2.5px solid #ffffff;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.5), 0 0 7px 1px rgba(var(--fg-rgb), 0.45);
}
@media (prefers-reduced-motion: reduce) {
  .my-location-pin { transition: none; }
  .my-location-halo { animation: none; opacity: 0.3; }
}
/* Menu button — top-right of the globe */
/* The menu bubble and the trip flag bubbles share size and shape (a circle).
   ONE knob — --globe-btn — sizes both, plus the icon and the flag-stack spacing,
   so they always match. Following Apple HIG / Material, icon buttons are FIXED
   comfortable sizes that STEP UP at breakpoints (not fluid % — that bloats on big
   screens). 44px everywhere — Apple HIG's minimum comfortable tap target; the old
   37px phone size was below spec and hard to hit one-handed. */
:root { --globe-btn: 44px; }
#globe-account-btn, .globe-trip-bubble {
  width: var(--globe-btn);
  height: var(--globe-btn);
  border-radius: var(--r-btn);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--fg);
  transition: transform var(--dur-tap) var(--ease-snap), background 0.15s ease, opacity 0.2s ease;
}
/* Phone account avatar — same spot/size the old hamburger used. Circular pic. */
#globe-account-btn {
  position: absolute;
  top: calc(env(safe-area-inset-top, 0px) + 16px);
  right: 16px;
  z-index: 6;
  padding: 0;
  overflow: hidden;
  border-radius: 50%;
}
#globe-account-btn:active, .globe-trip-bubble:active { transform: scale(0.93); background: var(--press); }
#globe-account-btn[aria-expanded="true"] { opacity: 0.92; }
.globe-account-pic {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  border-radius: 50%;
  background: rgba(var(--fg-rgb), 0.12);
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  font-size: 14px;
  font-weight: 600;
  color: var(--fg);
  line-height: var(--globe-btn);
  text-align: center;
}
.sidebar-profile-pic svg,
.globe-account-pic svg {
  width: 18px;
  height: 18px;
  opacity: 0.7;
}

/* "Reading your email" badge — sits in the button's own corner so it travels
   with it (portrait: top-right; landscape/desktop: top-left — see below),
   instead of a second fixed screen position to keep in sync. Big enough to
   actually notice, wearing the whale logo (clipped to a circle, same trick
   as the trip flag bubbles) with a soft glowing halo that pulses while an
   email is being read — then holds still on a checkmark once it's done. */
#email-processing-badge {
  position: absolute;
  top: -6px;
  right: -6px;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.6);
  animation: email-badge-glow 1.6s ease-in-out infinite;
}
#email-processing-badge[hidden] { display: none; }
.email-badge-logo {
  width: 100%;
  height: 100%;
  border-radius: 50%;
  object-fit: cover;
  display: block;
}
/* The checkmark sits over the logo, hidden until the pass finishes. Its own
   flex-centering (rather than the parent's) keeps the pop-in scale centered. */
.email-badge-check {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--fg);
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.8));
  opacity: 0;
  transform: scale(0.6);
  transition: opacity 0.2s ease, transform 0.2s ease;
}
#email-processing-badge.done {
  animation: none;
  box-shadow: 0 0 0 4px rgba(var(--fg-rgb), 0.28), 0 2px 10px rgba(0, 0, 0, 0.6);
}
#email-processing-badge.done .email-badge-logo { opacity: 0.35; }
#email-processing-badge.done .email-badge-check { opacity: 1; transform: scale(1); }
@keyframes email-badge-glow {
  0%, 100% { box-shadow: 0 0 0 0 rgba(var(--fg-rgb), 0.5), 0 2px 10px rgba(0, 0, 0, 0.6); }
  50%      { box-shadow: 0 0 0 6px rgba(var(--fg-rgb), 0), 0 2px 10px rgba(0, 0, 0, 0.6); }
}
@media (prefers-reduced-motion: reduce) {
  #email-processing-badge { animation: none; }
  .email-badge-check { transition: none; }
}

/* Globe-quadrant "reading a forwarded email" indicator — just the small plane
   itself, silently circling in place above the real globe, while that email
   gets read. Lives OUTSIDE the menu so it's visible whether or not the menu
   is open. Pinned to the TOP CENTER of the globe quadrant in every layout —
   deliberately its own fixed spot rather than either corner (where the menu
   button and the whale logo already live) so it reads as a distinct,
   unmissable "something just happened" signal instead of competing for space
   with either of them.

   Used to also carry a small wireframe-globe glyph behind the plane, wrapped
   in a circular glass "chip" (border + background + blur) — with the real
   globe rendered right below it, that chip read as a second, off-center
   button floating over the globe rather than as part of one animation. Both
   are gone now: just the plane, no button-shaped chrome around it. */
.globe-processing-icon {
  position: absolute;
  z-index: 6;
  width: var(--globe-btn);
  height: var(--globe-btn);
  top: calc(env(safe-area-inset-top, 0px) + 16px);
  left: 50%;
  right: auto;
  transform: translateX(-50%);
}
.globe-processing-icon[hidden] { display: none; }
.gpi-orbit { position: absolute; inset: 0; animation: orbit 1.4s linear infinite; }
.gpi-plane {
  position: absolute;
  top: -1px;
  left: 50%;
  width: 11px;
  height: 11px;
  margin-left: -5.5px;
  transform: rotate(90deg);
  color: var(--fg);
}
@media (prefers-reduced-motion: reduce) {
  .gpi-orbit { animation: none; }
}
/* Desktop — match the menu button's own 24px top inset for the same layout
   rhythm; stays horizontally centered (see the base rule's transform). */
@media (orientation: landscape) and (min-width: 1024px) and (pointer: fine) {
  .globe-processing-icon {
    top: calc(env(safe-area-inset-top, 0px) + 24px);
  }
}
/* The menu's own in-menu spinner already shows this same state while open —
   no need for two at once. */
#globe.menu-open .globe-processing-icon { display: none; }

#email-processing {
  position: absolute;
  z-index: 8;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  pointer-events: none;
}
#email-processing[hidden] { display: none !important; }
#email-processing-card {
  display: flex;
  align-items: center;
  gap: 14px;
  max-width: min(320px, calc(100vw - 40px));
  padding: 16px 18px;
  border-radius: var(--r-card);
  background: var(--glass-bg);
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
  border: 1px solid var(--glass-border);
  box-shadow: var(--glass-shadow);
  pointer-events: none;
}
.email-processing-plane {
  position: relative;
  width: 36px;
  height: 36px;
  flex-shrink: 0;
}
.email-processing-plane .gpi-plane {
  width: 14px;
  height: 14px;
  margin-left: -7px;
}
.email-processing-copy { min-width: 0; text-align: left; }
#email-processing-title {
  margin: 0 0 4px;
  font-size: 15px;
  font-weight: 600;
  letter-spacing: -0.02em;
  color: var(--fg2);
  line-height: 1.25;
}
#email-processing-sub {
  margin: 0;
  font-size: 13px;
  font-weight: 500;
  color: rgba(var(--fg-rgb), 0.55);
  line-height: 1.35;
}
@media (prefers-reduced-motion: reduce) {
  .email-processing-plane .gpi-orbit { animation: none; }
}

/* "Grabbing the information from your Gmail…" — the one moment the app has
   nothing to show yet, right after a first sign-in. Sits exactly where the
   processing icon sits (same inset, same glass), just as a readable sentence
   instead of a symbol: a brand-new empty app plus a spinner reads as broken,
   a sentence reads as working. Monochrome and glass, like every other non-map
   surface in the app. */
#gmail-grab-message {
  position: absolute;
  z-index: 6;
  top: calc(env(safe-area-inset-top, 0px) + 16px);
  left: 50%;
  transform: translateX(-50%);
  max-width: min(88%, 340px);
  padding: 10px 16px;
  border-radius: var(--r-chip);
  text-align: center;
  font-size: 13px;
  line-height: 1.35;
  color: rgba(var(--fg-rgb), 0.92);
  background: rgba(6, 6, 10, 0.55);
  border: 1px solid rgba(var(--fg-rgb), 0.16);
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.5);
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
  animation: gmail-grab-breathe 2.4s ease-in-out infinite;
}
#gmail-grab-message[hidden] { display: none; }
/* A slow, barely-there pulse — enough to say "still working", never enough to
   distract from the globe. */
@keyframes gmail-grab-breathe {
  0%, 100% { opacity: 0.72; }
  50%      { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  #gmail-grab-message { animation: none; opacity: 1; }
}
@media (orientation: landscape) and (min-width: 1024px) and (pointer: fine) {
  #gmail-grab-message { top: calc(env(safe-area-inset-top, 0px) + 24px); }
}
/* Same rule as the icon above: don't stack it on top of an open menu. */
#globe.menu-open #gmail-grab-message { display: none; }

/* The trip flag bubbles stack vertically right under the menu bubble
   (12px top gap + the button height + 10px gap), so they follow the button size. */
#globe-trips-bubbles {
  position: absolute;
  top: calc(env(safe-area-inset-top, 0px) + 16px + var(--globe-btn) + 10px);
  right: 16px;
  z-index: 6;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 10px;
}
/* In day view we hide the flag strips (the day view is one city, not the trip list). */
#globe-trips-bubbles[hidden] { display: none; }

/* The trip flags row. The trip budget used to dock at this row's right edge
   as a dollar bubble; money lives on the phone bar's Money tab now, so flags
   are the only thing in here. Hidden by JS (aiMenuTopRow.hidden) when the
   flags have nothing to show, so an untied "New trip" chat with no trips yet
   doesn't leave an empty gap. */
#ai-menu-top-row {
  display: flex;
  align-items: center;
  gap: 10px;
  /* Bottom padding is deliberately generous, not just decorative: a selected
     flag scales up (see .globe-trip-bubble-selected, up to 1.16x) and an
     idea-only flag's dashed ring extends further still — together the tallest
     case grows a good ~7-8px past the plain 44px circle this row otherwise
     only reserves. overflow-x: auto (on #ai-menu-flags below) forces
     overflow-y to auto too (a CSS overflow-module rule, not a choice here),
     so anything not accounted for in padding gets clipped instead of just
     overlapping the title below — that clipped bottom edge was exactly the
     bug. 16px clears every case with room to spare, and doubles as clean
     breathing room between the flags and the title in the chat panel's
     enlarged/landscape layout. Top padding matches so the row reads as
     centered/balanced. */
  /* The top value is a variable so the phone's landing screen can line the
     history clock up with this row without hard-coding the same number twice
     (see --flag-row-pad-top in the phone-bar block). */
  padding: var(--flag-row-pad-top, 14px) 16px 16px;
  flex: 0 0 auto;
}
#ai-menu-top-row[hidden] { display: none; }
/* Same flags, reused as a horizontal row above the AI chat's title (see
   renderAiMenuFlags) — one tap opens that trip's own reservation chat instead
   of the globe jump the floating stack does. Row scrolls sideways rather than
   wrapping so it stays one clean line even with several trips.
   overflow-x: auto clips at THIS element's own padding edge (setting
   overflow-x also forces overflow-y: auto, a CSS overflow-module rule) — a
   selected flag's scale(1.16) paints past the plain circle, so without
   padding here that growth gets clipped on whichever edges are flush with
   this box, worst on the first flag (left/top/bottom all flush at once) —
   that's the dashed/cropped-looking edge you get right after tapping the
   leftmost flag. 10px padding gives every edge room to breathe; the
   matching -10px margin cancels it back out so the row's own size/position
   is unchanged. */
#ai-menu-flags {
  display: flex;
  gap: 10px;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  flex: 1 1 auto;
  min-width: 0;
  padding: 10px;
  margin: -10px;
}
#ai-menu-flags[hidden] { display: none; }
/* The reservation chat's "start something new" control, sitting at the right
   end of the bubbles' own line (#ai-menu-flags is flex: 1 1 auto, so it takes
   the slack and this lands hard against the right edge on its own).
   It shows in exactly one state — inside a reservation chat — because that is
   the one place the header's own new-chat/history pair is hidden. On the
   landing screen the history clock already owns this corner and you are
   already in a new chat; in an untied chat the header pair is still there. */
#ai-menu-new-chat-top,
#ai-menu-history-top { display: none; }
#ai-menu.ai-trip-chat #ai-menu-new-chat-top { display: flex; }
#ai-history-btn[aria-pressed="true"],
#ai-menu-history-top[aria-pressed="true"] { color: var(--fg); }
#ai-menu-flags .globe-trip-bubble { flex: 0 0 auto; }
/* The destination flag fills the whole circle. --flag-size bumps the
   regular (confirmed) size a little past the plain --globe-btn (the one
   knob everything else still uses). Confirmed flags keep the plain solid
   glass edge (the shared GLASS RECIPE rule above already gives every
   .globe-trip-bubble a 1px solid border) — only .globe-trip-bubble-idea
   below swaps that for a dashed ring, so the outline marks "tentative"
   and nothing else. */
.globe-trip-bubble {
  --flag-size: calc(var(--globe-btn) * 1.08);
  width: var(--flag-size);
  height: var(--flag-size);
  overflow: hidden;
  position: relative;
  border-radius: 50%;   /* flags stay circular — they're a country mark, not chrome */
}
.globe-trip-bubble img {
  width: 100%;
  height: 100%;
  border-radius: 50%;
  object-fit: cover;
  display: block;
  /* Unselected flags are desaturated so the selected one (full color, see
     .globe-trip-bubble-selected below) pops out as "the one you're on" at a
     glance instead of every flag competing at equal color. */
  filter: grayscale(0.85) brightness(0.8);
  transition: filter var(--dur-tap) var(--ease-snap);
}
/* Idea-only trip flag (paste-a-link / unconfirmed): the ONLY flag state with
   an outline — a dashed ring instead of the confirmed flag's plain solid
   glass edge, so "tentative" is the one thing that gets a border here.
   border-color is transparent (hides the plain 1px glass border every other
   glass box gets) and the dashed ring is drawn by ::after right at the
   bubble's own edge. Also a touch bigger than a confirmed flag (relative to
   the already-bumped --flag-size) so it reads as distinct at a glance too.
   The flag image is padded down to fit inside the ring instead of touching
   it. */
.globe-trip-bubble-idea {
  --idea-size: calc(var(--flag-size) * 1.1);
  width: var(--idea-size);
  height: var(--idea-size);
  border-color: transparent;
  background: rgba(8, 8, 12, 0.35);
  padding: calc(var(--idea-size) * 0.11);
}
.globe-trip-bubble-idea::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 50%;
  border: 1.75px dashed rgba(var(--fg-rgb), 0.92);
  pointer-events: none;
}
/* The flag for whichever trip is currently framed on the globe/calendar —
   set by updateSelectedTripFlags, in step with revealedLayoverTripId — grows
   slightly so it's obvious at a glance which trip you're working on, in both
   the globe's own flag stack and its copy in the AI chat panel. Reuses the
   same transform transition .globe-trip-bubble already has (see --dur-tap/
   --ease-snap above), so it eases in whether you tapped the flag or the
   calendar scrolled to it. z-index lifts it above snug neighbors so the
   larger circle never looks clipped by the one next to it. */
.globe-trip-bubble-selected {
  transform: scale(1.22);
  z-index: 1;
}
.globe-trip-bubble-selected img {
  filter: none;
}
.globe-trip-bubble-selected:active { transform: scale(1.14); }
/* Mouse hover: a smaller, PURELY TEMPORARY grow — reverts the instant the
   pointer leaves, never sticks. Skipped once a flag is already the locked
   .globe-trip-bubble-selected size (that one only shrinks a touch on
   press, see above) and gated on real hover capability so a tap on
   touchscreens can't leave a flag stuck "hovered." The permanent, bigger
   grow only ever happens via a click/selection (.globe-trip-bubble-selected,
   set by updateSelectedTripFlags) — hover never locks anything by itself. */
@media (hover: hover) {
  .globe-trip-bubble:not(.globe-trip-bubble-selected):hover {
    transform: scale(1.08);
  }
  .globe-trip-bubble:not(.globe-trip-bubble-selected):hover img {
    filter: none;
  }
}
/* Layover sub-menu bubble — collapsed (tucked under the primary) at 75% size so
   it doesn't look like another trip. When THIS trip's month is open (.is-shown,
   via revealLayoverBubblesFor) it grows to the SAME size as the other flags and
   sits in the stack as a peer. Scroll away / pick another trip (retractAllLayover-
   Bubbles) shrinks it back to the smaller tucked size. */
.globe-trip-bubble-layover {
  width: calc(var(--globe-btn) * 0.75);
  height: calc(var(--globe-btn) * 0.75);
  margin-top: calc(-1 * (var(--globe-btn) * 0.75 + 10px));
  opacity: 0;
  transform: translateY(-40%);
  pointer-events: none;
  transition:
    margin-top 0.34s var(--ease-out),
    transform 0.34s var(--ease-out),
    opacity 0.26s ease,
    width 0.34s var(--ease-out),
    height 0.34s var(--ease-out);
}
.globe-trip-bubble-layover.is-shown {
  /* Match primary trip flags (same as .globe-trip-bubble / --flag-size). */
  width: var(--flag-size);
  height: var(--flag-size);
  margin-top: 0;
  opacity: 1;
  transform: translateY(0);
  pointer-events: auto;
}
@media (prefers-reduced-motion: reduce) {
  .globe-trip-bubble-layover { transition: none; }
}

/* When the AI menu is open, tuck the account avatar and flag bubbles — they
   sit where the overlay card lives on phone/tablet. */
#globe.ai-open #globe-account-btn,
#globe.ai-open #globe-trips-bubbles {
  display: none;
}

/* Globe-only blur while AI menu is open — calendar below stays live */
#ai-menu-scrim {
  position: absolute;
  inset: 0;
  z-index: 4;
  background: rgba(0, 0, 0, 0.38);
  -webkit-backdrop-filter: blur(14px);
  backdrop-filter: blur(14px);
  pointer-events: auto;
}
#ai-menu-scrim[hidden] { display: none !important; }

/* AI travel-agent menu. Opened card FILLS the whole
   globe quadrant (see #ai-menu.open's inset below for the thin edge margin) —
   same on phone, tablet, and desktop. It does not grow past that with the
   conversation; long chats scroll inside #ai-menu-thread. */
#ai-menu {
  position: absolute;
  z-index: 5;
  display: flex;
  pointer-events: none;
  /* closed: no footprint (hidden attribute also forces display:none) */
  inset: auto;
}
#ai-menu[hidden] { display: none !important; }
/* Open: fill the globe half (with a small inset) and flex-center the card. */
#ai-menu.open {
  inset: 0;
  top: max(10px, env(safe-area-inset-top, 0px));
  right: max(10px, env(safe-area-inset-right, 0px));
  bottom: 10px;
  left: max(10px, env(safe-area-inset-left, 0px));
  display: flex;
  justify-content: center;
  align-items: center;
  width: auto;
  max-width: none;
  height: auto;
  transform: none;
}
#ai-menu.open .globe-menu-card {
  /* Fill the quadrant exactly (the inset above is the only margin) — width
     AND height both 100%, no cap, so it reads as one solid pane instead of a
     bubble floating with gaps around it. Still never grows past that with
     the conversation; flex: 0 0 auto keeps it from stretching further. */
  width: 100%;
  height: 100%;
  max-width: none;
  max-height: none;
  min-width: 0;
  min-height: 0;
  flex: 0 0 auto;
  margin: auto;
  transform-origin: center center;
  overflow: hidden;              /* thread/history scroll inside, not the card shell */
}

/* ONE knob — --menu-fs — sets the open menu's base text size; every child below
   sizes in em off it, so the whole menu scales together. Stepped at breakpoints
   (Apple HIG / Material: control text is fixed sizes per device, not fluid):
   iPhone 14px, iPad 16px (touch wants bigger), desktop 15px (mouse, tighter).
   BOTH width AND height must be roomy — a landscape iPhone Pro/Max is wider
   than 768px but still short, and the bigger type was one reason the menu
   could not fit the globe half. */
:root { --menu-fs: 14px; }
@media (min-width: 768px) and (min-height: 500px)  { :root { --menu-fs: 16px; } }
@media (min-width: 1024px) and (min-height: 500px) { :root { --menu-fs: 15px; } }
.globe-menu-card {
  pointer-events: auto;
  font-size: var(--menu-fs);
  width: 100%;
  max-width: 300px;
  max-height: calc(100% - 58px);
  overflow-y: auto;
  overflow-x: hidden;               /* vertical scroll only */
  padding: 10px;
  border-radius: var(--r-card);
  background: rgba(6, 6, 10, 0.88);
  -webkit-backdrop-filter: blur(36px) saturate(1.12);
  backdrop-filter: blur(36px) saturate(1.12);
  border: 1px solid rgba(var(--fg-rgb), 0.18);
  box-shadow: 0 20px 50px rgba(0, 0, 0, 0.72), inset 0 1px 0 rgba(var(--fg-rgb), 0.07);
  transform-origin: top right;
  animation: globe-menu-in 0.28s var(--ease-out) both;
  box-sizing: border-box;
}
@keyframes globe-menu-in {
  from { opacity: 0; transform: scale(0.2) translateY(-6px); }
  to   { opacity: 1; transform: scale(1) translateY(0); }
}

/* Landscape: account avatar + trip flag bubbles stay top-LEFT. */
@media (orientation: landscape) {
  #globe-account-btn { top: calc(env(safe-area-inset-top, 0px) + 16px); left: 16px; right: auto; }
  #globe-trips-bubbles {
    top: calc(env(safe-area-inset-top, 0px) + 16px + var(--globe-btn) + 10px);
    left: 16px; right: auto; align-items: flex-start;
  }
}

/* Cost panel + money fab are desktop-only (flag → fab → left sheet). */
#cost-panel, #cost-fab { display: none !important; }

/* "Open source emails" panel — same desktop-only story as the cost panel
   above (see openSourceEmailsPanel in app.js): it takes over the AI chat +
   cost columns, both of which only really exist as this desktop layout. */
#email-panel { display: none !important; }

/* HOW THAT PANEL IS BUILT, as opposed to WHERE IT SITS.

   Everything below is the panel's own construction - the header row, the
   round back/close/print buttons, the list of emails, the reader's frame.
   None of it is desktop-specific; it is the same panel wherever the panel
   appears, exactly as #photo-panel's internals a few lines down are. It
   used to live inside the desktop block purely because desktop was the only
   layout that ever showed this panel, and the iPad landscape rail found the
   cost of that the moment it gave the panel a box: the box appeared and the
   inside of it did not lay out - header buttons at zero width, the reader
   not filling its own pane.

   Out here instead, so a layout only has to say where the panel goes. It
   costs nothing on the layouts that do not show it: the rule directly above
   hides the panel outright, and every selector here is inside it.

   The header's 48px of top padding stays desktop's number - it is there to
   clear a window's chrome, and a layout that starts at the top of the screen
   overrides just that (see the rail block). */
#email-list-view, #email-reader-view {
  display: flex;
  flex-direction: column;
  min-height: 0;
  flex: 1 1 auto;
}
#email-panel-header, #email-reader-header {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 48px 28px 20px;
  border-bottom: 1px solid var(--glass-border);
}
#email-panel-close, #email-reader-back, #email-reader-print {
  flex-shrink: 0;
  width: 36px;
  height: 36px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: 999px;
  background: var(--glass-bg);
  color: var(--fg);
  cursor: pointer;
  transition: background 0.15s ease, transform var(--dur-tap) var(--ease-out);
}
#email-panel-close:hover, #email-reader-back:hover, #email-reader-print:hover { background: rgba(var(--fg-rgb), 0.14); }
#email-panel-close:active, #email-reader-back:active, #email-reader-print:active { background: var(--press); transform: scale(0.93); }
#email-panel-close svg, #email-reader-back svg, #email-reader-print svg { width: 18px; height: 18px; }
/* Print sits at the far right of the header — the subject grows to fill
   the space between the back arrow and it. */
#email-reader-print { margin-left: auto; }
#email-panel-title {
  margin: 0;
  font-size: 20px;
  font-weight: 700;
  letter-spacing: -0.01em;
}
#email-reader-subject {
  flex: 1 1 auto;
  font-size: 16px;
  font-weight: 600;
  min-width: 0;
}
#email-list {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding: 10px 20px 28px;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.email-list-empty {
  margin: 24px 8px;
  color: rgba(var(--fg-rgb), 0.5);
  font-size: 14px;
}
.email-list-row {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 14px 12px;
  border: 1px solid var(--glass-border);
  border-radius: var(--r-chip);
  background: var(--glass-bg);
  color: var(--fg);
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: background 0.15s ease, transform var(--dur-tap) var(--ease-out);
}
.email-list-row:hover { background: rgba(var(--fg-rgb), 0.08); }
.email-list-row:active { background: var(--press); transform: scale(0.99); }
/* Reuses .res-leg-sum/.res-leg-date/.res-leg-fn — the exact row shape the
   Journey list already uses — so a list of emails reads as the same design
   language as the rest of the reservation detail view. */
.email-list-row .res-leg-fn { font-size: 15px; }
#email-reader-body {
  flex: 1 1 auto;
  min-height: 0;
  padding: 20px 28px 28px;
  overflow-y: auto;
}
/* The real email, full-size — this pane IS the reading view, so the frame
   fills all the room it has instead of cropping. */
.email-reader-frame {
  display: block;
  width: 100%;
  height: 100%;
  min-height: 100%;
  border: 1px solid var(--glass-border);
  border-radius: var(--r-chip);
  background: #fff;
}
.email-reader-text {
  max-height: none;
  height: 100%;
}

/* A still-tentative idea's photo gallery (openIdeaDetail, auto-opened, no
   button) — same desktop-only story. */
#photo-panel { display: none !important; }

/* Everything INSIDE that gallery, at the top level on purpose. Which box the
   panel occupies is a per-layout question (desktop gives it the email panel's
   bounds, the iPad landscape rail gives it the middle column), but the header,
   the scroll body and the photo slots are the same gallery on every one of
   them — so they are written once here and each layout only says where the
   panel sits. */
#photo-panel-header {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 48px 28px 20px;
  border-bottom: 1px solid var(--glass-border);
}
#photo-panel-close {
  flex-shrink: 0;
  width: 36px;
  height: 36px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: 999px;
  background: var(--glass-bg);
  color: var(--fg);
  cursor: pointer;
  transition: background 0.15s ease, transform var(--dur-tap) var(--ease-out);
}
#photo-panel-close:hover { background: rgba(var(--fg-rgb), 0.14); }
#photo-panel-close:active { background: var(--press); transform: scale(0.93); }
#photo-panel-close svg { width: 18px; height: 18px; }
#photo-panel-title {
  margin: 0;
  font-size: 20px;
  font-weight: 700;
  letter-spacing: -0.01em;
}
/* Idea photos, opened full-size (openPhotoGalleryPanel) — one big image
   after another, uncropped so nothing about the place is cut off, just
   scroll down for the next one. */
#photo-panel-body {
  flex: 1 1 auto;
  min-height: 0;
  padding: 20px 28px 28px;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 16px;
}
/* Each photo's own slot: it reserves space and shimmers before the photo
   arrives, so the gallery opens looking like a gallery instead of an empty
   column.

   THE IMAGE'S SHAPE IS NEVER THE SLOT'S BUSINESS. .photo-panel-img below is
   width: 100% / height: auto and nothing here overrides that, so a photo is
   always drawn at its own aspect ratio — while it is downloading, once it is
   done, and even if the JS that adds .is-loaded never runs at all.

   The first version of this got that wrong: the slot was a hard
   aspect-ratio: 4/3 box and the image was stretched across it with
   height: 100% + object-fit: cover until .is-loaded swapped it back. But a
   browser paints a big photo PROGRESSIVELY, well before its load event
   fires — so for the entire download you watched the picture squashed into
   a 4:3 crop, and it only snapped to its real shape at the very end. The
   slower the gallery, the longer it looked broken.

   So the placeholder reserves height with min-height, which an image can
   freely grow past, instead of a ratio it has to be forced into. .is-loaded
   is now cosmetic only — it stops the shimmer and fades the photo in. If it
   never lands, the worst case is a shimmer that keeps running behind a
   correctly-shaped, fully visible photo. */
.photo-panel-shot {
  position: relative;
  width: 100%;
  /* #photo-panel-body is a flex COLUMN that scrolls, so every slot here is a
     flex item — and a flex item's default flex-shrink: 1 lets the column
     squeeze all of them to share its height instead of letting them stack up
     and scroll. Ten photos in a 764px column got crushed to 50px each, and
     because this box clips (overflow: hidden below), a perfectly
     correctly-sized image was sliced into a letterbox strip. The image was
     never distorted — its container was. flex: 0 0 auto is the fix: a slot is
     exactly as tall as its photo and the column scrolls, which is the whole
     point of a gallery. Do not let this shrink. */
  flex: 0 0 auto;
  min-height: 220px;
  border-radius: var(--r-chip);
  overflow: hidden;
  background: linear-gradient(
    100deg,
    var(--glass-bg) 30%,
    color-mix(in srgb, var(--fg2) 9%, var(--glass-bg)) 50%,
    var(--glass-bg) 70%
  );
  background-size: 220% 100%;
  animation: photo-shimmer 1.3s linear infinite;
}
.photo-panel-shot.is-loaded {
  min-height: 0;   /* a short photo must not be padded out once it's in — safe
                      only because flex-shrink is 0 above; min-height: 0 on a
                      shrinkable flex item is exactly what let the column
                      crush these. */
  background: none;
  animation: none;
}
/* Keyframes moved out to the top level (search "photo-shimmer") — the
   phone's inline .idea-photo-shot needs the same animation, and a
   @keyframes declared inside this desktop media query does not exist on a
   phone at all. */
/* width/height here are the whole guarantee against a distorted photo, so
   nothing above may set height, object-fit or position on this element. */
.photo-panel-img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: var(--r-chip);
  border: 1px solid var(--glass-border);
  opacity: 1;
  transition: opacity 0.32s var(--ease-out);
}
/* Only the fade is class-driven, and it defaults to VISIBLE — an image with
   no .is-loaded yet is merely transparent over its own shimmer, never
   mis-shaped, and a failure to add the class can't hide a loaded photo
   because openPhotoGalleryPanel also settles every slot on a timer. */
.photo-panel-shot:not(.is-loaded) .photo-panel-img { opacity: 0; }
@media (prefers-reduced-motion: reduce) {
  .photo-panel-shot { animation: none; }
  .photo-panel-img { transition: none; }
}

/* DESKTOP: AI pane left, globe/calendar stacked right. */
@media (orientation: landscape) and (min-width: 1024px) and (pointer: fine) {
  /* DESKTOP LAYOUT: the AI travel agent is the left 60% of the screen, the
     globe and calendar stack in the right 40% (globe on top, calendar below)
     so a full month of square day cells stays in view. #ai-menu is still
     nested in the HTML inside #globe — here it is position: fixed over that
     left 60% of the SCREEN. The chat text itself stays a 694px reading
     measure (centered in the pane) so it does not stretch with the column. */
  /* The "cost" column is always present in the track list (width 0 when
     closed) rather than being added/removed via a class — a browser can only
     smoothly interpolate grid-template-columns between two values with the
     SAME number of tracks. Toggling between a 3-track and a 4-track value
     (the old approach) fails that test, so the whole grid — sidebar, AI pane,
     globe/calendar — snapped to its new position instead of easing there
     alongside the cost panel's own slide-in. Keeping the track count constant
     and only changing --cost-col-w lets every column move as one animation.
     var(--ease-out) (not a plain "ease") to match the settle curve every
     other view/panel in the app already uses, and a touch longer (0.4s vs
     0.35s) so a layout-triggering property like this — which the browser
     genuinely recomputes every frame, unlike a transform/opacity fade —
     reads as deliberate motion rather than a rushed snap. The stutter this
     used to have wasn't really the curve, though: it was every AI chat
     message's button-row/period-label ResizeObserver recalculating on each
     of those frames too (see watchAiButtonRows' observeLaterResizes in
     app.js) — fixed there by debouncing that work to run once motion
     actually stops, not on every intermediate width. */
  #app {
    display: grid;
    grid-template-columns: var(--sidebar-w, 56px) var(--cost-col-w, 0px) minmax(0, 1fr) var(--right-col);
    grid-template-rows: 45% 55%;
    grid-template-areas: "sidebar cost ai globe" "sidebar cost ai calendar";
    --cost-panel-w: 360px;
    --cost-col-w: 0px;
    --right-col: 40%;
    transition: grid-template-columns 0.4s var(--ease-out);
  }
  /* Cost panel open: wider sheet (440px); extra width from the globe/calendar
     column (40%→34%), not the AI pane. */
  body.cost-open #app {
    --cost-panel-w: 440px;
    --cost-col-w: 440px;
    --right-col: 34%;
  }
  #app-sidebar { grid-area: sidebar; }
  #globe { grid-area: globe; flex: none; }
  #calendar { grid-area: calendar; flex: none; padding-top: 20px; }

  /* Globe-only blur behind the AI card no longer applies — the AI card
     doesn't sit over the globe anymore, it has its own space. */
  #ai-menu-scrim { display: none !important; }

  /* Desktop uses the sidebar avatar — hide the phone globe account button. */
  #globe-account-btn { display: none !important; }
  /* Trip flags move exclusively to the AI chat's own flag row (#ai-menu-flags)
     at this breakpoint — the globe quadrant's copy is redundant once the chat
     is always open beside it, so it's removed outright rather than merely
     tucked away while a menu is open. */
  #globe-trips-bubbles { display: none !important; }
  /* Collapsed layover = smaller nested flag; revealed = same size as the other
     trip flags at this breakpoint (--globe-btn, same as the menu button). */
  .globe-trip-bubble-layover {
    width: calc(var(--globe-btn) * 0.75);
    height: calc(var(--globe-btn) * 0.75);
    margin-top: calc(-1 * (var(--globe-btn) * 0.75 + 10px));
  }
  .globe-trip-bubble-layover.is-shown {
    width: var(--flag-size);
    height: var(--flag-size);
    margin-top: 0;
  }
  /* Desktop AI chat: sits between the sidebar and the globe/calendar.
     A 1px light line on the right edge is the split between AI and
     globe/calendar (same hairline as the rest of the glass outlines). */
  #ai-menu.open {
    position: fixed;
    inset: 0;
    top: 0; right: var(--right-col); bottom: 0; left: var(--sidebar-w, 56px);
    width: auto;
    height: 100%;
    border-right: 1px solid var(--glass-border);
    transition: left 0.4s var(--ease-out), right 0.4s var(--ease-out);
  }
  body.cost-open #ai-menu.open {
    left: calc(var(--sidebar-w, 56px) + var(--cost-panel-w));
    right: var(--right-col);
  }

  /* Left cost sheet — fixed in the new grid column; slides from behind the sidebar. */
  #cost-panel {
    display: flex !important;
    flex-direction: column;
    position: fixed;
    top: 0;
    bottom: 0;
    left: var(--sidebar-w, 56px);
    width: var(--cost-panel-w);
    z-index: 5;
    padding: 48px 0 32px;
    box-sizing: border-box;
    background: var(--bg);
    border-right: 1px solid var(--glass-border);
    color: var(--fg2);
    transform: translateX(-100%);
    transition: transform 0.4s var(--ease-out);
    pointer-events: none;
    overflow: hidden;
  }
  body.cost-open #cost-panel {
    transform: translateX(0);
    pointer-events: auto;
  }
  /* Padding lives on the list itself (not the panel) so its scrollbar rides
     the panel's right border flush, same as #ai-menu-thread's pattern. */
  #cost-panel-list {
    flex: 1 1 auto;
    min-height: 0;
    padding-left: 20px;
    padding-right: 20px;
  }
  #cost-panel-add { flex-shrink: 0; margin-top: 8px; margin-left: 20px; margin-right: 20px; }
  #cost-panel .cost-total-row {
    flex-shrink: 0;
    flex-direction: column;
    align-items: flex-start;
    gap: 6px;
    margin: 0 20px 22px;
    padding: 0 0 18px;
    border-top: none;
    border-bottom: 1px solid rgba(var(--fg-rgb), 0.12);
  }
  #cost-panel .cost-total-row > span:first-child {
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: rgba(var(--fg-rgb), 0.45);
  }
  #cost-panel-total {
    font-size: 34px;
    font-weight: 600;
    letter-spacing: -0.03em;
    line-height: 1.05;
    font-variant-numeric: tabular-nums;
  }
  #cost-panel-total .cost-item-cents {
    font-size: 0.52em;
    font-weight: 600;
    color: rgba(var(--fg-rgb), 0.5);
  }

  /* Same row recipe as New Chat / History / flags — only extra is the
     open-state gap that nudges flags below it. */
  #cost-fab {
    display: flex !important;
    margin-bottom: 0;
    transition: margin-bottom 0.2s ease, background 0.15s ease, transform var(--dur-tap) var(--ease-out);
  }
  #cost-fab[hidden] { display: none !important; }
  #cost-fab[aria-pressed="true"] {
    margin-bottom: 10px;
    background: rgba(var(--fg-rgb), 0.1);
  }

  /* "Open source emails" panel — takes over exactly the space #ai-menu.open
     and #cost-panel together occupy (sidebar edge to the globe/calendar
     column), sitting above both so it fully replaces them instead of layering
     on top. Enters from the sidebar edge, growing rightward (left-to-right)
     to its resting bounds (sidebar edge to globe/calendar edge); closing
     reverses that, collapsing back down to the sidebar edge.

     Deliberately NOT a translateX on a fixed-width box (the shape every other
     slide-in panel here uses, e.g. #cost-panel): this panel's width isn't a
     constant — it's `100% - --sidebar-w - --right-col`, which changes with
     --cost-col-w/--right-col — so translateX(±100%) shifts it by however wide
     it HAPPENS to be, not by however far it needs to travel. That's exactly
     what broke the sidebar earlier (see git history): the "closed" box
     mirrored right back onto neighboring chrome instead of clearing it, and
     only z-index luck kept it from painting over. A LEFT-anchored version of
     that same translateX approach would replay the identical bug against the
     sidebar the instant it's closed.
     Animating `right` from the SAME point as `left` sidesteps the whole
     class: closed, right === left (a zero-width box at the sidebar edge —
     zero area, nothing to paint, nothing to be hidden behind); open, right
     eases out to the globe/calendar edge, growing the box rightward. No
     transform, no self-width math, so it can never re-cover a neighbor
     regardless of how wide the panel ends up being.

     z-index still stays below #app-sidebar's (6) — same reason #cost-panel
     is 5, not 6 — as a second line of defense, not the only one. */
  /* #photo-panel (a still-tentative idea's photos, opened automatically by
     openIdeaDetail the moment they load — no button) shares this exact same
     left-to-right slide — same reasoning, same math, just a second panel
     taking over the identical space. See openPhotoGalleryPanel/
     closePhotoGalleryPanel in app.js. */
  #email-panel, #photo-panel {
    display: flex !important;
    flex-direction: column;
    position: fixed;
    top: 0;
    bottom: 0;
    left: var(--sidebar-w, 56px);
    right: calc(100% - var(--sidebar-w, 56px));
    width: auto;
    z-index: 5;
    background: var(--bg);
    border-right: 1px solid var(--glass-border);
    transition: left 0.4s var(--ease-out), right 0.4s var(--ease-out);
    pointer-events: none;
    overflow: hidden;
  }
  body.email-panel-open #email-panel, body.photo-panel-open #photo-panel {
    right: var(--right-col);
    pointer-events: auto;
  }
  @media (prefers-reduced-motion: reduce) {
    #cost-fab { transition: none; transform: none !important; }
    #cost-panel { transition: none; }
    #ai-menu.open { transition: none; }
    #app { transition: none; }
    #email-panel, #photo-panel { transition: none; }
  }
  /* Flags, history, budget, and new-chat live in the sidebar on desktop. */
  #ai-menu-flags { display: none !important; }
  #ai-menu-top-row { display: none !important; }
  #ai-history-btn,
  #ai-new-chat-btn { display: none !important; }
  /* No floating box: fills the pane completely, no border/blur/shadow/rounded
     corners — just the conversation sitting directly on the page. Solid
     --bg (not "none") so this reads as a real panel rather than a
     transparent cutout showing the globe/space behind it — same base the
     globe/calendar side already sits on.
     Vertical pad only on the card. Header / history / budget stay a 694px
     reading column; the composer is a bit wider (752px, matching Grok's
     query bar) so it breaks that stack; the thread is full pane width so its
     scrollbar sits on the AI section's right edge, with message text kept at
     694px via horizontal padding. 24px minimum side inset on a narrower
     desktop. Bottom pad matches Grok.com — clear air under the composer. */
  #ai-menu.open .globe-menu-card {
    width: 100%;
    height: 100%;
    max-height: none;
    border: none;
    border-radius: 0;
    background: var(--bg);
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    box-shadow: none;
    font-size: 15px;
    padding: 48px 0 32px;
  }
  #ai-menu-header,
  #ai-history-list,
  #ai-menu-budget-view {
    max-width: 694px;
    width: calc(100% - 48px);
    margin-left: auto;
    margin-right: auto;
    box-sizing: border-box;
  }
  #ai-menu-form {
    max-width: 752px;
    width: calc(100% - 48px);
    margin-left: auto;
    margin-right: auto;
    box-sizing: border-box;
  }
  /* Full-pane scroller so the thin bar sits on the AI section's right edge;
     message measure stays 694px via side padding (24px min). */
  #ai-menu-thread {
    width: 100%;
    max-width: none;
    margin-left: 0;
    margin-right: 0;
    padding: 0 max(24px, calc((100% - 694px) / 2)) 0.6em;
    box-sizing: border-box;
    scrollbar-width: thin;
    scrollbar-color: rgba(var(--fg-rgb), 0.14) transparent;
  }
  #ai-menu-thread::-webkit-scrollbar { width: 6px; }
  #ai-menu-thread::-webkit-scrollbar-track { background: transparent; }
  #ai-menu-thread::-webkit-scrollbar-thumb {
    background: rgba(var(--fg-rgb), 0.14);
    border-radius: 99px;
  }
  /* Day view: keep a little air under the pane edge so the back/prev/next
     buttons are not flush with the top (used to pull the row up to line up
     with globe chrome). Margin-bottom stays larger for air between the
     title and the Morning/Afternoon/Evening columns underneath. */
  #day-header { margin-top: 10px; margin-bottom: 28px; }
  /* Day/month/year title: bigger, matching the month view's own desktop bump
     (#calendar-title, set in the landscape+fine block above) instead of
     staying at the base 18px used everywhere else. */
  #day-title { font-size: 23px; letter-spacing: -0.01em; }
}

/* iPad landscape (touch): keep the account avatar on the LEFT, lowered. */
@media (orientation: landscape) and (pointer: coarse) and (min-width: 1024px) {
  #globe-account-btn { left: 16px; right: auto; top: calc(env(safe-area-inset-top, 0px) + 72px); }
  #globe-trips-bubbles {
    left: 16px; right: auto; align-items: flex-start;
    top: calc(env(safe-area-inset-top, 0px) + 72px + var(--globe-btn) + 10px);
  }
}

/* Snappy luxurious animation for menu open (non-intrusive, translucent feel via glass) */
#ai-menu.open .globe-menu-card {
  animation: menu-open-pop 0.28s var(--ease-out) both;
}

@keyframes menu-open-pop {
  from {
    opacity: 0;
    transform: scale(0.92) translateY(-8px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  #ai-menu.open .globe-menu-card {
    animation: none;
  }
}

.globe-menu-row {
  display: flex;
  align-items: center;
  gap: 0.7em;
  width: 100%;
  text-align: left;
  font-size: 1em;
  font-weight: 600;
  color: var(--fg);
  border-radius: var(--r-chip);
  padding: 0.85em 1em;
  box-shadow: none;
  background: var(--glass-bg);
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
  border: 1px solid var(--glass-border);
  transition: transform var(--dur-tap) var(--ease-snap), background 0.15s ease;
}
.globe-menu-row:active { background: var(--press); transform: scale(0.98); }
.globe-menu-row[aria-expanded="true"] .globe-menu-row-chev { transform: rotate(90deg); }
.globe-menu-row-icon { flex-shrink: 0; display: inline-flex; opacity: 0.9; }
.globe-menu-row-icon svg { width: 1.3em; height: 1.3em; }
.globe-menu-row-label { flex: 1; min-width: 0; }
.globe-menu-row-chev {
  flex-shrink: 0;
  font-size: 1.28em;
  color: rgba(var(--fg-rgb), 0.45);
  transition: transform 0.2s ease;
  line-height: 1;
}

/* ---------- AI travel-agent menu: header, chat thread, history list ----------
   Typography follows the rest of the app (see day cards + ideas panel):
     • body / reading text ≈ --menu-fs (14 phone / 16 iPad / 15 desktop)
     • secondary / muted captions a step down (~0.85–0.93em), never below ~12px
     • weight + opacity carry hierarchy more than shrinking size further
   The chat card is a flex column: header + form stay fixed, thread scrolls. */

/* Chat card: column layout so the thread eats leftover height inside the
   FIXED shell and the composer stays put at the bottom. */
#ai-menu .globe-menu-card {
  display: flex;
  flex-direction: column;
  overflow: hidden;              /* scroll lives on the thread, not the whole card */
  /* Roomier than the hamburger menu's tight 10px/12px — matches the generous
     side margins Notion gives its own chat column (studied on app.notion.com:
     the thread's own text sits well clear of the panel edge on every side).
     em-based so it scales with --menu-fs across phone/tablet/desktop instead
     of a fixed px inset that would eat too much of a narrow phone card. */
  padding: 1.5em 2em 1.3em;
  box-sizing: border-box;
}

/* Title on the left, "history" + "new chat" icon buttons on the right —
   same row-of-controls pattern grok.com uses above its own chat thread. */
#ai-menu-header {
  display: flex;
  align-items: flex-start;
  gap: 6px;
  flex-shrink: 0;
  padding: 0 0 0.9em;    /* horizontal inset now lives on the card padding above */
}
#ai-menu-title {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 6px;
  color: rgba(var(--fg-rgb), 0.9);
}
.ai-menu-title-route {
  font-size: 20px;
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1.2;
  color: var(--fg2);
  white-space: normal;
  overflow-wrap: anywhere;
}
.ai-menu-title-meta {
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.04em;
  color: rgba(var(--fg-rgb), 0.5);
  font-variant-numeric: tabular-nums;
}
.ai-icon-btn {
  flex-shrink: 0;
  width: 1.75em;
  height: 1.75em;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--r-chip);
  color: rgba(var(--fg-rgb), 0.75);
  transition: background 0.15s ease;
}
.ai-icon-btn:active { background: var(--press); }
#ai-history-btn[aria-pressed="true"] { background: var(--press); color: var(--fg); }
/* Inside a reservation chat (set by syncAiEmptyState) neither of these has
   anything to do: a reservation has exactly one chat, so there is no second
   one to start under it and no list of past ones to pick from. Both jobs move
   to #ai-menu-new-chat-top, which goes home where those choices live. Desktop
   already hides this pair outright (the sidebar carries them there), so this
   is what makes the two layouts agree rather than something phone-only. */
#ai-menu.ai-trip-chat #ai-history-btn,
#ai-menu.ai-trip-chat #ai-new-chat-btn { display: none !important; }

/* The conversation itself — fills the card's leftover height (below header,
   above composer) and scrolls when the chat gets long. The card now fills
   the whole quadrant, so this just eats whatever height that leaves. */
#ai-menu-thread {
  flex: 1 1 auto;
  min-height: 0;
  max-height: none;
  overflow-y: auto;
  overflow-x: hidden;
  -webkit-overflow-scrolling: touch;
  display: flex;
  flex-direction: column;
  /* Space between whole TURNS (a question, a reply, the next question) —
     a different, bigger rhythm than the paragraph-to-paragraph spacing
     inside one reply (see .ai-p + .ai-p below). 14px read as one long run-on
     block, question and answer fused together; this is Grok/Notion's own
     turn-to-turn air, measured on grok.com and notion.com. */
  gap: 32px;
  /* Top/bottom only — a shorthand here would wipe the desktop thread's
     side padding that keeps messages at 694px while the bar sits on the pane edge. */
  padding-top: 0;
  padding-bottom: 0.6em;
  /* Soft fade where the scrolling thread meets the title above and the
     composer below, instead of content cutting off on a hard edge — but
     only on whichever edge there's actually more conversation to scroll
     to. Faded PAST edges (the very first message, or the very last) just
     look like broken/blurry text with nothing to explain it, so
     updateAiThreadFade (see its own comment) collapses these two custom
     properties to 0px the moment that edge is scrolled all the way home.
     -webkit- prefix required for iPhone Safari. */
  --ai-thread-fade-top: 20px;
  --ai-thread-fade-bottom: 20px;
  -webkit-mask-image: linear-gradient(to bottom, transparent, black var(--ai-thread-fade-top), black calc(100% - var(--ai-thread-fade-bottom)), transparent);
  mask-image: linear-gradient(to bottom, transparent, black var(--ai-thread-fade-top), black calc(100% - var(--ai-thread-fade-bottom)), transparent);
}
#ai-menu-thread[hidden] { display: none; }

/* Swaps in for #ai-menu-thread + #ai-menu-form when the phone bar's Money tab is
   tapped (see showAiBudgetView) — same "fills the card's leftover height"
   sizing, so the swap never jumps the card's own height around. */
#ai-menu-budget-view {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  padding: 0 0 0.6em;
}
#ai-menu-budget-view[hidden] { display: none; }
/* The trip's total, on the title's line rather than at the foot of the list.
   Money is the one number you open this view for, so it reads first and big
   instead of after a scroll past every line item — which is also why the
   "Total" label is gone: with nothing else on that line the number needs no
   naming. Bigger than the route beside it (22px against 20px) so it is
   clearly the figure and not a second title. align-self: baseline sits it on
   the route's own baseline; the header is flex-start otherwise and it would
   ride high against a two-line title. */
/* The trip's total, the line under the route inside the title block. Money is
   the number you open this view for, so it sits with the title rather than at
   the foot of the list — and with nothing else on its line it needs no "Total"
   label. The title block is a flex COLUMN (align-items: flex-start), so this
   needs no positioning of its own: it stacks under the route and sits hard
   against the left edge like everything else there.

   Sized in em, off the title block's own font, so it tracks every place that
   restyles the header for a screen size instead of pinning one pixel value
   that only suits the width it was measured at. nowrap because a total is one
   token and breaking "$4,293.14" across two lines would be nonsense. */
#ai-menu-budget-total-amount {
  align-self: flex-start;
  font-size: 1.65em;
  font-weight: 700;
  letter-spacing: -0.02em;
  line-height: 1.15;
  color: var(--fg);
  white-space: nowrap;
}
#ai-menu-thread[hidden] { display: none; }
#ai-menu-hint {
  font-size: 0.93em;             /* muted caption — like .detail-subtitle / .recs-sub */
  line-height: 1.45;
  color: rgba(var(--fg-rgb), 0.55);
  padding: 0 0 0.3em;
}

/* Chat bubbles — user on the right (a quiet highlight, like Grok's own),
   the assistant flush left with no bubble background (reads as "the app
   talking", not a boxed-in reply). Size matches Grok.com chat messages. */
.ai-msg {
  font-size: 15px;
  line-height: 1.5;
  letter-spacing: normal;
  max-width: 92%;
  min-width: 0;
  overflow-wrap: break-word;
  word-break: normal;
}
.ai-msg-user {
  align-self: flex-end;
  background: rgba(var(--fg-rgb), 0.1);
  border-radius: var(--r-chip);
  padding: 8px 16px;
  color: var(--fg);
  position: relative;   /* the hover Edit/Copy buttons hang off the bottom of it */
  /* The thread's own turn-to-turn gap is 32px, and the Edit/Copy strip now
     hangs almost exactly that far below a user bubble — so on hover the
     buttons sat right up against the reply underneath, which is what made the
     pair feel wedged in. This is added ON TOP of the thread gap and applies
     only to a user message, so the question-to-answer step is the one that
     opens up and the reply-to-next-question rhythm is untouched. */
  margin-bottom: 14px;
}
/* Inline actions — a traveler's own bubble only (see renderAiMessage). Two
   small square buttons, a pencil and two sheets, sit just UNDER the bubble
   and fade in only while it's hovered, the same way Grok.com does it. They
   are positioned OUTSIDE the bubble's own box on purpose: an "Edit" row
   living inside it made the bubble tall and lopsided, with more air under
   the text than over it. Only on a real mouse — touch has no hover, so
   nothing shows on a phone. Saving swaps the whole bubble's view for a
   textarea (.ai-msg-editing hides .ai-msg-view). */
/* THE INVISIBLE HIT AREA.

   On grok.com the buttons live in a band that reaches well below the text,
   and the whole band is live: the pointer can be several pixels under the
   last line, nowhere near a button, and the pair still lights up. Measured
   on a real grok thread (DevTools, 2026-09-06) that band runs 44px past the
   bubble's bottom edge and spans the row, not just the two buttons.

   Ours used to be neither. It was only as wide as the buttons themselves
   (`right: 0` with no `left`), so the space below the text but left of them
   was dead; and it was `pointer-events: none` until already hovered, which
   meant the band could KEEP a hover the bubble had started but could never
   START one. Coming at the message from underneath — the natural direction
   when you're reaching for Edit — nothing happened at all.

   So: stretch it edge to edge (left + right), give it the full 44px of
   grok's depth, and leave it permanently hit-testable. Being a child of the
   bubble, the pointer landing anywhere in here counts as :hover on the
   bubble, which is what lights the buttons — the band is the trigger, not
   just the keeper. It stays invisible and costs nothing: it occupies the
   margin under a user message (see .ai-msg-user's margin-bottom, which is
   sized to clear it) so there is never anything beneath it to block.

   The top edge still overlaps the bubble by 2px on purpose. A real gap there
   would be a seam the pointer could cross where it is over neither box, and
   the buttons would flicker out mid-reach. */
.ai-msg-actions {
  position: absolute;
  top: calc(100% - 2px);
  left: 0;
  right: 0;
  padding-top: 8px;
  padding-bottom: 4px;   /* 2 + 8 + 32 + 4 = grok's 44px of reachable depth */
  display: flex;
  justify-content: flex-end;   /* full-width band, buttons still at the end */
  gap: 2px;            /* grok.com's own pitch: 34px between two 32px buttons */
  opacity: 0;
  transform: translateY(-2px);
  transition: opacity 0.12s ease, transform 0.12s ease;
}
/* Each slot is a fixed 32x32 box that holds TWO buttons stacked exactly on
   top of each other (edit+cancel in one slot, copy+save in the other) — see
   renderAiMessage's editSlot/copySlot. Sharing one box is what makes the
   swap land pixel-perfect: the cross replaces the pencil in ITS box, the
   tick replaces the sheets in ITS box, rather than the whole pair reflowing
   into a differently-sized row and landing a few px off. */
.ai-msg-act-slot {
  position: relative;
  width: 32px;
  height: 32px;
  flex-shrink: 0;
}
/* Measured off grok.com's own buttons (2026-09-06, DevTools on a real
   thread): 32x32, fully round, NO background and NO border at rest, glyph at
   rgb(158,158,158) — a ghost button that only becomes a button under the
   pointer. The bordered chips this had before read as form controls stapled
   to the bubble; these read as part of the message. */
.ai-msg-act-btn, .ai-msg-edit-act {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  padding: 0;
  border: none;
  border-radius: 9999px;
  background: transparent;
  color: rgb(158, 158, 158);
  cursor: pointer;
  /* Fast/snappy on purpose — this is a state swap the traveler just
     triggered by clicking Edit, not a passive hover reveal, so it should
     feel immediate rather than eased-and-lingering. */
  transition: background 0.15s ease, color 0.15s ease,
              opacity 0.14s var(--ease-snap), transform 0.14s var(--ease-snap);
}
.ai-msg-act-btn:hover, .ai-msg-edit-act:hover { color: var(--fg); background: rgba(var(--fg-rgb), 0.09); }
.ai-msg-act-btn:active, .ai-msg-edit-act:active { background: rgba(var(--fg-rgb), 0.15); }
.ai-msg-act-btn.ai-msg-copied { color: var(--fg); }
.ai-msg-act-btn svg, .ai-msg-edit-act svg { width: 14px; height: 14px; }
/* Rest state: the pencil/sheets sit at full size and opacity, the cross/tick
   are shrunk and invisible underneath them — ready to pop in without ever
   contributing to layout while hidden (position: absolute already keeps them
   out of flow; opacity+scale is just what animates). The band above is
   always hit-testable so it can trigger the hover; buttons inside it are
   not, until they are actually visible — otherwise an invisible button
   could be clicked by someone who just happened to press the mouse while
   passing through. */
.ai-msg-act-btn { pointer-events: none; }
.ai-msg-edit-act { opacity: 0; transform: scale(0.55); pointer-events: none; }
/* The threshold: the pointer being anywhere on the bubble (or on the strip
   holding the buttons, which is part of it) shows them; leaving shows them
   out again. :focus-within keeps them reachable by keyboard too, where
   there is no pointer to hover with. */
@media (hover: hover) {
  .ai-msg-user:hover .ai-msg-actions,
  .ai-msg-user:focus-within .ai-msg-actions {
    opacity: 1;
    transform: none;   /* settles the last 2px as it fades in */
  }
  .ai-msg-user:hover .ai-msg-act-btn,
  .ai-msg-user:focus-within .ai-msg-act-btn {
    pointer-events: auto;
  }
}
.ai-thread-busy .ai-msg-actions { display: none; }   /* a reply is already generating */
/* While editing the band stays exactly where it is and simply swaps its
   contents: the pencil and sheets shrink out, the cross and tick pop in at
   full size, each in the SAME slot the button it replaces was just sitting
   in. It is pinned visible rather than hover-revealed, because the two
   buttons you need to finish an edit must not vanish when the pointer
   wanders off the message. `!important` on pointer-events is the one place
   that needs it: the hover rule just above is a more specific selector
   (:hover adds a level) and would otherwise re-enable the now-invisible
   pencil/sheets the instant the mouse — already sitting on the bubble from
   the click that opened editing — registers as "hovering". */
.ai-msg-editing .ai-msg-actions {
  opacity: 1;
  transform: none;
}
.ai-msg-editing .ai-msg-act-btn {
  opacity: 0;
  transform: scale(0.55);
  pointer-events: none !important;
}
.ai-msg-editing .ai-msg-edit-act {
  opacity: 1;
  transform: scale(1);
  pointer-events: auto;
}
.ai-msg-editing .ai-msg-view { display: none; }
/* A user bubble is normally shrink-wrapped to its own short text
   (align-self: flex-end on .ai-msg-user) — far too narrow for comfortably
   editing a long prompt. While editing, stretch it to the same full width
   as the composer input at the bottom of the panel, instead of staying
   pinched to the original bubble's size. */
/* Editing does NOT resize the bubble's WIDTH. It used to stretch to the full
   width of the thread the moment Edit was tapped, which threw the message
   across the pane and moved the very words being corrected — width stays
   pinned to what startEditAiMessage measured. Height is the opposite: the
   textarea auto-grows (growAiEditInput) as the traveler types, and the
   bubble grows with it in normal document flow, pushing the rest of the
   thread down rather than scrolling inside a fixed box. min-width only
   rescues a two-word message from being a sliver to type into. */
.ai-msg-user.ai-msg-editing {
  min-width: min(220px, 100%);
  box-sizing: border-box;
}
/* The textarea is the only thing in here now that the cross and tick have
   moved out to the button band, so "the height of the edit box" is just
   "the height of the textarea it holds". */
.ai-msg-edit-box {
  display: flex;
  flex-direction: column;
  min-width: 200px;
}
/* No field inside a bubble — the textarea IS the bubble's text. Transparent,
   borderless, zero padding and the exact type of the message it replaces, so
   tapping the pencil changes nothing you can see except a caret appearing.
   That is the whole of Grok's inline edit, and the reason it feels like
   correcting a sentence rather than opening a form. */
.ai-msg-edit-input {
  display: block;
  width: 100%;
  resize: none;
  /* Auto-grow (growAiEditInput) sets an explicit height every time the text
     changes, so the box never needs its own scrollbar. */
  overflow-y: hidden;
  background: transparent;
  border: none;
  border-radius: 0;
  padding: 0;
  font: inherit;
  font-size: 15px;
  line-height: 1.5;
  color: var(--fg);
}
.ai-msg-edit-input:focus { outline: none; }
/* The editor's cross and tick — sizing, position, hover/active, and the
   opacity+scale swap are all on the shared .ai-msg-act-btn, .ai-msg-edit-act
   rule above (same slot as the pencil/sheets they replace). Only the tick's
   own accent color lives here. */
/* The tick is the confirming action, so it carries the weight — the same
   solid-white treatment .btn-save has, at icon size. */
.ai-msg-edit-act-save { color: rgba(var(--fg-rgb), 0.85); }
.ai-msg-edit-act-save:hover { background: var(--invert-bg); color: var(--invert-fg); }
.ai-msg-assistant {
  align-self: flex-start;
  color: rgba(var(--fg-rgb), 0.92);
  /* Old chats (and any stray text node) keep their real line breaks; new
     replies are laid out block by block — see appendFormattedReply. */
  white-space: pre-line;
}
/* One line of a reply. Real space between every paragraph, bullet and section
   so a long itinerary is easy to read instead of one solid chunk. */
.ai-msg-assistant .ai-p {
  margin: 0;
  white-space: normal;   /* the line is its own block now — nothing to preserve */
}
.ai-msg-assistant .ai-p + .ai-p { margin-top: 19px; }
/* A sub-heading the app itself parses ("Morning:", "Hotel:", a day/date
   line) — one tier below .ai-h2, but still clearly bigger than body text so
   a day heading like "Nov 4 - Istanbul" reads as a real section start, not
   another line of prose. A quiet hairline above it (same treatment as .ai-h2)
   separates one day from the next — same width as the text column itself,
   never stretching out to the bubble's own edge. */
.ai-msg-assistant .ai-h {
  color: var(--fg);
  font-size: 1.3em;
  font-weight: 700;
  padding-top: 0.8em;
  border-top: 1px solid rgba(var(--fg-rgb), 0.12);
}
/* The very first heading in a message has nothing above it to divide from —
   the hairline would just be a stray line under the message's own top edge. */
.ai-msg-assistant .ai-h:first-child {
  padding-top: 0;
  border-top: none;
}
.ai-msg-assistant .ai-p + .ai-h { margin-top: 1.25em; }
/* The time-of-day label, riding in front of its own sentence rather than
   sitting on a bold line of its own ("MORNING  Walk along the river…").

   Small caps and quiet on purpose: it is a signpost, not a statement, and the
   sentence beside it is what someone actually reads. Three of these used to be
   full-width bold headings inside every single day, set in the SAME style as
   the day's own date line, which is what made a planned day read as a wall of
   labels with no hierarchy in it.

   Given a fixed column so the sentences line up down the answer no matter
   which of the three words it is — the ragged left edge you get from a plain
   inline label is most of what made it feel untidy. 12ch, because it has to
   clear the longest of the three: measured at this size "AFTERNOON" is 77.1px
   against 72.8px for the other two, and at 10ch it was overflowing its own
   column and shunting that one sentence 4px right of the rest. */
.ai-msg-assistant .ai-period {
  display: inline-block;
  /* The gap lives INSIDE this width rather than as a margin, so the hanging
     indent below can cancel it with one matching number instead of two. */
  min-width: 13ch;
  /* text-indent INHERITS, so the hanging indent on the paragraph below would
     otherwise apply a second time inside this box and drag the word itself
     13ch off to the left, out of its own bounds — which made the label
     disappear completely. It has to be cancelled here. */
  text-indent: 0;
  /* NOT plain `vertical-align: baseline` — measured live across every
     period label on the page with actual glyph bounding boxes
     (getBoundingClientRect on a Range over just the label text vs. just
     the following word, both with no descenders): baseline alignment on
     an inline-block is computed from ITS OWN line box at its own smaller
     10.8px font-size, and that lands this label's text a consistent,
     reproducible 1px ABOVE the sentence beside it, every single time —
     a real measured mismatch, not an optical illusion, and not sub-pixel
     noise (every instance measured the exact same 1px). None of the named
     keyword values land on a clean, exact 0: text-bottom overshoots to
     -5px, text-top undershoots to -0.5px (itself a fractional, unreliable
     pixel that would round unpredictably). Nudging the baseline down by
     the exact measured 1px is what actually closes the gap to 0px,
     confirmed live across every label on the page — not a guess. This is
     font-size-pair specific: if the label's 0.72em or the sentence's own
     font-size ever changes, re-measure with the same technique (see
     firstWord glyph range vs. label glyph range) rather than assume this
     same 1px still applies. */
  vertical-align: -1px;
  font-size: 0.72em;
  font-weight: 700;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: rgba(var(--fg-rgb), 0.45);
}
/* Hangs the whole paragraph in the label's column: the padding pushes every
   line right, the matching negative indent pulls the FIRST line back so the
   label sits in the gutter. Without it a sentence that wrapped dropped its
   second line under the label and the tidy column fell apart on exactly the
   long entries that needed it most.
   NOT 13ch here, on purpose: "ch" is relative to the font-size of the element
   it's used ON, and .ai-period's 13ch is measured at ITS OWN smaller 0.72em
   font-size — a narrower box than 13ch measured at this paragraph's full 1em
   size. Using 13ch here too made this indent wider than the label actually
   is, pushing every wrapped line past the first word instead of under it
   (confirmed live — a two-line "Arrive at…" sentence landed its second line
   to the right of "Arrive"). 13 × 0.72 = 9.36ch is the same physical width in
   THIS element's own (larger) ch unit. */
.ai-msg-assistant .ai-p-period {
  padding-left: 9.36ch;
  text-indent: -9.36ch;
}
/* Narrow phones: a 13ch column plus a sentence is too much of the screen to
   give away, so the label sits on its own line above and the text runs full
   width — the indent has to come off with it. */
@media (max-width: 430px) {
  .ai-msg-assistant .ai-period {
    display: block;
    min-width: 0;
    margin: 0 0 0.15em;
  }
  .ai-msg-assistant .ai-p-period {
    padding-left: 0;
    text-indent: 0;
  }
}
/* The same label when what follows is a bullet list or another heading, so it
   has no sentence to ride in front of — kept, quietly, on its own line. */
.ai-msg-assistant .ai-period-line {
  font-size: 0.72em;
  font-weight: 700;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: rgba(var(--fg-rgb), 0.45);
}
.ai-msg-assistant .ai-p + .ai-period-line { margin-top: 1.1em; }
/* A MAJOR section heading ("## Day by day", "## Practical notes" — see
   FORMATTING in askGeminiForTripChat and formatInlineMarkdown). Noticeably
   bigger and heavier than .ai-h — not just a shade bolder — with its own
   quiet hairline rule above it, the same "a new topic starts, visibly" cue
   a well-typeset itinerary uses, drawn by CSS on every heading rather than
   asking the model to type one. */
.ai-msg-assistant .ai-h2 {
  color: var(--fg);
  font-size: 1.55em;
  font-weight: 800;
  letter-spacing: -0.015em;
  line-height: 1.3;
  padding-top: 0.9em;
  border-top: 1px solid rgba(var(--fg-rgb), 0.12);
}
/* The very first heading in a message has nothing above it to divide from —
   the hairline would just be a stray line under the message's own top edge. */
.ai-msg-assistant .ai-h2:first-child {
  padding-top: 0;
  border-top: none;
}
.ai-msg-assistant .ai-p + .ai-h2 { margin-top: 1.5em; }
/* "**bold**" (see formatInlineMarkdown) — a named place, a time, a price,
   inline in running text. Weight only, never a color change: this app is
   monochrome everywhere outside the globe. */
.ai-msg-assistant strong { font-weight: 700; color: var(--fg); }
/* Dashed items get a real bullet drawn in; numbered items keep their number.
   Both hang so a wrapped second line lines up under the first word. */
.ai-msg-assistant .ai-li {
  padding-left: 1.1em;
  position: relative;
}
.ai-msg-assistant .ai-li + .ai-li { margin-top: 10px; }
.ai-msg-assistant .ai-li::before {
  content: '•';
  position: absolute;
  left: 0.2em;
  color: rgba(var(--fg-rgb), 0.55);
}
.ai-msg-assistant .ai-li-num {
  padding-left: 1.6em;
  text-indent: -1.6em;
}
.ai-msg a:not(.ai-msg-btn) {
  color: var(--fg);
  font-weight: 600;
  text-decoration: underline;
  text-underline-offset: 2px;
}
/* Same look as the link above, but for an in-app action ("jump to this day")
   instead of an external href — a plain button dressed as the same text link. */
.ai-msg-link-btn {
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  color: var(--fg);
  font-weight: 600;
  text-decoration: underline;
  text-underline-offset: 2px;
  cursor: pointer;
}

/* A pair of real glass pill buttons (e.g. a flight search reply's "Open in
   Google Flights" + "Add tentative plan to calendar") — same glass-chip
   language as .fill-ideas-btn, not the plain underlined text links above. */
.ai-msg-btn-row {
  display: flex;
  flex-wrap: wrap;
  gap: 9px;
  margin-top: 10px;
}
/* A link-only message (a re-shared "View in Google Flights" pill, no text
   above it) has nothing for that 10px gap to separate — the button row is
   the bubble's only child, so the gap just added itself on top of the
   bubble's own 8px padding, making a user message that's a bare link read
   as 18px above the pill vs 8px below it. Zeroed only when the row is
   first, so a row that follows real text keeps its usual breathing room. */
.ai-msg-btn-row:first-child { margin-top: 0; }
/* Whatever comes right after a button row deserves the same real gap a new
   paragraph/heading already gets from the one before IT (see the .ai-p +
   .ai-h etc. rules below) — a button row has no margin-bottom of its own,
   so with nothing here the very next line sat jammed right against it,
   worst of all a "Morning:"/"Afternoon:"/"Evening:" sub-heading reading as
   glued to the button row for the period before it (confirmed live). Same
   pixel/em values as the equivalent .ai-p-led transitions, just keyed off
   a button row instead. */
.ai-msg-btn-row + .ai-p { margin-top: 19px; }
.ai-msg-btn-row + .ai-h { margin-top: 1.25em; }
.ai-msg-btn-row + .ai-h2 { margin-top: 1.5em; }
.ai-msg-btn-row + .ai-msg-photo { margin-top: 10px; }
/* Raised glass, not flat glass — a soft outer shadow plus a hairline inner
   highlight along the top edge, the same "real, liftable object" cue the
   day-card hover state (.item-pin-hover) already uses elsewhere in this
   app. A quiet lift on hover and a real press-down on tap (translateY, not
   just a scale) make the row feel like a considered control, not a plain
   text link that happens to have a border. */
.ai-msg-btn {
  display: inline-flex;
  align-items: center;
  /* Held to its column's width (see equalizeAiButtonRows), a button is often
     wider than its own label — so the label has to be centred in it. Left to
     the default the text hugged the left edge and left a gap on the right,
     which looks more broken than the ragged widths did. */
  justify-content: center;
  gap: 6px;
  padding: 9px 15px;
  border-radius: var(--r-btn);
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  color: var(--fg);
  text-decoration: none;
  cursor: pointer;
  background: var(--glass-bg);
  border: 1px solid var(--glass-border);
  box-shadow: 0 1px 0 0 rgba(var(--fg-rgb), 0.07) inset, 0 6px 14px -8px rgba(0, 0, 0, 0.55);
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
  transition: transform var(--dur-tap) var(--ease-snap), background 0.15s ease,
              border-color 0.15s ease, box-shadow 0.15s ease;
}
/* :not(:disabled) on both — an "Added" pill (.ai-msg-btn-added, always
   disabled — see markAddToCalendarButtonAdded) used to still lift/press
   under the cursor exactly like a live button, even though the click
   itself was already blocked. It looked clickable when it very much
   wasn't. Browsers apply :hover/:active to a disabled <button> unless the
   CSS says otherwise (unlike :focus, which they already withhold). */
@media (hover: hover) {
  .ai-msg-btn:not(:disabled):hover {
    background: var(--hover);
    border-color: rgba(var(--fg-rgb), 0.3);
    transform: translateY(-1px);
    box-shadow: 0 1px 0 0 rgba(var(--fg-rgb), 0.09) inset, 0 10px 20px -10px rgba(0, 0, 0, 0.6);
  }
}
.ai-msg-btn:not(:disabled):active {
  background: var(--press);
  transform: translateY(0) scale(0.97);
  box-shadow: 0 1px 0 0 rgba(var(--fg-rgb), 0.05) inset, 0 2px 6px -3px rgba(0, 0, 0, 0.5);
}
.ai-msg-btn:disabled { opacity: 0.6; cursor: default; }
/* The venue/topic name that OPENS the row (see isActionPill in
   renderLinkGroup) reads as the primary pick — a plain .ai-msg-btn with none
   of the idea/icon/added modifier classes — a touch brighter fill than the
   dashed-outline action beside it, so the pair reads as "this, then do this
   to it" rather than two buttons of equal weight. */
.ai-msg-btn:not(.ai-msg-btn-idea):not(.ai-msg-btn-icon):not(.ai-msg-btn-added) {
  background: rgba(var(--fg-rgb), 0.1);
}
@media (hover: hover) {
  .ai-msg-btn:not(.ai-msg-btn-idea):not(.ai-msg-btn-icon):not(.ai-msg-btn-added):hover {
    background: rgba(var(--fg-rgb), 0.16);
  }
}
/* One shared reminder under a row of pills whose date/time couldn't be
   baked into the link itself (see dateNote in buildAiChatRecommendationLinks)
   — small and quiet, not competing with the buttons or the reply above. */
.ai-msg-btn-note {
  margin-top: 6px;
  font-size: 12px;
  color: rgba(var(--fg-rgb), 0.55);
}
/* ── The trip chat's opening summary ──────────────────────────────────────────
   Reads top to bottom in order of what matters: WHERE you're going, then the
   facts about it, then what's actually booked. Monochrome like everything that
   isn't the map, with the same muted tone and tabular figures the day cards use. */
.ai-msg-summary { max-width: 100%; }
.trip-summary-route {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0 7px;
  font-size: 19px;
  font-weight: 600;
  line-height: 1.25;
  letter-spacing: -0.01em;
  color: var(--fg);
}
.trip-summary-arrow {
  font-size: 14px;
  font-weight: 400;
  color: rgba(var(--fg-rgb), 0.38);
}
.trip-summary-meta {
  margin-top: 5px;
  font-size: 12px;
  letter-spacing: 0.04em;
  color: rgba(var(--fg-rgb), 0.55);
  font-variant-numeric: tabular-nums;
}
.trip-summary-foot {
  margin: 14px 0 0;
  font-size: 12.5px;
  line-height: 1.45;
  color: rgba(var(--fg-rgb), 0.5);
}
/* A plain section label ("Hotel") ahead of that category's own button row —
   see the `heading` link type in renderAiMessage. */
.ai-msg-link-heading {
  margin-top: 10px;
  font-size: 13px;
  font-weight: 600;
  color: rgba(var(--fg-rgb), 0.7);
}
/* Dashed outline — same "not booked yet" signal as .item-idea everywhere else. */
.ai-msg-btn-idea {
  border-style: dashed;
  border-width: 2px;
  border-color: rgba(var(--fg-rgb), 0.4);
}
/* Icon-only confirmation button ("View on your calendar") — same glyph as the
   reservation's own type (flight/hotel/restaurant/etc., see CATEGORY_SVG),
   ringed in a dotted circle so it reads as "tap to jump there", not a label. */
.ai-msg-btn-icon {
  width: 40px;
  height: 40px;
  padding: 0;
  justify-content: center;
  border-style: dotted;
  border-width: 2px;
  border-color: rgba(var(--fg-rgb), 0.4);
}
.ai-msg-btn-icon svg { width: 20px; height: 20px; }
/* The clicked "Add to calendar" pill's own post-click state — its own kind's
   icon plus the single word "Added", replacing the pill's original label in
   place (see markAddToCalendarButtonAdded). Same pill shape/tone (still
   .ai-msg-btn(-idea)), just content-swapped and inert, so it reads as "this
   exact control confirmed it" rather than a different element appearing. */
.ai-msg-btn-added { gap: 6px; cursor: default; }
/* A live, clickable pill that carries its activity's own glyph beside a
   short, uniform label ("Book now") instead of repeating the venue's full
   name — see buildAiChatDayPlanSections. Same icon+label layout and sizing
   as .ai-msg-btn-added above (this is the SAME visual idea, just for a pill
   that isn't disabled yet), so a whole row of these — a tour, a restaurant,
   a Add to calendar beside each — reads as one consistent button size
   instead of one pill stretching wide with a full place name and its
   neighbor staying short. */
.ai-msg-btn-added svg,
.ai-msg-btn-icon-label svg { width: 16px; height: 16px; flex-shrink: 0; }
/* A raw URL the model slipped into plain reply text, swapped for a small
   search pill instead (see RAW_URL_RE in appendFormattedReply) — smaller
   than a normal action pill since it's standing in for a stray link, not a
   whole recommendation. */
.ai-msg-inline-search-btn {
  margin: 2px 0 6px;
  padding: 6px 12px;
  font-size: 12px;
}

/* One real photo of the actual place, with its OWN button row right under
   IT (see attachInlinePlanPhotoFor and the flush/leftover call order in
   appendFormattedReply) — the same "a good travel agent's written
   itinerary has pictures in it" idea Grok's own replies use. A plain
   rounded card, a soft fade-in once the image actually decodes (never a
   hard pop-in), a small quiet caption below — never a colored border or a
   badge, this app's own monochrome-everywhere-but-the-globe rule applies
   here too. Empty (no url resolved) leaves nothing behind — see the
   `figure.remove()` case in attachInlinePlanPhotoFor.
   The image is shown WHOLE, at its own natural aspect ratio, never forced
   into a fixed box — a fixed width+height with object-fit:contain used to
   pillarbox a tall/portrait photo with black bars down both sides (the box
   was letterboxed to fit the image, not sized to it). align-items:
   flex-start is what keeps it hugging the left edge instead of the flex
   column stretching it back out to the bubble's full width. */
.ai-msg-photo {
  margin: 10px 0 0;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 6px;
  max-width: 100%;
}
/* The carousel itself: every real photo known for this one place (see
   attachInlinePlanPhotoFor), left to right, one gentle horizontal scroll —
   never a single static picture again for a place that has more than one. */
.ai-msg-photo-strip {
  display: flex;
  gap: 8px;
  max-width: 100%;
  overflow-x: auto;
  overflow-y: hidden;
  scroll-snap-type: x proximity;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: thin;
  scrollbar-color: rgba(var(--fg-rgb), 0.14) transparent;
}
.ai-msg-photo-strip::-webkit-scrollbar { height: 6px; }
.ai-msg-photo-strip::-webkit-scrollbar-track { background: transparent; }
.ai-msg-photo-strip::-webkit-scrollbar-thumb { background: rgba(var(--fg-rgb), 0.14); border-radius: 99px; }
.ai-msg-photo-strip img {
  display: block;
  flex: none;
  width: auto;
  height: 220px;
  max-width: 85vw;
  border-radius: var(--r-chip);
  border: 1px solid var(--glass-border);
  opacity: 0;
  scroll-snap-align: start;
  transition: opacity 0.35s var(--ease-out);
}
.ai-msg-photo-strip img.ai-msg-photo-loaded { opacity: 1; }
.ai-msg-photo figcaption {
  font-size: 12px;
  color: rgba(var(--fg-rgb), 0.5);
  padding: 0 2px;
}

/* One real pick (a hotel, a restaurant, a tour) inside a day-by-day plan —
   its own photo, its own name, one short honest reason it's worth it, then
   its own buttons. The photo's own figcaption is redundant here (the bold
   name right under it says the same thing), so it's hidden rather than
   shown twice. */
.ai-plan-item { margin: 14px 0 0; }
.ai-plan-item .ai-msg-photo { margin-top: 0; }
.ai-plan-item .ai-msg-photo figcaption { display: none; }
.ai-plan-item-name {
  font-weight: 600;
  margin: 8px 0 2px;
}
.ai-plan-item-why {
  font-size: 13px;
  line-height: 1.4;
  color: rgba(var(--fg-rgb), 0.55);
  margin: 0 0 8px;
}
/* Room to breathe after a card's own buttons — whatever follows (the next
   day's heading, the next period, a fresh paragraph, another card) needs
   clearly more air than the tight spacing inside the card itself. */
.ai-msg-assistant .ai-plan-item + .ai-p { margin-top: 22px; }
.ai-msg-assistant .ai-plan-item + .ai-h { margin-top: 1.4em; }
.ai-msg-assistant .ai-plan-item + .ai-h2 { margin-top: 1.6em; }
.ai-msg-assistant .ai-plan-item + .ai-period-line { margin-top: 1.3em; }
.ai-msg-assistant .ai-plan-item + .ai-plan-item { margin-top: 22px; }

/* History list — replaces the thread in place when the clock icon is
   tapped; same fixed card, scrolls when the list is long. */
#ai-history-list {
  flex: 1 1 auto;
  min-height: 0;
  max-height: none;
  overflow-y: auto;
  overflow-x: hidden;
  -webkit-overflow-scrolling: touch;
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 0 0.1em 0.5em;
}
#ai-history-list[hidden] { display: none; }
.ai-history-empty {
  font-size: 0.93em;
  color: rgba(var(--fg-rgb), 0.5);
  padding: 0.3em 0.1em;
}
.ai-history-item {
  display: flex;
  align-items: center;
  gap: 2px;
  width: 100%;
  font-size: 0.93em;             /* secondary list row — just under body */
  padding: 0.15em 0.25em 0.15em 0.7em;
  border-radius: var(--r-chip);
  background: var(--glass-bg);
  border: 1px solid var(--glass-border);
  transition: background 0.15s ease;
}
.ai-history-item:active, .ai-history-item.active { background: var(--press); }
.ai-history-item-main {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: 1;
  min-width: 0;
  text-align: left;
  padding: 0.4em 0;
}
.ai-history-item-pin { flex-shrink: 0; font-size: 0.85em; }
.ai-history-item-title {
  flex: 1;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  font-weight: 600;
  color: var(--fg);
}
.ai-history-item-time {
  flex-shrink: 0;
  font-size: 0.85em;             /* footer-scale timestamp */
  color: rgba(var(--fg-rgb), 0.5);
}
.ai-history-item-menu-btn {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 2em;
  height: 2em;
  border-radius: var(--r-btn);
  color: rgba(var(--fg-rgb), 0.55);
  transition: background 0.15s ease, color 0.15s ease;
}
.ai-history-item-menu-btn:hover, .ai-history-item-menu-btn:active {
  background: rgba(var(--fg-rgb), 0.12);
  color: var(--fg);
}
.ai-history-item-rename-input {
  flex: 1;
  min-width: 0;
  font: inherit;
  font-weight: 600;
  color: var(--fg);
  background: rgba(var(--fg-rgb), 0.09);
  border: 1px solid var(--glass-border);
  border-radius: 8px;
  padding: 0.2em 0.5em;
}

/* History panel's own tiny header: the Select/Cancel toggle, plus a live
   "N Selected" count once multi-select is on — the same row Apple's own
   Notes/Mail list keeps above its rows for this exact purpose. */
#ai-history-toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  flex-shrink: 0;
  padding: 0 0.25em 0.5em;
}
#ai-history-toolbar[hidden] { display: none; }
#ai-history-select-count {
  font-size: 0.85em;
  font-weight: 600;
  color: rgba(var(--fg-rgb), 0.5);
}
#ai-history-select-btn {
  font-size: 0.9em;
  font-weight: 600;
  padding: 0.3em 0.6em;
  border-radius: var(--r-chip);
  color: rgba(var(--fg-rgb), 0.75);
  transition: background 0.15s ease, color 0.15s ease;
}
#ai-history-select-btn:hover, #ai-history-select-btn:active {
  background: rgba(var(--fg-rgb), 0.1);
  color: var(--fg);
}

/* The leading circle each row grows once Select is tapped — an empty ring
   that fills solid with a check mark once tapped, Apple's own Mail/Notes/
   Photos selection-circle language, just in this app's monochrome instead
   of iOS blue. */
.ai-history-item-select {
  flex-shrink: 0;
  width: 1.3em;
  height: 1.3em;
  margin-right: 2px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  border: 1.5px solid rgba(var(--fg-rgb), 0.35);
  color: transparent;
  transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}
.ai-history-item-select.checked {
  background: var(--invert-bg);
  border-color: var(--invert-bg);
  color: var(--invert-fg);
}

/* Bottom action bar for multi-select — Select All / Delete, the same
   pairing Apple's own list views show once Edit/Select is tapped. A thin
   top hairline separates it from the scrolling list above, like an iOS
   toolbar sitting under a list. */
#ai-history-bulk-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  flex-shrink: 0;
  padding: 0.6em 0.25em 0;
  border-top: 1px solid var(--glass-border);
}
#ai-history-bulk-bar[hidden] { display: none; }
#ai-history-select-all-btn, #ai-history-bulk-delete-btn {
  font-size: 0.9em;
  font-weight: 600;
  padding: 0.3em 0.6em;
  border-radius: var(--r-chip);
  color: rgba(var(--fg-rgb), 0.75);
  transition: background 0.15s ease, color 0.15s ease, opacity 0.15s ease;
}
#ai-history-select-all-btn:hover, #ai-history-select-all-btn:active,
#ai-history-bulk-delete-btn:not(:disabled):hover, #ai-history-bulk-delete-btn:not(:disabled):active {
  background: rgba(var(--fg-rgb), 0.1);
  color: var(--fg);
}
#ai-history-bulk-delete-btn:disabled {
  opacity: 0.4;
  cursor: default;
}

#ai-menu-form {
  display: flex;
  align-items: flex-end;         /* mic stays pinned to the bottom as the box grows */
  gap: 3px;                      /* plus↔input and input↔mic spacing — tightened on request */
  flex-shrink: 0;
  padding-top: 0.35em;
  transition: padding 0.22s var(--ease-out);
}
#ai-menu-form[hidden] { display: none; }
/* Growing past one line: the plus/mic icons drop below the box instead of
   squeezing beside a taller textarea — same "compose box opens up" behavior
   Notion/ChatGPT use, in both the home hero and this compact/reservation-chat
   composer. .ai-input-wrap claims the whole first row (its own flex-basis:
   100% is what forces the wrap); order puts it first regardless of where it
   sits in the markup, so the icons land on the row under it without changing
   their left/right positions at rest. Toggled by autoGrowAiInput() the
   moment the textarea's content exceeds one line — see its own comment. */
#ai-menu.ai-composer-tall #ai-menu-form { flex-wrap: wrap; }
#ai-menu.ai-composer-tall .ai-input-wrap { order: 1; flex-basis: 100%; width: 100%; }
#ai-menu.ai-composer-tall .ai-plus-wrap { order: 2; margin-top: 8px; }
#ai-menu.ai-composer-tall #ai-menu-mic { order: 3; margin-top: 8px; margin-left: auto; }
/* Wraps just the textarea (not the plus/mic buttons) so #ai-ph-track
   (the rotating-placeholder overlay — see startAiPlaceholderCycle) can be
   `inset: 0` and land EXACTLY on the textarea's own box in every mode/
   breakpoint, with no hand-tuned left/right px math to keep in sync with
   the buttons around it (the empty-mode-only version of this overlay used
   to do that math against the whole form; this wrapper is what let the
   same overlay extend to the compact/reservation-chat composer too). */
.ai-input-wrap {
  position: relative;
  flex: 1;
  min-width: 0;
}
/* Compact composer: same 16px as other phone inputs (stops Safari zoom),
   but less padding than before so it doesn't dominate the panel. Grows with
   text via autoGrowAiInput() up to a few lines, then scrolls. */
#ai-menu-input {
  width: 100%;
  resize: none;
  overflow-y: hidden;
  background: rgba(var(--fg-rgb), 0.07);
  border: 1px solid var(--glass-border);
  border-radius: var(--r-chip);
  padding: 0.4em 0.85em 0.4em 0.5em;
  font-size: 16px;                 /* 16px stops iPhone Safari zooming on focus */
  line-height: 1.35;
  font-family: inherit;
  color: var(--fg);
  /* One clean line at rest — matches what autoGrowAiInput() sets in JS for
     an empty box, so there's no flash of a taller box before that JS runs.
     Only grows (via inline height, set by that same JS) once real text is
     typed — never for an empty/placeholder-only box.
     Height is line-height + this padding, EXACTLY 2.15em — the same 2.15em
     #ai-menu-mic uses for its own box below. The old value
     (0.45em padding + a leftover +2px, from when this box still had a
     border) added up to ~2.25em+2px: taller than the buttons, so
     align-items: flex-end (needed so the buttons pin to the bottom as the
     box grows multi-line — see below) bottom-aligned the two boxes but left
     the textarea's own text sitting visibly lower than the button icons.
     Matching the resting height exactly removes that gap without touching
     the flex-end growth behavior. */
  height: calc(1.35em + 0.8em);
  max-height: 5.5em;               /* hard CSS cap so a tall paste can't blow the layout */
  transition: height 0.18s var(--ease-out);
}
#ai-menu-input:focus {
  outline: none;
  background: rgba(var(--fg-rgb), 0.11);
  box-shadow: 0 0 0 3px rgba(var(--fg-rgb), 0.14);
}
/* The INPUT's own text stays at 16px always (hardcoded above — needed to
   stop iOS Safari zooming the whole page on focus). The PLACEHOLDER can be
   sized independently of that (::placeholder supports its own font-size),
   so it's set to --menu-fs instead — the same scale .ai-msg (the chat
   bubbles right above it) uses. On a real iPhone this closed a real, visible
   gap: the empty-box placeholder read noticeably bigger than every chat
   message, even though typed text and Safari's zoom guard were both fine. */
/* nowrap + ellipsis: belt-and-suspenders alongside the JS fix in
   autoGrowAiInput — a wrapped, 2-line placeholder is what made the resting
   composer look too tall/off-center in the first place. This keeps it a
   clean single line even if some future code path skips that JS. */
#ai-menu-input::placeholder {
  color: rgba(var(--fg-rgb), 0.4);
  font-size: var(--menu-fs);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Mic — matches the compact input height (not oversized vs the text). */
#ai-menu-mic {
  flex-shrink: 0;
  width: 2.15em;
  height: 2.15em;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--r-chip);
  transition: transform var(--dur-tap) var(--ease-snap), background 0.15s ease, margin-top 0.22s var(--ease-out);
  background: var(--glass-bg);
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
  border: 1px solid var(--glass-border);
  color: var(--fg);
}
#ai-menu-mic:active { background: var(--press); transform: scale(0.93); }
/* Listening — a quiet red glow so it's obvious the mic is live. */
#ai-menu-mic[aria-pressed="true"] {
  background: rgba(255, 70, 70, 0.22);
  border-color: rgba(255, 120, 120, 0.4);
  color: #ff9a9a;
  animation: ai-mic-pulse 1.4s ease-in-out infinite;
}
@keyframes ai-mic-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(255, 90, 90, 0.35); }
  50%      { box-shadow: 0 0 0 6px rgba(255, 90, 90, 0); }
}
@media (prefers-reduced-motion: reduce) {
  #ai-menu-mic[aria-pressed="true"] { animation: none; }
}

/* ---------- AI chat: Notion-matched styling, on the app's own base --bg ----------
   The traveler asked for the AI chat panel to look like the Notion AI side
   panel (app.notion.com) — a plain, solid dark surface instead of the app's
   usual see-through glass, since a chat panel of readable paragraphs doesn't
   need the globe showing through it — but the app's base --bg (the same
   base the globe/calendar side already sits on — see --bg's own comment),
   not Notion's own dark-gray #202020. Scoped entirely under #ai-menu so the
   hamburger menu (.globe-menu-card is shared with it) keeps its normal
   glass look untouched. Surfaces that need to lift off that base (message
   pills, composer) use a near-black #141414 with a soft white hairline
   instead of Notion's literal gray hex, so they still read as "a raised
   card" rather than banding against the base. Text colors are still
   Notion's: body rgb(240,239,237), muted rgb(155,154,151). */
#ai-menu .globe-menu-card {
  background: var(--bg);
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  border: 1px solid rgba(var(--fg-rgb), 0.12);
  border-radius: 16px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.25), 0 20px 50px rgba(0, 0, 0, 0.6);
}
#ai-menu-title, .ai-menu-title-route { color: var(--fg2); }
.ai-icon-btn { color: var(--fg2-dim); }
.ai-icon-btn:active,
.ai-icon-btn:hover { background: rgba(var(--fg-rgb), 0.07); }
#ai-history-btn[aria-pressed="true"] { background: rgba(var(--fg-rgb), 0.11); color: var(--fg2); }
#ai-menu-hint { color: var(--fg2-dim); }

#ai-menu .ai-msg-assistant { color: var(--fg2); }
#ai-menu .ai-msg-assistant .ai-h { color: var(--fg2); }
#ai-menu .ai-msg-assistant .ai-li::before { color: var(--fg2-dim); }
#ai-menu .ai-msg a:not(.ai-msg-btn), #ai-menu .ai-msg-link-btn { color: var(--fg2); }

/* User's own message — a quiet raised pill against the black, not the app's
   white-glass tint. */
#ai-menu .ai-msg-user {
  background: var(--card-bg);
  border: 1px solid rgba(var(--fg-rgb), 0.12);
  color: var(--fg2);
}
/* Ghost buttons need no panel-specific surface — only the resting glyph
   colour is nudged to this panel's muted grey. */
#ai-menu .ai-msg-act-btn,
#ai-menu .ai-msg-edit-act { color: var(--fg2-dim); }
#ai-menu .ai-msg-act-btn:hover,
#ai-menu .ai-msg-edit-act:hover { background: rgba(var(--fg-rgb), 0.09); color: var(--fg2); }
#ai-menu .ai-msg-edit-act-save:hover { background: var(--invert-bg); color: var(--invert-fg); }
/* Only the text colour here — a background and a border would rebuild the
   very field .ai-msg-edit-input was flattened to get rid of. */
#ai-menu .ai-msg-edit-input { color: var(--fg2); }

/* Action pills (e.g. "Open in Google Flights") — same raised near-black
   card look as everything else in the panel, no blur. */
#ai-menu .ai-msg-btn {
  background: var(--card-bg);
  border: 1px solid rgba(var(--fg-rgb), 0.12);
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  color: var(--fg2);
}
/* :not(:disabled) — see the base .ai-msg-btn:hover/:active rule's own
   comment: an "Added" pill must not still look pressable under the cursor. */
#ai-menu .ai-msg-btn:not(:disabled):hover { background: rgba(var(--fg-rgb), 0.08); border-color: rgba(var(--fg-rgb), 0.2); }
#ai-menu .ai-msg-btn:not(:disabled):active { background: rgba(var(--fg-rgb), 0.14); }
/* The rule above flattens every pill in the chat panel to one solid 1px
   border, same specificity (an ID + a class) as .ai-msg-btn-idea/
   .ai-msg-btn-icon above — so on THIS panel it silently won, and a
   proposed-but-not-yet-added "Add to calendar" pill read identically to an
   already-booked one. Restated at the same specificity, after it in the
   file, so it wins the tie: the panel's flat-card look still applies
   everywhere, but tentative (dashed) and jump-to-it (dotted) pills keep the
   exact same "not booked yet" signal .item-idea uses everywhere else in the
   app, right down to the border width/color — this is the one thing that
   tells a traveler apart, at a glance, which of two pills sitting side by
   side is still just a suggestion. */
#ai-menu .ai-msg-btn-idea {
  border-style: dashed;
  border-width: 2px;
  border-color: rgba(var(--fg-rgb), 0.4);
}
#ai-menu .ai-msg-btn-icon {
  border-style: dotted;
  border-width: 2px;
  border-color: rgba(var(--fg-rgb), 0.4);
}

#ai-menu .ai-msg-btn-note { color: var(--fg2-dim); }

#ai-menu .ai-history-item {
  background: transparent;
  border: 1px solid transparent;
}
#ai-menu .ai-history-item:hover { background: rgba(var(--fg-rgb), 0.07); }
#ai-menu .ai-history-item:active, #ai-menu .ai-history-item.active { background: rgba(var(--fg-rgb), 0.11); }
#ai-menu .ai-history-item-title { color: var(--fg2); }
#ai-menu .ai-history-item-time, #ai-menu .ai-history-empty { color: var(--fg2-dim); }
#ai-menu .ai-history-item-menu-btn { color: var(--fg2-dim); }
#ai-menu .ai-history-item-menu-btn:hover, #ai-menu .ai-history-item-menu-btn:active {
  background: rgba(var(--fg-rgb), 0.11);
  color: var(--fg2);
}
#ai-menu .ai-history-item-rename-input {
  background: var(--card-bg);
  border: 1px solid rgba(var(--fg-rgb), 0.12);
  color: var(--fg2);
}

#ai-menu #ai-history-select-count { color: var(--fg2-dim); }
#ai-menu #ai-history-select-btn,
#ai-menu #ai-history-select-all-btn,
#ai-menu #ai-history-bulk-delete-btn { color: var(--fg2-dim); }
#ai-menu #ai-history-select-btn:hover, #ai-menu #ai-history-select-btn:active,
#ai-menu #ai-history-select-all-btn:hover, #ai-menu #ai-history-select-all-btn:active,
#ai-menu #ai-history-bulk-delete-btn:not(:disabled):hover, #ai-menu #ai-history-bulk-delete-btn:not(:disabled):active {
  background: rgba(var(--fg-rgb), 0.11);
  color: var(--fg2);
}
#ai-menu .ai-history-item-select { border-color: rgba(var(--fg-rgb), 0.3); }
#ai-menu .ai-history-item-select.checked { background: var(--invert-bg); border-color: var(--invert-bg); color: var(--invert-fg); }
#ai-menu #ai-history-bulk-bar { border-top-color: rgba(var(--fg-rgb), 0.12); }

/* Composer — one rounded card lifted off the black (bg #141414, soft white
   hairline) with the textarea transparent inside it and plain icon buttons
   (no separate glass chips) sitting on the same row. Sits with the same
   breathing room above it as the thread has below the header (margin-top),
   so header/thread/composer all read as one evenly-spaced column instead of
   being pinned edge to edge. */
#ai-menu #ai-menu-form {
  background: var(--card-bg);
  border: 1px solid rgba(var(--fg-rgb), 0.14);
  border-radius: var(--r-chip);
  padding: 10px 10px 10px 8px;
  margin-top: 0.5em;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.35);
  transition: border-color 0.15s ease, background 0.15s ease;
}
#ai-menu #ai-menu-form:focus-within {
  background: color-mix(in srgb, var(--card-bg), var(--fg) 6%);
  border-color: rgba(var(--fg-rgb), 0.26);
}
#ai-menu #ai-menu-input {
  background: transparent;
  border: none;
  color: var(--fg2);
}
#ai-menu #ai-menu-input:focus {
  background: transparent;
  box-shadow: none;
}
#ai-menu #ai-menu-input::placeholder { color: var(--fg2-faint); }
#ai-menu #ai-menu-mic {
  background: transparent;
  border: none;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  color: var(--fg2-dim);
}
#ai-menu #ai-menu-mic:hover { background: rgba(var(--fg-rgb), 0.07); color: var(--fg2); }
#ai-menu #ai-menu-mic:active { background: rgba(var(--fg-rgb), 0.09); }

/* Grok-style Attach plus — left of the composer text field. */
.ai-plus-wrap {
  position: relative;
  flex-shrink: 0;
  align-self: flex-end;
  transition: margin-top 0.22s var(--ease-out);
}
#ai-menu-plus {
  width: 40px;
  height: 40px;
  padding: 8px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 9999px;
  background: transparent;
  border: none;
  color: var(--fg2-dim);
  flex-shrink: 0;
  transition: background 0.15s ease, color 0.15s ease;
}
#ai-menu-plus svg {
  display: block;
  transition: transform var(--dur-tap) var(--ease-snap);
  transform-origin: center center;
}
#ai-menu-plus:hover { background: rgba(var(--fg-rgb), 0.07); color: var(--fg2); }
#ai-menu-plus:active { background: rgba(var(--fg-rgb), 0.09); }
#ai-menu-plus[aria-expanded="true"] svg { transform: rotate(45deg); }
#ai-plus-menu {
  position: absolute;
  bottom: calc(100% + 8px);
  left: 0;
  min-width: 220px;
  padding: 6px;
  background: var(--card-bg);
  border: 1px solid rgba(var(--fg-rgb), 0.12);
  border-radius: 16px;
  box-shadow: 0 8px 28px rgba(0, 0, 0, 0.45);
  z-index: 30;
}
#ai-plus-menu[hidden] { display: none; }
.ai-plus-menu-item {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  min-height: 40px;
  padding: 0 12px;
  border-radius: 10px;
  color: var(--fg2);
  font-size: 14px;
  font-weight: 550;
  letter-spacing: -0.01em;
  text-align: left;
  background: transparent;
  border: none;
}
.ai-plus-menu-item[hidden] { display: none; }
.ai-plus-menu-item:hover { background: rgba(var(--fg-rgb), 0.07); }
.ai-plus-menu-icon {
  width: 18px;
  height: 18px;
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--fg2-dim);
}
.ai-plus-menu-icon svg { display: block; }

/* ---------- Cost view: item list, add button, running total ----------
   Lives inside the AI chat card's own budget view now (#ai-menu-budget-view)
   — .cost-list/.cost-item/etc. below are shared with that card's own
   header markup, just no separate #cost-menu-* card shell anymore. */
.cost-list {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
  -webkit-overflow-scrolling: touch;
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 0 0.1em 0.5em;
  /* Same thin, translucent-white scrollbar as the AI chat thread
     (#ai-menu-thread) so the cost view's scrollbar matches it exactly. */
  scrollbar-width: thin;
  scrollbar-color: rgba(var(--fg-rgb), 0.14) transparent;
}
.cost-list::-webkit-scrollbar { width: 6px; }
.cost-list::-webkit-scrollbar-track { background: transparent; }
.cost-list::-webkit-scrollbar-thumb {
  background: rgba(var(--fg-rgb), 0.14);
  border-radius: 99px;
}
.cost-item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 0.6em 0.7em;
  border-radius: var(--r-chip);
  transition: background 0.15s ease, box-shadow 0.22s var(--ease-out), transform 0.22s var(--ease-out);
}
/* Dashed outline for a tentative/rough-estimate cost line — same "not booked,
   not paid" signal as .item-idea on the calendar and .ai-msg-btn-idea in
   chat. */
.cost-item-tentative {
  border: 2px dashed rgba(var(--fg-rgb), 0.4);
  border-radius: var(--r-chip);
}
/* THE MONEY LANDING — the cost panel's own version of .item-just-added's glow
   on the calendar card (see markJustAddedCostRow, app.js). This row is
   brand new DOM, not an existing element getting a class toggled, so unlike
   the calendar card it also rises in from a hair lower + smaller rather than
   only glowing in place — the two together read as "this just arrived",
   not merely "this is different". Quick and quiet on purpose: money
   appearing should feel instant and certain, never showy. */
.cost-item-just-added {
  animation: cost-item-just-added 1.2s var(--ease-out) forwards;
}
@keyframes cost-item-just-added {
  0%   { transform: translateY(5px) scale(0.97); box-shadow: 0 0 0 0 rgba(var(--fg-rgb), 0); }
  12%  { transform: translateY(0) scale(1); box-shadow: 0 0 0 1.5px rgba(var(--fg-rgb), 0.85), 0 0 16px 3px rgba(var(--fg-rgb), 0.2); }
  100% { transform: translateY(0) scale(1); box-shadow: 0 0 0 0 rgba(var(--fg-rgb), 0); }
}
@media (prefers-reduced-motion: reduce) {
  .cost-item-just-added { animation: none; box-shadow: 0 0 0 1.5px rgba(var(--fg-rgb), 0.6); }
}
.cost-item-icon {
  flex: 0 0 auto;
  width: 30px;
  height: 30px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--r-btn);
  color: rgba(var(--fg-rgb), 0.85);
  background: rgba(var(--fg-rgb), 0.07);
  border: 1px solid rgba(var(--fg-rgb), 0.10);
  transition: background 0.15s ease, color 0.15s ease;
}
.cost-item-icon:active { background: var(--press); color: var(--fg); }
.cost-item-icon svg { width: 16px; height: 16px; display: block; }
/* Same glass-recipe rectangle look the other pop-up forms use for fields
   (see .sheet-card input) — 16px stops iPhone Safari zooming on focus. */
.cost-item input {
  background: rgba(var(--fg-rgb), 0.07);
  border: 1px solid rgba(var(--fg-rgb), 0.10);
  border-radius: var(--r-btn);
  padding: 0.55em 0.85em;
  font-size: 16px;
  font-family: inherit;
  color: var(--fg);
  min-width: 0;
}
.cost-item input:focus {
  outline: none;
  background: rgba(var(--fg-rgb), 0.11);
  box-shadow: 0 0 0 3px rgba(var(--fg-rgb), 0.14);
}
.cost-item-label {
  flex: 1 1 auto;
  font-weight: 500;
}
.cost-item-amount-wrap {
  flex: 0 0 auto;
  width: 6em;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 1px;
  background: rgba(var(--fg-rgb), 0.07);
  border: 1px solid rgba(var(--fg-rgb), 0.10);
  border-radius: var(--r-btn);
  padding: 0 0.55em 0 0.65em;
  cursor: text;
  font-size: 16px;
  font-variant-numeric: tabular-nums;
  text-align: right;
}
.cost-item-amount-wrap:focus-within,
.cost-item-amount-wrap.is-editing {
  background: rgba(var(--fg-rgb), 0.11);
  box-shadow: 0 0 0 3px rgba(var(--fg-rgb), 0.14);
}
.cost-item-money-read {
  flex: 1 1 auto;
  min-width: 0;
  padding: 0.55em 0;
  font-size: inherit;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  text-align: right;
  white-space: nowrap;
  color: var(--fg);
}
.cost-item-money-empty {
  color: rgba(var(--fg-rgb), 0.4);
  font-weight: 600;
}
.cost-item-cents {
  font-size: 0.72em;
  font-weight: 600;
  color: rgba(var(--fg-rgb), 0.55);
}
.cost-item-amount {
  display: none;
  flex: 1 1 auto;
  width: 100%;
  min-width: 0;
  background: transparent !important;
  border: none !important;
  box-shadow: none !important;
  padding: 0.55em 0 !important;
  text-align: right;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  appearance: textfield;
  -moz-appearance: textfield;
}
.cost-item-amount-wrap.is-editing .cost-item-money-read { display: none; }
.cost-item-amount-wrap.is-editing .cost-item-amount { display: block; }
.cost-item-amount::-webkit-outer-spin-button,
.cost-item-amount::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}
@media (pointer: fine) {
  .cost-item:hover {
    background: rgba(var(--fg-rgb), 0.05);
  }
  .cost-item:hover .cost-item-amount,
  .cost-item:hover .cost-item-money-read {
    color: var(--fg);
  }
  .cost-item:hover .cost-item-cents {
    color: rgba(var(--fg-rgb), 0.7);
  }
  .cost-item-icon:hover {
    background: rgba(var(--fg-rgb), 0.12);
    color: var(--fg);
  }
}
/* This line's third of the same three-way pairing as .item.item-pin-hover above:
   hovering this row lights the matching card and pin, and hovering EITHER of those
   lights this row back (highlightLinkedEntry/highlightBudgetRow) — same lift language
   as the card gets, so all three read as one highlight no matter which one your eye
   started on. No box-shadow/glow here on purpose (removed on request) — just the
   background wash + lift. A hand-typed row with no reservation behind it never gets
   this class — see the entryId check in buildCostRow. */
.cost-item.cost-item-linked-hover {
  background: rgba(var(--fg-rgb), 0.07);
  transform: translateY(-2px);
}
.cost-item.cost-item-linked-hover .cost-item-amount,
.cost-item.cost-item-linked-hover .cost-item-money-read {
  color: var(--fg);
}
.cost-item.cost-item-linked-hover .cost-item-cents {
  color: rgba(var(--fg-rgb), 0.7);
}
/* The month-view half of a SEPARATE pairing (see highlightBudgetRowsForDay):
   hovering a day cell washes every cost line dated that day, not just one
   reservation's own family — several unrelated lines can share a day. A
   plain wash, no lift: it never claims the tighter "this IS that one
   reservation" link .cost-item-linked-hover makes above. */
.cost-item.cost-item-day-hover {
  background: rgba(var(--fg-rgb), 0.05);
  box-shadow: inset 0 0 0 1px rgba(var(--fg-rgb), 0.3);
}
/* City heading above that city's own expenses, on a multi-city trip — the
   budget then reads in the order the trip is travelled (first city at the
   top, final destination last), with that city's subtotal on the right and
   the grand total still at the very bottom. Monochrome and quiet: it's a
   signpost between groups, never louder than the numbers themselves. */
.cost-city-heading {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
  margin-top: 6px;
  padding: 0 0.2em 0.35em;
  border-bottom: 1px solid rgba(var(--fg-rgb), 0.12);
}
.cost-list > .cost-city-heading:first-child { margin-top: 0; }
.cost-city-name {
  font-size: 0.8em;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: rgba(var(--fg-rgb), 0.75);
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
}
.cost-city-subtotal {
  font-size: 0.9em;
  font-weight: 600;
  color: rgba(var(--fg-rgb), 0.6);
}
/* A CITY heading is a real <button> that folds its own rows away (see
   renderCostList), so it has to shed everything a browser gives a button and
   look exactly like the plain heading it sits beside — same rule under it,
   same spacing, same quiet type. Only the pointer and the chevron say it does
   anything. */
button.cost-city-heading {
  width: 100%;
  font: inherit;
  color: inherit;
  background: none;
  border: none;
  border-bottom: 1px solid rgba(var(--fg-rgb), 0.12);
  border-radius: 6px;
  cursor: pointer;
  text-align: left;
  -webkit-tap-highlight-color: transparent;
  transition: background 0.18s ease;
}
/* A nested city heading has no rule under it (see .cost-city-heading-sub) —
   restated here because the button selector above is the more specific one. */
button.cost-city-heading-sub { border-bottom: none; }
button.cost-city-heading:hover .cost-city-name,
button.cost-city-heading:focus-visible .cost-city-name { color: rgba(var(--fg-rgb), 0.95); }
button.cost-city-heading:focus-visible {
  outline: 1px solid rgba(var(--fg-rgb), 0.35);
  outline-offset: 2px;
}
/* The name carries the chevron, so the subtotal stays hard right. */
.cost-city-chevron {
  display: inline-flex;
  width: 0.9em;
  height: 0.9em;
  flex: none;
  opacity: 0.6;
  transition: transform 0.22s var(--ease-snap), opacity 0.18s ease;
}
.cost-city-chevron svg { width: 100%; height: 100%; display: block; }
button.cost-city-heading:hover .cost-city-chevron { opacity: 0.9; }
/* Folded: the chevron points right instead of down — one glyph, two states. */
.cost-city-collapsed .cost-city-chevron { transform: rotate(-90deg); }
/* A folded city keeps its subtotal at full strength: with the rows hidden it
   IS the number you're reading, so it must not stay dimmer than the country
   heading above it. Written to out-specify .cost-city-heading-sub's quieter
   nested-city colour further down the sheet, which otherwise wins on order. */
button.cost-city-heading.cost-city-collapsed .cost-city-subtotal,
button.cost-city-heading.cost-city-collapsed .cost-city-name {
  color: rgba(var(--fg-rgb), 0.82);
}
/* A city's rows, opening/closing — same 0fr/1fr grid-row trick
   .res-acc-body already uses for a reservation's own foldable detail, so a
   city folding away in the budget feels like the exact same motion as
   everything else in the app that expands/collapses, not a bespoke one.
   --ease-snap (used everywhere else this app wants a fold to feel immediate
   and alive, e.g. the day-turn swipe) rather than a generic ease, per the
   "snappy, not sluggish" brief this replaced the old plain hidden-attribute
   toggle for — that old version had NO transition at all, which is the
   "dynamic... with some life/motion to it" gap this closes. */
.cost-city-rows {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 0.24s var(--ease-snap);
}
.cost-city-rows.cost-city-open { grid-template-rows: 1fr; }
.cost-city-rows-inner { overflow: hidden; min-height: 0; }
@media (prefers-reduced-motion: reduce) {
  .cost-city-chevron { transition: none; }
  button.cost-city-heading { transition: none; }
  .cost-city-rows { transition: none; }
}
.cost-empty {
  font-size: 0.93em;
  color: rgba(var(--fg-rgb), 0.5);
  padding: 0.5em 0.2em;
}

.cost-add-btn {
  flex-shrink: 0;
  margin-top: 4px;
  padding: 0.7em;
  font-size: 0.95em;
  font-weight: 600;
  color: var(--fg);
  background: rgba(var(--fg-rgb), 0.07);
  border: 1px dashed rgba(var(--fg-rgb), 0.3);
  border-radius: var(--r-chip);
  transition: transform var(--dur-tap) var(--ease-snap), background 0.15s ease;
}
.cost-add-btn:active { transform: scale(0.98); background: var(--press); }

.cost-total-row {
  flex-shrink: 0;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px solid rgba(var(--fg-rgb), 0.12);
  font-size: 1.05em;
  font-weight: 700;
  color: var(--fg);
}

@media (orientation: landscape) and (min-width: 1024px) and (pointer: fine) {
  .cost-item input { font-size: 14px; }
  .cost-item-amount-wrap { font-size: 14px; }
}

/* Desktop chat type — Grok.com chat messages (measured 2026-08-25):
   body 15px / 22.5px; user bubble same size, padding 8px 16px.
   Composer stays 16px (iOS Safari zoom guard). Thread height comes from
   the quadrant-filling card, not a vh cap. */
@media (orientation: landscape) and (min-width: 1024px) and (pointer: fine) {
  /* px (not em) so the card's font-size: 15px does not shrink the title. */
  .ai-menu-title-route {
    font-size: 24px;
    font-weight: 600;
    letter-spacing: -0.02em;
    line-height: 1.2;
    color: var(--fg2);
  }
  .ai-menu-title-meta {
    font-size: 13px;
  }
  .ai-msg {
    font-size: 15px;
    line-height: 22.5px;
    max-width: 100%;
  }
  .ai-msg-user {
    font-size: 15px;
    line-height: 22.5px;
    padding: 8px 16px;
  }
  #ai-menu-hint { font-size: 14px; line-height: 20px; }
  .ai-history-item { font-size: 14px; }
  .ai-history-item-time { font-size: 13px; }
  #ai-menu-input {
    font-size: 16px;
    line-height: 24px;
    padding: 6px 12px 6px 8px;
    height: calc(24px + 12px);   /* = 36px, matching #ai-menu-mic below */
  }
  #ai-menu-input::placeholder { font-size: 16px; }
  #ai-menu-mic {
    width: 36px;
    height: 36px;
  }
}

/* Portrait phone: keep the composer from eating the fixed card. */
@media (orientation: portrait) and (max-width: 819px) {
  #ai-menu-input {
    max-height: 4.2em;           /* ~3 lines */
  }
}

.globe-menu-section-label {
  font-size: 0.78em;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: rgba(var(--fg-rgb), 0.45);
  margin: 14px 4px 8px;
}
.globe-menu-trips {
  display: flex;
  flex-direction: column;
  gap: 10px;                         /* reservations never touch each other */
}
.globe-menu-trip {
  display: flex;
  align-items: center;
  gap: 0.7em;
  width: 100%;
  text-align: left;
  font-size: 1em;
  color: var(--fg);
  border-radius: var(--r-chip);
  padding: 0.85em 1em;
  background: var(--glass-bg);
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
  border: 1px solid var(--glass-border);
  box-shadow: none;
  transition: transform var(--dur-tap) var(--ease-snap), background 0.15s ease;
}
.globe-menu-trip:active { background: var(--press); transform: scale(0.98); }
.globe-menu-trip-icon {
  flex-shrink: 0;
  width: 1.55em;
  height: 1.55em;
  color: rgba(var(--fg-rgb), 0.92);
}
.globe-menu-trip-icon svg { width: 1.55em; height: 1.55em; display: block; }
/* Destination flag: a round color flag filling the circle */
.trip-flag-bubble {
  display: block;
  width: 1.55em;
  height: 1.55em;
  border-radius: 50%;
  overflow: hidden;
  box-shadow: 0 0 0 1px rgba(var(--fg-rgb), 0.18);
}
.trip-flag-img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.globe-menu-trip-text {
  flex: 1;
  min-width: 0;
  font-weight: 500;
  line-height: 1.3;
}
.globe-menu-trips-empty {
  font-size: 0.9em;
  color: rgba(var(--fg-rgb), 0.45);
  text-align: center;
  padding: 14px 8px;
}

@media (prefers-reduced-motion: reduce) {
  .globe-menu-card { animation: none; }
}

/* Anything with the "hidden" attribute is truly hidden, even if styled here */
[hidden] { display: none !important; }

/* ---------- BOTTOM HALF: calendar ---------- */
#calendar {
  /* Bottom inset: light clear of the iPhone home indicator so the last week
     never sits under it. Kept tight on purpose so the last visible week-row
     sits fully above the Safari URL bar (not half-cropped with a fat black
     band under it). Falls back to 2px where there's no safe-area. Top padding
     keeps the day-view back button fully inside this half (never under the
     globe seam). Landscape bumps padding-top further above. */
  padding: 14px 16px calc(2px + env(safe-area-inset-bottom, 0px) / 3);
  display: flex;
  flex-direction: column;
  touch-action: pan-y;               /* vertical pan only — no sideways scroll gesture */
  box-sizing: border-box;
}

/* ---------- THE LOGIN / SIGN-IN OVERLAY ----------
   Full-window frosted scrim over the live app (guest paywall / sign-in).
   Blur sits on #auth-view only — #auth-card stays sharp glass on top. */
#auth-view {
  position: fixed;
  inset: 0;
  z-index: 80;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  background: rgba(0, 0, 0, 0.55);
  -webkit-backdrop-filter: blur(20px) saturate(1.15);
  backdrop-filter: blur(20px) saturate(1.15);
  padding: max(20px, env(safe-area-inset-top)) 20px max(20px, env(safe-area-inset-bottom));
  overflow: auto;
  box-sizing: border-box;
}
#auth-view[hidden] { display: none !important; }
#auth-card {
  width: 100%;
  max-width: 340px;
  margin: 0;
  padding: 14px 20px;
  border-radius: var(--r-card);
  background: var(--glass-bg);
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
  border: 1px solid var(--glass-border);
  box-shadow: var(--glass-shadow);
  animation: pop-in 0.4s var(--ease-out) both;
  flex-shrink: 0;
}
#auth-title {
  margin: 0 0 1px;
  font-size: 17px;
  line-height: 1.2;
  font-weight: 700;
  letter-spacing: -0.02em;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
}
#auth-logo {
  width: 18px;
  height: 18px;
  flex: none;
}
#auth-sub {
  margin: 0 0 6px;
  font-size: 12px;
  line-height: 1.2;
  opacity: 0.72;
}
#google-signin[hidden], #auth-divider[hidden] { display: none !important; }
#google-signin {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  padding: 8px 16px;
  border-radius: var(--r-btn);
  font-size: 14px;
  line-height: 1.2;
  font-weight: 600;
  color: var(--fg);
  cursor: pointer;
  background: var(--glass-bg);
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
  border: 1px solid var(--glass-border);
  box-shadow: var(--glass-shadow);
  transition: transform 0.15s var(--ease-out), background 0.15s;
}
#google-signin:active { transform: scale(0.97); }
#google-signin svg { display: block; flex: none; width: 18px; height: 18px; }
#auth-inapp-hint {
  margin: 0 0 8px;
  font-size: 12px;
  line-height: 1.35;
  color: rgba(var(--fg-rgb), 0.72);
  text-align: center;
}
#auth-copy-link {
  width: 100%;
  margin: 0 0 8px;
  padding: 8px 16px;
  border-radius: var(--r-btn);
  font-size: 13px;
  font-weight: 600;
  color: var(--fg);
  cursor: pointer;
  background: var(--glass-bg);
  border: 1px solid var(--glass-border);
  box-shadow: var(--glass-shadow);
}
#auth-later {
  display: block;
  width: 100%;
  margin: 10px 0 0;
  padding: 6px;
  border: none;
  background: none;
  font-size: 13px;
  line-height: 1.2;
  color: rgba(var(--fg-rgb), 0.5);
  cursor: pointer;
}
#auth-later:active { color: rgba(var(--fg-rgb), 0.75); }

/* The email/password form sitting above the Google button */
#auth-form { display: flex; flex-direction: column; text-align: left; }
#auth-form label {
  font-size: 11px;
  line-height: 1.2;
  color: rgba(var(--fg-rgb), 0.5);
  margin-top: 4px;
  margin-bottom: 1px;
}
#auth-form label:first-of-type { margin-top: 0; }
#auth-form input {
  background: rgba(var(--fg-rgb), 0.07);
  border: 1px solid rgba(var(--fg-rgb), 0.10);
  border-radius: var(--r-btn);
  padding: 6px 12px 6px 14px;
  font-size: 16px;                 /* 16px stops iPhone Safari zooming on focus */
  line-height: 1.2;
  color: var(--fg);
}
#auth-form input:focus {
  outline: none;
  background: rgba(var(--fg-rgb), 0.11);
  box-shadow: 0 0 0 3px rgba(var(--fg-rgb), 0.14);
}
#auth-error {
  margin: 5px 0 0;
  font-size: 12px;
  line-height: 1.25;
  color: #ff8a8a;
  text-align: left;
}
#auth-submit { width: 100%; margin-top: 8px; padding: 9px; }
#auth-toggle { margin: 6px 0 0; text-align: center; }
#auth-toggle-btn {
  font-size: 12px;
  line-height: 1.2;
  color: rgba(var(--fg-rgb), 0.55);
  background: none;
  border: none;
  padding: 3px;
}
#auth-toggle-btn span { color: var(--fg); font-weight: 600; }
#auth-divider {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 6px 0 6px;
  color: rgba(var(--fg-rgb), 0.4);
  font-size: 11px;
  line-height: 1.2;
}
#auth-divider::before, #auth-divider::after {
  content: '';
  flex: 1;
  height: 1px;
  background: rgba(var(--fg-rgb), 0.12);
}
/* The luxurious exit: card lifts and fades as the calendar arrives. */
#auth-view.signing-out {
  animation: auth-out 0.22s var(--ease-out) both;
  pointer-events: none;
}
@keyframes auth-out {
  from { opacity: 1; transform: none; }
  to   { opacity: 0; transform: scale(1.04) translateY(-10px); }
}
@media (prefers-reduced-motion: reduce) {
  #auth-card { animation: none; }
  #auth-view.signing-out { animation: none; opacity: 0; }
}
/* Roomier screens only (tablets + desktops). BOTH width AND height must be
   roomy — width alone is not enough.

   Why: a landscape iPhone Pro/Max is WIDER than 768px (~844–932) but still
   only ~390–430px TALL. The login card lives in the right half and must use
   the compact sizes above so every field stays fully on screen. The old
   query was `@media (min-width: 768px), …` which matched those phones by
   width alone and blew the card up past the short landscape height (bottom
   of the box clipped). Requiring min-height: 500px keeps phones compact in
   either orientation while still giving iPads/desktops the roomier look. */
@media (min-width: 768px) and (min-height: 500px) {
  #auth-card { padding: 28px 24px; }
  #auth-title { margin-bottom: 6px; font-size: 26px; gap: 8px; }
  #auth-logo { width: 28px; height: 28px; }
  #auth-sub { margin-bottom: 22px; font-size: 14px; }
  #auth-form label { margin-top: 12px; margin-bottom: 4px; }
  #auth-form input { padding: 11px 12px 11px 14px; }
  #auth-error { margin-top: 10px; }
  #auth-submit { margin-top: 16px; }
  #auth-toggle { margin-top: 14px; }
  #auth-divider { margin: 18px 0 14px; }
  #google-signin { padding: 13px 16px; }
}
/* Desktop paywall: a bit roomier card (phone stays compact). */
@media (orientation: landscape) and (min-width: 1024px) and (pointer: fine) {
  #auth-card { max-width: 380px; padding: 28px 28px; }
  #auth-title { font-size: 22px; }
  #auth-sub { font-size: 14px; }
}
/* Both the month grid and the single-day view fill the bottom half. No
   overflow property here on purpose: #calendar (the parent) already has
   overflow:hidden on both axes, which is all the horizontal containment
   this needs. Setting overflow-x:hidden here too (with overflow-y left
   unset) used to trip the CSS spec rule that turns a lone non-visible axis'
   sibling into overflow-y:auto — that made THIS box itself a second, tighter
   clipping container (top inset ~28px) that cropped the day title's -20px
   lift and the back button's alignment nudge, a few px above that line. */
.cal-view {
  flex: 1;
  min-height: 0;
  min-width: 0;
  max-width: 100%;
  display: flex;
  flex-direction: column;
}

/* Header rows: a title in the middle, buttons on the sides */
#calendar-header {
  display: flex;
  align-items: center;
  justify-content: center;
  margin-bottom: 18px;          /* breathing room between the month title and the weekday row */
}
#day-header {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 8px;
  margin-bottom: 10px;
  flex-shrink: 0;
  min-height: calc(var(--globe-btn) + 6px);   /* room for the back bubble (matches the menu button) + a hair of air */
}
/* The back button used to be lifted OUT of the row's flow (position:
   absolute, left:0) so the title group could sit dead-centre across the
   FULL header width. That centering math never accounted for the back
   button's own footprint, so on any header narrower than roughly title-
   group-width + 2×back-button-width, the centred day-prev bubble was
   computed to land right on top of the back button — confirmed live at a
   ~450px-wide day column, the exact width the desktop three-panel layout
   produces at plenty of ordinary window sizes, not just some extreme edge
   case. Keeping the back button IN normal flex flow instead makes
   #day-header-center's flex:1 resolve against the space actually left over
   after it, so the two can never overlap — the title group centers within
   its own remaining track, not the header's full width. */
#day-header-center {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
}
/* The one calendar type-scale, locked to the iPhone-portrait proportion and used
   at EVERY screen size: the month/day TITLE is 18px and the calendar day NUMBER is
   title ÷ 1.44 (≈12.5px) — the exact ratio that looks right on the phone. Both the
   title and the day cells read from these, so the proportion never drifts. */
:root {
  --cal-title-fs: 18px;
  --cal-ratio: 1.44;
  --cal-day-fs: calc(var(--cal-title-fs) / var(--cal-ratio));
}
#calendar-title {
  font-size: var(--cal-title-fs);
  font-weight: 600;
  text-align: center;
}
#day-title {
  min-width: 0;
  font-size: var(--cal-title-fs);   /* same size as the month title (#calendar-title) */
  font-weight: 600;
  text-align: center;
  white-space: nowrap;
  letter-spacing: -0.02em;
}
@media (orientation: landscape) and (pointer: fine) {
  /* Desktop: pin the title to a 300px BASIS so the ‹ › buttons don't shuffle
     as the day text changes length, and give them a little more breathing
     room — but allow it to actually shrink (0 1 300px, not 0 0 300px) below
     that once the day column itself gets narrow, so the title gives up room
     before it ever forces the nav bubbles to overflow/collide with anything
     beside them. */
  #day-header-center { gap: 18px; }
  #day-title {
    flex: 0 1 300px;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  /* Desktop month/year title: a bigger, more prominent heading (the way native
     desktop calendars do it — title is the anchor, not body-sized), with more
     whitespace between it and the weekday + grid below. Only the TITLE grows;
     the day-number scale stays as is, so the cells don't change. */
  #calendar-title { font-size: 23px; letter-spacing: -0.01em; }
  #calendar-header { margin-bottom: 48px; }
}
/* The ‹ back button — a small glass bubble, icon dead center */
#back-to-month, #detail-back {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  padding: 0;
  border-radius: var(--r-btn);
  color: var(--fg);
  flex-shrink: 0;
  transition: transform var(--dur-tap) var(--ease-snap), background 0.15s ease;
}
/* Day view only: the back button matches the menu bubble's size exactly
   (--globe-btn), on every screen — #detail-back keeps the smaller 34px above. */
#back-to-month {
  width: var(--globe-btn);
  height: var(--globe-btn);
}
#back-to-month svg, #detail-back svg {
  display: block;
  flex-shrink: 0;
}
#back-to-month:active, #detail-back:active { background: var(--press); transform: scale(0.94); }
/* Desktop only: tiny glass bubbles to step backward/forward one day */
.day-nav-bubble {
  display: none;
  flex-shrink: 0;
  width: 30px;
  height: 30px;
  border-radius: var(--r-btn);
  align-items: center;
  justify-content: center;
  color: rgba(var(--fg-rgb), 0.85);
  box-shadow: none;
  transition: transform var(--dur-tap) var(--ease-snap), background 0.15s ease;
}
.day-nav-bubble:active:not(:disabled) { background: var(--press); transform: scale(0.94); }
/* The prev/next day bubbles are a MOUSE convenience only — on touch you swipe
   left/right instead, so they never appear on a touch device. */
@media (orientation: landscape) and (pointer: fine) {
  .day-nav-bubble { display: flex; }
}
button:disabled { opacity: 0.25; }

/* ---------- DAY VIEW: three columns — Morning / Afternoon / Evening ----------
   No clock numbers; an item sits vertically by its time within its column
   (e.g. an 11 AM event lands near the bottom of Morning). */
/* #day-body is the positioning context for #day-stage (the day on screen) and,
   only while a drag/turn is happening, its "ghost" sibling — the neighboring
   day's real content, dragged in from off-screen. See the day-turn JS. */
#day-body { flex: 1; min-height: 0; overflow: hidden; position: relative; }
/* Both panes stay on their OWN GPU layer permanently (translateZ + will-change),
   rather than only while a turn is in progress: toggling will-change on right
   as the slide starts works fine in Chrome (which promotes the layer eagerly),
   but on real iPhone Safari the layer-creation cost lands on the animation's
   own first frame(s), which is what actually reads as "stuttery" there even
   though desktop Chrome never shows it. A layer this small (one screen of day
   cards) costs nothing to keep composited all the time, and it removes the
   promotion-timing race entirely instead of trying to win it. */
#day-stage, .day-stage-ghost {
  position: absolute;
  inset: 0;
  height: 100%;
  width: 100%;
  -webkit-backface-visibility: hidden;
  backface-visibility: hidden;
  will-change: transform;
}
#day-stage {
  transform: translate3d(0, 0, 0);
  transition: transform var(--dur-day-turn) var(--ease-snap);
}
/* The ghost is purely visual — real content, dragged into view, but never
   itself interactive (its items get their real click handlers only once
   they become the actual #day-stage, after the turn commits). */
.day-stage-ghost { pointer-events: none; }
/* SWIPE PERFORMANCE (phones): while a day is turning, the glass cards drop their
   backdrop-blur (both panes' own GPU layer is permanent — see above). Recomputing
   many blurs against a moving layer every frame is what makes the swipe jitter on
   real iPhones; the blur returns the instant the turn settles and is imperceptible
   during the brief slide. This rule already covers both panes: .day-stage-ghost
   is a #day-body child too, same as #day-stage. */
#day-body.day-turning .item {
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}
/* THE DAY, AS ONE FLOWING GRID (test layout — replaced Morning/Afternoon/Evening).
   Cards run left-to-right in time order and wrap onto the next row, so reading
   order is simply the order the day happens in. Three across keeps each card
   exactly the size it was when it filled one of three columns.
   align-content: start stops a half-empty day from stretching its rows apart;
   the whole grid scrolls as one, rather than three lanes scrolling separately. */
.day-flow {
  display: grid;
  /* minmax(0, 1fr), NOT 1fr: a plain 1fr column will not shrink below its own
     content's minimum width, so one long unbreakable title ("Sultanhan Hotel -
     Special Class") made its column bulge and squeezed the other two. On a
     phone that came out as 172px next to two 99px cards. The 0 minimum is what
     forces all three to stay equal — the flex layout this replaced did the same
     job with min-width: 0 on each column. */
  grid-template-columns: repeat(3, minmax(0, 1fr));
  /* Row height, EXPLICITLY set by sizeDayItems (see app.js) instead of left as
     "auto". A plain "auto" row track sizes itself from each item's own
     intrinsic content height, computed BEFORE that item's aspect-ratio: 1 is
     resolved against its final (stretched) column width — Chrome's grid
     track-sizing pass and the aspect-ratio box-sizing pass disagree here, so
     "auto" rows measured out ~30-40px SHORTER than the 181px square a card
     actually renders at. The cards themselves stayed perfect squares (nothing
     got squashed), they just spilled downward past their own row's boundary
     into the row below — on a light day with 1-2 rows this was invisible;
     the moment a busy day (a flight, an arrival, a hotel check-in, a
     restaurant idea, a second hotel idea, a "Ideas in <city>" card, an
     affiliate box) actually filled three-plus rows, each row bled into the
     next. var(--day-h, auto) falls back to plain "auto" the instant
     JavaScript hasn't run yet (first paint, before requestAnimationFrame),
     so the grid is never left with an invalid track value. */
  grid-auto-rows: var(--day-h, auto);
  gap: 10px;
  height: 100%;
  align-content: start;
  padding-top: 4px;
  overflow-y: auto;
  overflow-x: hidden;               /* vertical only — text wraps, never pan sideways */
  scrollbar-width: none;
}
.day-flow::-webkit-scrollbar { display: none; }

/* A saved entry. Fills the column width and is ALWAYS a PERFECT SQUARE
   (aspect-ratio 1). Extra events scroll inside the column — we never squash
   the boxes into shorter rectangles to fit more on screen. */
.item {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: flex-start;
  gap: 0;                     /* time→title spacing comes from .item-time margin-bottom */
  width: 100%;
  aspect-ratio: 1;            /* always a perfect square */
  height: auto;               /* height comes from aspect-ratio + width — never compressed */
  flex: none;                 /* don't let the column's flex stretch/squash the box */
  padding: 14px 14px 7px;     /* bottom space = HALF the top space */
  border-radius: 16px;
  font-size: 13px;
  cursor: pointer;
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
  overflow: hidden;
  transition: transform var(--dur-tap) var(--ease-snap), background 0.15s ease,
              box-shadow 0.22s var(--ease-out), border-color 0.15s ease;
}
.item:active { background: var(--press); transform: scale(0.97); }
/* The card's third of the three-way pairing: hovering a card lights its pin on the map
   and its line in the open budget panel; hovering either of THOSE lights the card back
   (.item-pin-hover, set from highlightLinkedEntry). A slim glowing ring + a soft lift —
   quiet enough to sit on a photo card, definite enough to read as "this is the same
   thing" the instant your eye jumps from the globe or the budget to here. Hover only —
   a phone has no pointer to hover with, and tapping already opens the entry. */
@media (hover: hover) {
  .item:hover { background: var(--press); }
}
.item.item-pin-hover {
  background: var(--press);
  box-shadow: inset 0 0 0 1.5px rgba(var(--fg-rgb), 0.5), 0 10px 26px rgba(0, 0, 0, 0.45);
  transform: translateY(-2px);
}
/* THE ENTRY THAT JUST LANDED. Adding a reservation used to leave you scanning a
   month of small cells for one new icon; the day now opens on it, and this is
   what puts your eye in the right place when it does. A white ring that pulses
   twice and then holds, until markJustAddedReservation takes the class back off
   a few seconds later — loud enough to find instantly, quiet enough that it
   never looks like a permanent property of the reservation. Monochrome like
   everything outside the globe. */
.item.item-just-added {
  background: var(--press);
  animation: item-just-added 1.1s var(--ease-out) 2 forwards;
}
@keyframes item-just-added {
  0%   { box-shadow: 0 0 0 0 rgba(var(--fg-rgb), 0.55), 0 0 0 0 rgba(var(--fg-rgb), 0); }
  45%  { box-shadow: 0 0 0 2px rgba(var(--fg-rgb), 0.88), 0 0 18px 4px rgba(var(--fg-rgb), 0.22); }
  100% { box-shadow: 0 0 0 1.5px rgba(var(--fg-rgb), 0.5), 0 0 0 0 rgba(var(--fg-rgb), 0); }
}
/* Still unmistakably marked without the pulse, for anyone who asked for less motion. */
@media (prefers-reduced-motion: reduce) {
  .item.item-just-added {
    animation: none;
    box-shadow: 0 0 0 2px rgba(var(--fg-rgb), 0.75);
  }
}

/* ---- Drag-and-drop rescheduling — TENTATIVE reservations only ----
   A confirmed reservation's .item never gets this class (see buildDayGrid /
   reservationIsDraggable), so none of this ever applies to a real booking.
   touch-action: none stops the browser starting its own scroll gesture from
   a draggable card the instant a finger lands on it — necessary so our own
   press-and-hold timer (attachTentativeDrag) gets to decide whether this
   touch is a drag, not a race against native panning. A phone can still
   scroll the day from any confirmed card, the gaps between cards, or the day
   header — a draggable card is a small enough fraction of a real itinerary
   that this is a fair trade for reliable dragging. */
.item-draggable {
  touch-action: none;
}
@media (hover: hover) {
  .item-draggable:hover { cursor: grab; }
}
/* The original card, left in its slot while its ghost follows the pointer —
   dimmed almost to nothing rather than hidden outright, so the grid doesn't
   visibly reflow (and everything else jump) the instant a drag starts. */
.item.item-dragging {
  opacity: 0.25;
}
/* The floating copy that actually follows the pointer. Fixed positioning +
   a translate transform (set from JS on every pointermove) is what lets it
   track the finger smoothly without ever touching layout — the grid
   underneath never has to reflow while a drag is in progress. */
.item-drag-ghost {
  position: fixed;
  z-index: 500;
  pointer-events: none;
  cursor: grabbing;
  transform-origin: center;
  box-shadow: 0 22px 44px rgba(0, 0, 0, 0.5), inset 0 0 0 1.5px rgba(var(--fg-rgb), 0.4);
  opacity: 0.96;
}
/* Whichever OTHER card the drag is currently hovering over — the one the
   dropped card would land next to. A simple highlighted ring reads clearly
   in a grid that wraps left-to-right then down, where a directional
   before/after line would be ambiguous the moment a drag crosses a row. */
.item.item-drop-target {
  box-shadow: inset 0 0 0 2px rgba(var(--fg-rgb), 0.85), 0 0 16px 2px rgba(var(--fg-rgb), 0.18);
}

.item-time {
  flex-shrink: 0;
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.06em;
  color: rgba(var(--fg-rgb), 0.55);
  font-variant-numeric: tabular-nums;
  line-height: 1.2;
  /* The gap between the time and the title: ~80% of the time's OWN font size, so it
     scales correctly on every screen. The content then fills downward from here. */
  margin-bottom: 0.8em;
}
.item-row {
  display: flex;
  align-items: flex-start;    /* content fills DOWN from the gap (top-aligned) */
  flex: 1;
  width: 100%;
  min-width: 0;
  min-height: 0;
  gap: 7px;
}
.item-icon { flex-shrink: 0; display: inline-flex; color: rgba(var(--fg-rgb), 0.9); }
.item-icon svg { width: 16px; height: 16px; display: block; }
.item-main {
  display: flex;
  flex-direction: column;
  justify-content: flex-start;   /* lines stack from the top, filling down */
  gap: 2px;
  flex: 1;
  min-width: 0;
  overflow: hidden;
}
/* Title (the line right after the time): SAME size for EVERY event type. It's set
   explicitly here — and at each screen size below — so it can never drift between
   a flight, a restaurant, a tour, etc. */
.item-primary {
  font-size: 13px;
  font-weight: 600;
  line-height: 1.2;
  min-height: 1.2em;
  max-width: 100%;
  overflow-wrap: break-word;
  word-break: normal;
}
.item-detail {
  font-size: 11px;
  font-weight: 500;
  color: rgba(var(--fg-rgb), 0.72);
  line-height: 1.3;
  min-height: 1.3em;
  max-width: 100%;
  overflow-wrap: break-word;
  word-break: normal;
}

/* A card with a city photo (see decorateDayItemWithCityPhoto/applyItemCityImage,
   app.js) — the photo shows FULL, undimmed color across the whole card (no
   dark wash over it — dimming a photo just to put text on it defeats the
   point of showing the photo at all). On top of it: ONLY the kind icon and
   the time, together in one small rounded chip, top-left — no title, no
   detail line, nothing else crowding the picture. The rest of what the
   card used to show inline is one tap away already (the card's own click
   handler opens the full detail view); this chip is a label on the photo,
   not a second copy of that detail view. */
.item.item-has-photo {
  position: relative;
  padding: 0;
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  border-color: rgba(var(--fg-rgb), 0.16);
}
/* .item-has-photo alone paints INSTANTLY, no animation — this is the common
   case once warmAdjacentDayImages/prefetchTripCityImages (app.js) have had a
   moment to preload the actual pixels, not just the URL. Every day swipe
   tears the day's cards down and rebuilds fresh ones (see buildDayGrid), so
   animating unconditionally here used to replay this fade on EVERY card, on
   EVERY swipe, whether or not anything was actually new — which is exactly
   what read as flashing on a fast swipe through several days.
   .item-photo-fresh is the one true exception: added only when THIS card
   itself had to wait on the image loading (applyItemCityImage's `fresh`
   flag) — a real first-time reveal earns a considered fade; an already-warm
   repaint never plays one. */
.item.item-has-photo.item-photo-fresh {
  animation: item-photo-in 0.5s var(--ease-out);
}
@keyframes item-photo-in {
  from { filter: opacity(0); transform: scale(1.015); }
  to   { filter: opacity(1); transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  .item.item-has-photo.item-photo-fresh { animation: none; }
}
/* .item-row (the title/detail block, .item-main, lives inside it) has
   nothing left to show on a photo card — see applyItemCityImage, which
   already lifted .item-icon out of it and into .item-chip below. */
.item.item-has-photo .item-row { display: none; }
/* Icon + time, together — built by applyItemCityImage (app.js) by moving
   the existing .item-icon and .item-time into one shared wrapper, so a
   photoless card's own layout (which uses those same two elements, just
   arranged differently) never has to know this mode exists. */
.item.item-has-photo .item-chip {
  position: absolute;
  top: 8px;
  left: 8px;
  z-index: 2;
  display: flex;
  align-items: center;
  gap: 5px;
  padding: 3px 8px 3px 5px;
  background: rgba(var(--card-bg-rgb), 0.68);
  border: 1px solid rgba(var(--fg-rgb), 0.22);
  border-radius: 999px;
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  font-size: 10px;
}
.item.item-has-photo .item-chip .item-icon {
  color: var(--fg);
}
.item.item-has-photo .item-chip .item-icon svg { width: 11px; height: 11px; display: block; }
.item.item-has-photo .item-chip .item-time {
  margin: 0;
  padding: 0;
  /* No font-size here on purpose — inherits from .item-chip, which is what
     the breakpoint overrides below actually scale. */
  font-weight: 600;
  color: var(--fg);
}

/* Touch devices: the card is a perfect square (height = width, set on .item above),
   so its size simply follows the column width. These rules only set the padding,
   gaps, text and icon sizes for touch. A packed day scrolls (see .day-flow). */
@media (pointer: coarse) {
  #day-body .item {
    padding: 14px 14px 7px;
    gap: 0;
    font-size: 13px;
    border-radius: 16px;
  }
  #day-body .item-time { font-size: 11px; }
  #day-body .item-row { gap: 7px; }
  #day-body .item-main { gap: 2px; }
  #day-body .item-primary { font-size: 13px; }
  #day-body .item-detail { font-size: 11px; }
  #day-body .item-icon svg { width: 16px; height: 16px; }
  #day-body .item.item-has-photo .item-chip { font-size: 10px; }
  #day-body .item.item-has-photo .item-chip .item-icon svg { width: 11px; height: 11px; }
}

/* iPad PORTRAIT (a TOUCH device wider than a phone): the cards' padding/text sit
   between the iPhone and the desktop. The square's size follows the column width
   (aspect-ratio on .item); a packed column scrolls. min-width 768px keeps every
   iPhone out.

   PORTRAIT is load-bearing, not decoration. "Wider than a phone" was a fair
   reading of a wide viewport when a tablet in landscape gave the day view the
   whole screen. It does not hold any more: landscape on a coarse device is the
   rail layout, where the day view sits in one column beside the AI panel and the
   rail — 451px at 1024, narrower than the phone this step-up exists to be bigger
   than. So the step-up is for tablets held UPRIGHT, where the pane really is the
   full width; in landscape the cards keep the compact sizing above, which is the
   size that suits the box they are actually in. */
@media (pointer: coarse) and (min-width: 768px) and (orientation: portrait) {
  #day-body .item {
    padding: 18px 18px 9px;
    gap: 0;
    font-size: 16px;
    border-radius: 18px;
  }
  #day-body .item-time { font-size: 12.5px; }
  #day-body .item-row { gap: 9px; }
  #day-body .item-main { gap: 3px; }
  #day-body .item-primary { font-size: 16px; }
  #day-body .item-detail { font-size: 13px; }
  #day-body .item-icon svg { width: 20px; height: 20px; }
  #day-body .item.item-has-photo .item-chip { font-size: 11px; padding: 4px 9px 4px 6px; }
  #day-body .item.item-has-photo .item-chip .item-icon svg { width: 12px; height: 12px; }
}

/* Only a real desktop (a device with a MOUSE) gets the roomy, larger day-event
   cards. Touch devices — phones AND the small iPhone Ultra — keep the clean,
   smaller mobile font sizing (the iPhone 14 Pro Max portrait look) even when the
   layout is side-by-side. */
@media (orientation: landscape) and (pointer: fine) {
  #day-body .item {
    padding: 22px 24px 11px;
    font-size: 19px;
    border-radius: 20px;
    gap: 0;
  }
  #day-body .item-time { font-size: 13px; }
  #day-body .item-icon svg { width: 22px; height: 22px; }
  #day-body .item.item-has-photo .item-chip { font-size: 12px; padding: 4px 10px 4px 6px; }
  #day-body .item.item-has-photo .item-chip .item-icon svg { width: 13px; height: 13px; }
  #day-body .item-primary { font-size: 19px; }
  #day-body .item-detail { font-size: 14px; }
}

/* ---------- DETAIL VIEW: one reservation, opened by TAPPING a day bubble ---------- */
/* ONE header row for EVERY reservation, confirmed or a still-tentative idea
   (setDetailHeader, app.js) — back button, name + one-line detail, and (idea
   only) a "Book" button. A plain sibling of #detail-body, not inside its
   scroll port, so nothing here ever scrolls out of reach, all the way to
   the bottom of a long panel. padding-top gives it real breathing room off
   the map/photo panel sitting directly above it. */
#detail-header {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 14px 0 12px;
  margin-bottom: 10px;
  border-bottom: 1px solid var(--glass-border);
}
#detail-header-title { flex: 1; min-width: 0; }
#detail-header-name {
  font-size: 15px;
  font-weight: 700;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
#detail-header-meta {
  font-size: 12px;
  opacity: 0.65;
  margin-top: 1px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
#detail-header-name:empty, #detail-header-meta:empty { display: none; }
/* A real button, not a plain text link — the header's one primary action, so
   it gets the same "touch brighter fill" .ai-msg-btn already uses for ITS
   primary pick (see that rule's own comment) rather than plain glass, plus
   its reservation's own type glyph beside the label (see setDetailHeader). */
#detail-header-action {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 9px 16px 9px 12px;
  border-radius: var(--r-btn);
  font-size: 14px;
  font-weight: 700;
  color: inherit;
  text-decoration: none;
  cursor: pointer;
  background: rgba(var(--fg-rgb), 0.1);
  box-shadow: 0 1px 0 0 rgba(var(--fg-rgb), 0.07) inset;
  transition: transform var(--dur-tap) var(--ease-snap), background 0.15s ease, box-shadow 0.15s ease;
}
.detail-header-action-icon { display: flex; flex: none; }
.detail-header-action-icon svg { width: 16px; height: 16px; }
@media (pointer: fine) {
  #detail-header-action:hover {
    background: rgba(var(--fg-rgb), 0.16);
    box-shadow: 0 1px 0 0 rgba(var(--fg-rgb), 0.09) inset, 0 10px 20px -10px rgba(0, 0, 0, 0.6);
  }
}
#detail-header-action:active {
  background: var(--press);
  transform: scale(0.96);
  box-shadow: 0 1px 0 0 rgba(var(--fg-rgb), 0.05) inset;
}
@media (orientation: landscape) and (min-width: 1024px) and (pointer: fine) {
  #detail-header-name { font-size: 17px; }
  #detail-header-meta { font-size: 13px; }
  #detail-header-action { font-size: 15px; padding: 10px 18px 10px 14px; }
  .detail-header-action-icon svg { width: 18px; height: 18px; }
}
#detail-body {
  flex: 1;
  min-height: 0;
  min-width: 0;
  overflow-y: auto;
  overflow-x: hidden;              /* vertical only */
  scrollbar-width: none;
  display: flex;
  flex-direction: column;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-y: contain;
  overscroll-behavior-x: none;
}
#detail-body::-webkit-scrollbar { display: none; }

@keyframes photo-shimmer {
  from { background-position: 180% 0; }
  to   { background-position: -80% 0; }
}

/* ---------- A tentative event's inline photo gallery (phone only) ----------
   Built by openIdeaDetail as the first thing in #detail-body, filled by
   showIdeaPhotoStrip. It sits between the header (title + "Book Now") and the
   event's own details, and scrolls VERTICALLY by itself: photos leave off the
   top and arrive from the bottom.

   overscroll-behavior: contain is the rule that makes the ride buttons stay
   put. Without it, flicking past the last photo chains the scroll to
   #detail-body underneath and drags "Start your drive" / "Order an Uber" off
   the screen — which is the one thing this must never do. The gallery is also
   a strictly vertical scroller (overflow-x: hidden), so a left/right swipe
   still reaches detailView's own handler and steps to the next event.

   Height is a share of the screen, not a fixed pixel count, so the details
   and the buttons below always get the rest of it on any phone. */
.idea-photo-strip {
  /* 0 0 auto, never shrink: #detail-body is a scrolling flex column, and a
     shrinkable item here would be squeezed by whatever sits below instead of
     keeping its own scroll port — the same trap .photo-panel-shot documents. */
  flex: 0 0 auto;
  height: 42vh;
  margin-bottom: 12px;
  overflow-y: auto;
  overflow-x: hidden;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  /* Each photo comes to rest at the top of the strip, so a scroll lands on a
     picture rather than halfway between two. */
  scroll-snap-type: y proximity;
  scrollbar-width: none;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.idea-photo-strip[hidden] { display: none; }
.idea-photo-strip::-webkit-scrollbar { display: none; }

/* One photo's slot. Same deal as .photo-panel-shot: it reserves height with
   min-height (never an aspect-ratio box) and the image inside is width: 100% /
   height: auto, so a photo is drawn at its own shape the whole way through its
   download — a browser paints a big photo progressively, long before its load
   event, and a forced ratio makes that look broken for the entire wait. */
.idea-photo-shot {
  position: relative;
  flex: 0 0 auto;
  width: 100%;
  min-height: 180px;
  scroll-snap-align: start;
  border-radius: var(--r-chip);
  overflow: hidden;
  background: linear-gradient(
    100deg,
    var(--glass-bg) 30%,
    color-mix(in srgb, var(--fg2) 9%, var(--glass-bg)) 50%,
    var(--glass-bg) 70%
  );
  background-size: 220% 100%;
  animation: photo-shimmer 1.3s linear infinite;
}
.idea-photo-shot.is-loaded {
  min-height: 0;
  background: none;
  animation: none;
}
/* width/height here are the whole guarantee against a distorted photo, so
   nothing above may set height, object-fit or position on this element. */
.idea-photo-img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: var(--r-chip);
  border: 1px solid var(--glass-border);
  opacity: 1;
  transition: opacity 0.32s var(--ease-out);
}
/* Defaults to VISIBLE — a photo with no .is-loaded yet is merely transparent
   over its own shimmer, so a missed class can never hide a loaded photo. */
.idea-photo-shot:not(.is-loaded) .idea-photo-img { opacity: 0; }
@media (prefers-reduced-motion: reduce) {
  .idea-photo-shot { animation: none; }
  .idea-photo-img { transition: none; }
}

/* Detail sections ease in with a light stagger after the panel opens */
#detail-view:not([hidden]) .res-section-panel,
#detail-view:not([hidden]) .detail-head,
#detail-view:not([hidden]) .detail-rows,
#detail-view:not([hidden]) .detail-nav {
  animation: view-in 0.26s var(--ease-out) both;
}
#detail-view:not([hidden]) .res-section-panel:nth-child(1),
#detail-view:not([hidden]) .detail-head { animation-delay: 0.04s; }
#detail-view:not([hidden]) .res-section-panel:nth-child(2) { animation-delay: 0.07s; }
#detail-view:not([hidden]) .res-section-panel:nth-child(3) { animation-delay: 0.10s; }
#detail-view:not([hidden]) .res-section-panel:nth-child(4) { animation-delay: 0.13s; }
#detail-view:not([hidden]) .detail-nav { animation-delay: 0.12s; }

/* The reservation's headline: big icon + title, small subtitle under it */
.detail-head { display: flex; align-items: center; gap: 12px; margin-bottom: 18px; }
.detail-head .detail-icon { flex-shrink: 0; color: var(--fg); }
.detail-head .detail-icon svg { width: 30px; height: 30px; display: block; }
.detail-title {
  font-size: 24px;
  font-weight: 700;
  line-height: 1.1;
  max-width: 100%;
  min-width: 0;
  overflow-wrap: break-word;
  word-break: normal;
}
.detail-subtitle {
  font-size: 13px;
  color: rgba(var(--fg-rgb), 0.55);
  margin-top: 3px;
  overflow-wrap: break-word;
}

/* The facts of the booking — a quiet label on the left, the value on the right */
.detail-rows { display: flex; flex-direction: column; gap: 2px; margin-bottom: 22px; }
.detail-row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 14px;
  padding: 11px 2px;
  border-bottom: 1px solid rgba(var(--fg-rgb), 0.08);
}
.detail-row .k { font-size: 13px; color: rgba(var(--fg-rgb), 0.5); }
.detail-row .v {
  font-size: 15px;
  font-weight: 500;
  text-align: right;
  font-variant-numeric: tabular-nums;
  min-width: 0;
  flex: 1;
  overflow-wrap: break-word;
  word-break: normal;
}
.detail-row .k { flex-shrink: 0; }

/* The navigation buttons — liquid glass, fully see-through, blurred behind so
   the label reads cleanly over the globe. */
.detail-nav-label {
  font-size: 13px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: rgba(var(--fg-rgb), 0.45);
  margin-bottom: 10px;
}
/* Two bubbles side by side: Start your drive | Order an Uber */
.detail-nav { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: auto; }
.nav-btn {
  min-width: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  padding: 15px;
  border-radius: var(--r-btn);
  color: var(--fg);
  font-size: 16px;
  font-weight: 600;
  text-decoration: none;
  cursor: pointer;
  transition: transform var(--dur-tap) var(--ease-snap), background 0.15s ease;
}
.nav-btn svg { width: 20px; height: 20px; flex-shrink: 0; }
/* "Start your drive" / "Order an Uber" always stay on ONE line */
.nav-btn span { white-space: nowrap; }
.nav-btn:active { transform: scale(0.97); background: var(--press); }

/* ── RESERVATION DETAIL — organized, uniform tiles, tap to expand ── */
.res-section-panel { margin-bottom: 10px; }
.res-section-panel:last-child { margin-bottom: 0; }
/* HEADING tier — top-level section buttons (Airline, Journey, Travelers, etc.).
   Biggest weight so a traveler's eye lands on these first. */
.res-section-panel > .res-acc-head .res-acc-summary {
  font-size: 16px;
  font-weight: 700;
}
.res-section-panel.open > .res-acc-body {
  padding-top: 4px;
}
/* The "at a glance" hero atop a flight's detail view (see buildFlightRouteHero,
   app.js) — airline + flight number as a small caption, then departure and
   arrival as a big boarding-pass-style strip: airport code, time, an arrow
   with the flight's duration underneath it, then the other airport code and
   time. This is the actual answer to "when do I leave, from/to where" —
   the trip-dates tile below it is a date RANGE for the whole booking, not
   a substitute for this. */
.res-route-hero {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 4px 2px 12px;
  border-bottom: 1px solid rgba(var(--fg-rgb), 0.08);
}
.res-route-label {
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: rgba(var(--fg-rgb), 0.55);
}
.res-route-strip {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 8px;
}
.res-route-end {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}
.res-route-strip .res-route-end:last-child { align-items: flex-end; text-align: right; }
.res-route-code {
  font-size: 26px;
  font-weight: 800;
  letter-spacing: 0.01em;
  line-height: 1.1;
}
/* Hotel/transfer hero variant: the big value there is a date or a time
   ("Wed, Nov 4", "7:00 PM") rather than a 3-letter airport code, so it needs
   more room than 26px gives before it wraps — same weight/position, smaller
   size, to keep the same "one big fact per end" hierarchy the flight hero
   set up. */
.res-route-code-sm { font-size: 16px; }
.res-route-time {
  font-size: 13px;
  font-weight: 600;
  color: rgba(var(--fg-rgb), 0.68);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
/* The transfer hero's supporting line — a pickup/drop-off location NAME,
   which (unlike a flight's departure/arrival time) can run long enough to
   need real wrapping instead of .res-route-time's single-line nowrap. */
.res-route-sub {
  display: block;
  font-size: 12px;
  font-weight: 500;
  color: rgba(var(--fg-rgb), 0.62);
  line-height: 1.3;
  margin-top: 1px;
}
.res-route-plusday {
  margin-left: 3px;
  font-size: 10px;
  font-weight: 700;
  vertical-align: super;
  color: rgba(var(--fg-rgb), 0.55);
}
.res-route-mid {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-start;
  gap: 2px;
  padding-top: 2px;
  min-width: 40px;
}
.res-route-arrow {
  font-size: 18px;
  color: rgba(var(--fg-rgb), 0.5);
  line-height: 1;
}
.res-route-dur {
  font-size: 10px;
  color: rgba(var(--fg-rgb), 0.42);
  white-space: nowrap;
}

.res-block { display: flex; flex-direction: column; gap: 8px; }
.res-stack { display: flex; flex-direction: column; gap: 6px; }
.res-tiles {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
}
.res-tile {
  min-height: 72px;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
  gap: 4px;
  padding: 12px 14px;
  border-radius: var(--r-chip);
  text-align: left;
  color: var(--fg);
  text-decoration: none;
  box-shadow: none;
  transition: transform var(--dur-tap) var(--ease-snap), background 0.15s ease, border-color 0.15s ease;
}
.res-tile-wide { grid-column: 1 / -1; min-height: 64px; }
/* Car rental: a wide left column (locations, car class) + a narrow right column
   (the small time/date values), each tile pinned to its column so the rows line
   up even when a value is missing. */
.res-tiles.res-tiles-carsplit { grid-template-columns: 3fr 1fr; }
.res-tiles-carsplit .res-col-l { grid-column: 1; }
.res-tiles-carsplit .res-col-r { grid-column: 2; }
/* Three equal bubbles forced onto one row at every width (col count set inline).
   Values wrap to more lines rather than crop, so nothing is ever cut off. */
/* Airline row: confirmation + trip dates side by side, then a slim icon column.
   Titles top-aligned so they share one visual line. */
.res-airline-row {
  display: grid;
  grid-template-columns: 1.4fr 1fr auto;
  gap: 8px;
  align-items: stretch;
}
/* The two text bubbles: borderless (neither one "highlighted"). Title stays at
   the TOP; the value fills the space below it and sits centered in the middle. */
.res-airline-row > .res-tile { min-width: 0; justify-content: flex-start; border: none; }
.res-airline-row > .res-tile .res-tile-v { flex: 1; display: flex; align-items: center; }
/* Stacked icon buttons (phone over website) filling the row's height. They KEEP
   their outline so it's clear they're tappable; the icon sits dead center. */
.res-icon-col {
  display: grid;
  grid-template-rows: 1fr 1fr;
  gap: 6px;
}
.res-icon-btn {
  min-height: 0;
  width: 44px;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}
/* Same icon buttons, laid out in a row: used where a tile grid has no
   adjacent text tile to stack against (restaurant/car/tour details). */
.res-icon-row {
  display: flex;
  gap: 8px;
}
.res-icon-row .res-icon-btn { height: 44px; }
.res-tile-ic { display: inline-flex; align-items: center; color: var(--fg); }
.res-tile-ic svg { width: 20px; height: 20px; display: block; }
.res-tile-k {
  font-size: 10px;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: rgba(var(--fg-rgb), 0.45);
  line-height: 1.25;
  width: 100%;
  min-width: 0;
}
.res-tile-v {
  font-size: 15px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.03em;
  line-height: 1.25;
  width: 100%;
  min-width: 0;
}
/* The vendor confirmation is the single most important field — sized like a section
   title so the eye lands on it first. (Only this tile; ticket numbers stay normal.) */
.res-tile-conf .res-tile-v { font-size: 16px; font-weight: 700; }
.res-tile-copy { cursor: pointer; width: 100%; border: none; font: inherit; }
/* Plain info tiles (Dates, Check-in, etc. — not buttons or links) drop the glass
   outline so they match the flat Confirmation tile. The glass outline is reserved
   for the tappable tiles. They keep the same dark glass fill and shadow. */
.res-tile:not(.res-tile-copy):not(.res-tile-link) { border: none; }
.res-tile-copy:active, .res-tile-link:active { transform: scale(0.97); background: var(--press); }
.res-tile-copy.copied { border-color: rgba(var(--fg-rgb), 0.4); }
.res-tile-copy.copied .res-tile-v { color: rgba(var(--fg-rgb), 0.75); font-size: 13px; }
.res-tile-link .res-tile-v { text-decoration: underline; text-underline-offset: 3px; text-decoration-color: rgba(var(--fg-rgb), 0.3); }

.res-accordion {
  border-radius: var(--r-chip);
  box-shadow: none;
  flex-shrink: 0;
}
.res-accordion:not(.open) { overflow: hidden; }
.res-acc-head {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 12px 14px;
  border: none;
  background: none;
  color: var(--fg);
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: transform var(--dur-tap) var(--ease-snap), background 0.15s ease;
}
.res-acc-head:active { background: var(--press); transform: scale(0.99); }
/* A plain button living in this same row shape (see appendOpenSourceEmailsRow)
   — it navigates instead of expanding, so centering its one line of text
   reads as a button/CTA rather than a dropdown row waiting for a chevron. */
.res-acc-head-center { justify-content: center; }
.res-acc-head-center .res-acc-summary { flex: none; width: auto; text-align: center; }
/* The one summary that is a fixed LABEL rather than a reservation's value, so
   it keeps its line instead of being wrapped to fit - see
   appendOpenSourceEmailsRow in app.js, which also keeps it out of the text
   fitter (the nowrap alone would not: that fitter writes real line breaks
   into the text, and pre-line honours them whatever this says). */
.res-acc-summary-1line { white-space: nowrap; }
/* SUBHEADING tier — nested accordion buttons ("Itinerary details", traveler names).
   Explicit size so they no longer inherit the oversized 16px browser default and sit
   one clear step below the section headings. */
.res-acc-summary { flex: 1; min-width: 0; width: 100%; font-size: 14px; font-weight: 600; }
.res-acc-chev {
  flex-shrink: 0;
  font-size: 18px;
  color: rgba(var(--fg-rgb), 0.4);
  transition: transform 0.2s ease;
  line-height: 1;
}
.res-accordion.open .res-acc-chev { transform: rotate(90deg); }
.res-acc-body {
  display: grid;
  grid-template-rows: 0fr;
  padding: 0 14px;
  transition: grid-template-rows 0.16s cubic-bezier(0.4, 0, 0.2, 1), padding 0.16s cubic-bezier(0.4, 0, 0.2, 1);
}
.res-accordion.open > .res-acc-body {
  grid-template-rows: 1fr;
  padding: 0 14px 12px;
}
.res-acc-body-inner {
  overflow: hidden;
  min-height: 0;
}
.res-accordion.open > .res-acc-body > .res-acc-body-inner {
  overflow: visible;
}
.res-acc-inner { display: flex; flex-direction: column; gap: 8px; }

.res-leg-sum { display: flex; flex-direction: column; gap: 3px; }
.res-leg-date { font-size: 11px; color: rgba(var(--fg-rgb), 0.45); }
.res-leg-main { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.res-leg-fn { font-size: 16px; font-weight: 700; letter-spacing: 0.02em; }
.res-leg-route { font-size: 14px; font-weight: 500; color: rgba(var(--fg-rgb), 0.82); }
.res-accordion.res-leg-active { border-color: rgba(var(--fg-rgb), 0.28); }
.res-layover {
  font-size: 10px;
  color: rgba(var(--fg-rgb), 0.4);
  text-align: center;
  padding: 2px 0;
}
.res-traveler-flat {
  padding: 12px 14px;
  border-radius: var(--r-chip);
  font-size: 14px;          /* subheading tier — matches a traveler name in an accordion */
  font-weight: 600;
}
.res-detail-line {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  padding: 8px 0;
  border-bottom: 1px solid rgba(var(--fg-rgb), 0.06);
  font-size: 13px;
}
.res-detail-line:last-child { border-bottom: none; }
/* The photo credit a real Openverse license requires (see
   appendCityPhotoCredit, app.js) — small and muted on purpose, the way a
   photo credit reads anywhere else, never competing with the reservation's
   own details above it. */
.res-photo-credit {
  margin-top: 12px;
  font-size: 10px;
  line-height: 1.4;
  color: rgba(var(--fg-rgb), 0.4);
  text-align: center;
}
.res-photo-credit a { color: inherit; text-decoration: underline; }

/* A forwarded-email import's only real copy — plain text, no layout to
   preserve — so unlike .email-reader-frame (app.js's showEmailReaderView,
   the sandboxed iframe for a real html_body) this stays in the app's own
   dark glass look instead of forcing a white background. Small and
   scrollable by default (a quick "yes, this is really what arrived" check);
   .email-reader-text (combined on the same element for the full-panel
   reader) overrides that to fill the reading pane instead. */
.res-email-text-preview {
  display: block;
  width: 100%;
  max-height: 220px;
  overflow-y: auto;
  margin: 0;
  padding: 12px;
  border: 1px solid var(--glass-border);
  border-radius: var(--r-chip);
  background: var(--glass-bg);
  color: rgba(var(--fg-rgb), 0.82);
  font: 12px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;
  white-space: pre-wrap;
  word-break: break-word;
}
.res-detail-k { color: rgba(var(--fg-rgb), 0.48); flex-shrink: 0; }
.res-detail-v {
  font-weight: 500;
  text-align: right;
  font-variant-numeric: tabular-nums;
  min-width: 0;
  flex: 1;
}

/* Legacy detail pieces (non-flight reservations) */
.detail-section { margin-bottom: 16px; }
.detail-section:last-child { margin-bottom: 0; }
.detail-section-title {
  font-size: 11px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: rgba(var(--fg-rgb), 0.42);
  margin-bottom: 8px;
}
.detail-travelers { display: flex; flex-direction: column; gap: 8px; }
.detail-traveler {
  padding: 10px 12px;
  border-radius: 14px;
  border: 1px solid rgba(var(--fg-rgb), 0.1);
  background: rgba(var(--fg-rgb), 0.04);
}
.detail-traveler-name { font-size: 15px; font-weight: 600; }
.detail-traveler-ticket {
  font-size: 12px;
  color: rgba(var(--fg-rgb), 0.5);
  margin-top: 3px;
  font-variant-numeric: tabular-nums;
}
.detail-notice {
  font-size: 12px;
  line-height: 1.45;
  color: rgba(var(--fg-rgb), 0.72);
  padding: 10px 12px;
  border-radius: 12px;
  border: 1px solid rgba(var(--fg-rgb), 0.16);
  background: rgba(var(--fg-rgb), 0.06);
}
.detail-journey { display: flex; flex-direction: column; gap: 8px; }
.detail-leg {
  padding: 11px 12px;
  border-radius: 14px;
  border: 1px solid rgba(var(--fg-rgb), 0.1);
  background: rgba(var(--fg-rgb), 0.04);
}
.detail-leg-date {
  font-size: 11px;
  color: rgba(var(--fg-rgb), 0.48);
  margin-bottom: 4px;
}
.detail-leg-flight { font-size: 15px; font-weight: 600; margin-bottom: 4px; }
.detail-leg-route {
  font-size: 14px;
  font-weight: 500;
  font-variant-numeric: tabular-nums;
}
.detail-leg-cities {
  font-size: 12px;
  color: rgba(var(--fg-rgb), 0.52);
  margin-top: 3px;
}
.detail-leg-meta {
  font-size: 12px;
  color: rgba(var(--fg-rgb), 0.48);
  margin-top: 5px;
}
.detail-layover {
  font-size: 11px;
  color: rgba(var(--fg-rgb), 0.45);
  text-align: center;
  padding: 2px 0;
}
.detail-notes, .detail-airline-tips { display: flex; flex-direction: column; gap: 8px; }
.detail-note, .detail-airline-tips p {
  font-size: 13px;
  line-height: 1.45;
  color: rgba(var(--fg-rgb), 0.68);
}
/* Tappable contact links — phone taps open the dialer, web opens a new tab */
.contact-list { display: flex; flex-direction: column; gap: 8px; }
.contact-btn {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding: 12px 14px;
  border-radius: var(--r-chip);
  color: var(--fg);
  text-decoration: none;
  transition: transform var(--dur-tap) var(--ease-snap), background 0.15s ease;
  box-shadow: none;
}
.contact-btn:active { transform: scale(0.97); background: var(--press); }
.contact-btn-icon { flex-shrink: 0; margin-top: 2px; color: rgba(var(--fg-rgb), 0.9); }
.contact-btn-icon svg { width: 18px; height: 18px; display: block; }
.contact-btn-body { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.contact-btn-label { font-size: 14px; font-weight: 600; }
.contact-btn-value {
  font-size: 15px;
  font-weight: 500;
  color: rgba(var(--fg-rgb), 0.92);
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-color: rgba(var(--fg-rgb), 0.35);
}
.contact-btn-hint { font-size: 11px; color: rgba(var(--fg-rgb), 0.48); margin-top: 2px; }

/* Copyable reference codes — tap to copy, brief "Copied" flash */
.copy-bubble-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  gap: 8px;
}
.copy-bubble-wrap { display: flex; flex-direction: column; gap: 5px; min-width: 0; }
.copy-bubble-label {
  font-size: 10px;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: rgba(var(--fg-rgb), 0.45);
  line-height: 1.3;
}
.copy-bubble {
  width: 100%;
  text-align: center;
  padding: 11px 10px;
  border-radius: 14px;
  font-size: 15px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.06em;
  color: var(--fg);
  cursor: pointer;
  box-shadow: none;
  transition: transform var(--dur-tap) var(--ease-snap), background 0.15s ease, border-color 0.15s ease;
}
.copy-bubble:active { transform: scale(0.96); background: var(--press); }
.copy-bubble.copied {
  border-color: rgba(var(--fg-rgb), 0.45);
  background: rgba(var(--fg-rgb), 0.12);
  letter-spacing: 0.04em;
  font-size: 13px;
}
.detail-leg .copy-bubble-wrap { margin-top: 8px; }
.detail-leg .copy-bubble { font-size: 13px; padding: 8px 10px; }

/* Compact detail typography so more of a booking fits on screen when you tap a
   reservation (detail view only). Applies to ANY touch device — in BOTH
   orientations — plus any portrait window, so phones never get the big desktop
   detail fonts. Only a real desktop (mouse) in landscape keeps the larger sizes. */
@media (orientation: portrait), (pointer: coarse) {
  /* (Day-header centering now lives in the base rules — applies to every layout.) */
  #detail-view #detail-header { margin-bottom: 6px; }
  #detail-view #detail-body.detail-simple {
    overflow: hidden;
    justify-content: space-between;
  }
  #detail-view #detail-body.detail-rich {
    overflow-y: auto;
    overflow-x: hidden;
    justify-content: flex-start;
    -webkit-overflow-scrolling: touch;
    padding-bottom: 16px;
  }
  #detail-view .res-block,
  #detail-view .res-stack,
  #detail-view .res-acc-inner { min-width: 0; }
  #detail-view .res-leg-main { flex-wrap: wrap; }
  #detail-view .res-section-panel .res-stack { gap: 6px; }
  #detail-view .detail-head { gap: 8px; margin-bottom: 8px; }
  #detail-view .detail-head .detail-icon svg { width: 20px; height: 20px; }
  #detail-view .detail-title { font-size: 16px; }
  #detail-view .detail-subtitle { font-size: 10px; margin-top: 2px; }
  #detail-view .detail-rows { gap: 0; margin-bottom: 8px; flex-shrink: 1; min-height: 0; }
  #detail-view .detail-row { padding: 5px 2px; }
  #detail-view .detail-row .k { font-size: 10px; }
  #detail-view .detail-row .v { font-size: 12px; }
  #detail-view .detail-nav-label { font-size: 9px; margin-bottom: 5px; }
  #detail-view .detail-nav { gap: 6px; margin-top: 0; flex-shrink: 0; }
  #detail-view .nav-btn { padding: 10px 12px; font-size: 13px; gap: 7px; }
  #detail-view .nav-btn svg { width: 16px; height: 16px; }
  #detail-view .detail-section { margin-bottom: 10px; }
  #detail-view .detail-section-title { font-size: 9px; margin-bottom: 5px; }
  #detail-view .detail-traveler-name { font-size: 13px; }
  #detail-view .detail-traveler-ticket { font-size: 10px; }
  #detail-view .detail-leg-flight { font-size: 13px; }
  #detail-view .detail-leg-route { font-size: 12px; }
  #detail-view .detail-leg-cities,
  #detail-view .detail-leg-meta { font-size: 10px; }
  #detail-view .detail-leg-date { font-size: 10px; }
  #detail-view .detail-notice { font-size: 10px; padding: 7px 9px; }
  #detail-view .detail-note,
  #detail-view .detail-airline-tips p { font-size: 12px; }
  #detail-view .contact-btn { padding: 8px 10px; }
  #detail-view .contact-btn-label { font-size: 12px; }
  #detail-view .contact-btn-value { font-size: 13px; }
  #detail-view .contact-btn-hint { font-size: 10px; }
  #detail-view .copy-bubble-grid { grid-template-columns: 1fr 1fr; gap: 5px; }
  #detail-view .copy-bubble-label { font-size: 9px; }
  #detail-view .copy-bubble { font-size: 13px; padding: 8px 7px; }
  #detail-view .res-tile { min-height: 52px; padding: 8px 10px; }
  /* All these bubbles share one size on mobile so the cramped trio (trip dates /
     call airline / manage booking) fits its phone number cleanly. */
  #detail-view .res-tile-v { font-size: 11px; letter-spacing: 0.01em; }
  #detail-view .res-tile-k { font-size: 8px; }
  /* Mobile: the two text bubbles are roughly half as tall, and the stacked icon
     buttons shrink to match (icon centered). Desktop keeps its taller sizing. */
  #detail-view .res-airline-row > .res-tile { min-height: 0; padding: 5px 9px; gap: 2px; }
  /* Confirmation matches the section title size on mobile too (beats the row override). */
  #detail-view .res-tile-conf .res-tile-v,
  #detail-view .res-airline-row > .res-tile-conf .res-tile-v { font-size: 13px; font-weight: 700; }
  #detail-view .res-icon-col { gap: 4px; }
  #detail-view .res-icon-btn { min-height: 0; width: 38px; }
  #detail-view .res-icon-btn svg { width: 15px; height: 15px; }
  #detail-view .res-leg-fn { font-size: 13px; }
  #detail-view .res-leg-route { font-size: 11px; }
  #detail-view .res-leg-date { font-size: 9px; }
  #detail-view .res-traveler-flat { font-size: 12px; padding: 10px 12px; }
  #detail-view .res-detail-line { font-size: 11px; padding: 6px 0; }
  #detail-view .res-detail-k, #detail-view .res-detail-v { font-size: 11px; }
  #detail-view .res-section-panel > .res-acc-head .res-acc-summary { font-size: 13px; }   /* heading */
  #detail-view .res-acc-summary { font-size: 12px; }                                     /* subheading (nested) */
  #detail-view .res-acc-head { padding: 10px 12px; }
  #detail-view .res-acc-chev { font-size: 16px; }
  #detail-view .res-section-panel { margin-bottom: 6px; }
  /* Bubbles INSIDE an opened dropdown: a touch smaller again so they don't feel
     crunched (mobile only). */
  #detail-view .res-acc-inner .res-tile { min-height: 46px; padding: 7px 9px; }
  #detail-view .res-acc-inner .res-tile-v { font-size: 11px; }
  #detail-view .res-acc-inner .res-tile-k { font-size: 8px; }
  #detail-view .res-acc-inner .res-detail-line,
  #detail-view .res-acc-inner .res-detail-k,
  #detail-view .res-acc-inner .res-detail-v { font-size: 10px; }
}

/* ============================================================
   iPad DETAIL VIEW — a TOUCH device wider than a phone. The compact block above
   gives iPad the phone-sized detail text (too small for the big screen); here we
   step it UP to sit between the phone and the desktop, inspired by the iPhone
   look but roomier. min-width 768px keeps every iPhone out; both orientations.
   ============================================================ */
/* PORTRAIT only, for the reason written up at the day-card block above: in
   landscape a coarse device is the rail layout, and this view is a phone-width
   column there, not a big screen. */
@media (pointer: coarse) and (min-width: 768px) and (orientation: portrait) {
  /* Headline */
  #detail-view .detail-head { gap: 11px; margin-bottom: 12px; }
  #detail-view .detail-head .detail-icon svg { width: 26px; height: 26px; }
  #detail-view .detail-title { font-size: 20px; }
  #detail-view .detail-subtitle { font-size: 12px; margin-top: 3px; }
  /* Fact rows */
  #detail-view .detail-rows { gap: 1px; margin-bottom: 14px; }
  #detail-view .detail-row { padding: 8px 2px; }
  #detail-view .detail-row .k { font-size: 12px; }
  #detail-view .detail-row .v { font-size: 14px; }
  /* Drive / Uber nav buttons */
  #detail-view .detail-nav-label { font-size: 11px; margin-bottom: 8px; }
  #detail-view .detail-nav { gap: 8px; }
  #detail-view .nav-btn { padding: 13px 16px; font-size: 15px; gap: 9px; }
  #detail-view .nav-btn svg { width: 18px; height: 18px; }
  /* Sections / accordions */
  #detail-view .res-section-panel { margin-bottom: 8px; }
  #detail-view .res-section-panel > .res-acc-head .res-acc-summary { font-size: 15px; }  /* heading */
  #detail-view .res-acc-summary { font-size: 13px; }                                    /* subheading */
  #detail-view .res-acc-head { padding: 11px 13px; }
  #detail-view .res-acc-chev { font-size: 17px; }
  /* Tiles */
  #detail-view .res-tile { min-height: 62px; padding: 10px 12px; }
  #detail-view .res-tile-v { font-size: 13px; }
  #detail-view .res-tile-k { font-size: 9px; }
  #detail-view .res-tile-conf .res-tile-v,
  #detail-view .res-airline-row > .res-tile-conf .res-tile-v { font-size: 15px; font-weight: 700; }
  #detail-view .res-tile-ic svg { width: 18px; height: 18px; }
  #detail-view .res-icon-btn { width: 41px; }
  #detail-view .res-icon-btn svg { width: 17px; height: 17px; }
  /* Flight legs */
  #detail-view .res-leg-fn { font-size: 15px; }
  #detail-view .res-leg-route { font-size: 13px; }
  #detail-view .res-leg-date { font-size: 10px; }
  /* Travelers + misc lines */
  #detail-view .res-traveler-flat { font-size: 13px; padding: 11px 13px; }
  #detail-view .res-detail-line { font-size: 12px; padding: 7px 0; }
  #detail-view .res-detail-k, #detail-view .res-detail-v { font-size: 12px; }
  #detail-view .res-acc-inner .res-tile { min-height: 54px; padding: 9px 11px; }
  #detail-view .res-acc-inner .res-tile-v { font-size: 13px; }
  #detail-view .res-acc-inner .res-tile-k { font-size: 9px; }
  #detail-view .res-acc-inner .res-detail-line,
  #detail-view .res-acc-inner .res-detail-k,
  #detail-view .res-acc-inner .res-detail-v { font-size: 11px; }
  /* Contact + copy bubbles */
  #detail-view .contact-btn-label { font-size: 13px; }
  #detail-view .contact-btn-value { font-size: 14px; }
  #detail-view .contact-btn-hint { font-size: 11px; }
  #detail-view .copy-bubble-label { font-size: 10px; }
  #detail-view .copy-bubble { font-size: 14px; padding: 9px 9px; }
}

/* iPad PORTRAIT: give the DAY view and the reservation detail page the SAME width
   as the calendar month grid — by centring them all to the one shared width
   (--cal-maxw, defined with the month grid above). Headers centre too so the back
   ‹ button lines up with the content edge, exactly like the month view. */
@media (orientation: portrait) and (min-width: 769px) {
  #day-header, #day-body,
  #detail-header, #detail-body {
    width: 100%;
    max-width: var(--cal-maxw);
    margin-inline: auto;
  }
}

/* EVERY landscape screen (iPhone landscape, iPad landscape, desktop): give the day
   view (Morning/Afternoon/Evening columns) and the event detail page the SAME
   centred width + side margins as the month grid, so all three calendar pages line
   up to one constraint. (--sq is defined for all landscape; the grid is --sq·7+24px,
   so the day/detail views reuse exactly that.) Headers centre too so the back ‹
   button aligns with the content edge. */
@media (orientation: landscape) {
  #day-header, #day-body,
  #detail-header, #detail-body {
    width: 100%;
    max-width: calc(var(--sq) * 7 + 24px);   /* the very same width as the month grid */
    margin-inline: auto;
  }
}

/* ============================================================
   SHARED POP-UP PIECES
   ============================================================ */
/* The dimmed layer behind any pop-up */
#add-overlay, .sheet-overlay, #item-menu, #trip-menu, #chat-menu, #cost-row-menu, #cost-type-menu {
  position: fixed;
  inset: 0;
  z-index: 10;
  background: rgba(0, 0, 0, 0.42);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
}
#item-menu, #cost-row-menu, #cost-type-menu { z-index: 11; align-items: flex-end; padding: 16px; }  /* sits at the bottom */
/* Budget type picker: dim + frost so the sheet behind softens, not a hard blackout. */
#cost-type-menu {
  background: rgba(0, 0, 0, 0.38);
  -webkit-backdrop-filter: blur(16px);
  backdrop-filter: blur(16px);
}
/* #trip-menu (right-click/long-press a globe flag bubble) stays centered —
   it belongs to the globe quadrant it's pinned to (see positionMenuOverGlobe),
   not a bottom sheet like #item-menu's calendar-entry edit/delete. */
#trip-menu { z-index: 11; align-items: center; justify-content: center; padding: 16px; }
/* #chat-menu (rename/pin/delete a chat) belongs to the AI menu card it's
   opened from (see positionMenuOverElement in app.js), same idea as
   #trip-menu pinning to the globe instead of the calendar. */
#chat-menu { z-index: 12; align-items: flex-end; padding: 16px; }

/* #item-menu (edit/delete one entry) is a full-viewport overlay by default,
   so its card centers on the WHOLE page — wrong the moment the globe takes
   up part of the screen (any landscape split, or the stacked portrait
   layout). JS (positionMenuOverCalendar) pins its box to the LIVE #calendar
   pane's rect instead, on every device/orientation — no media query to keep
   in sync as breakpoints change. #trip-menu gets the same pin-to-rect
   treatment but aimed at #globe instead (positionMenuOverGlobe), so its card
   centers in the globe's own quadrant rather than at the click/tap position.
   Desktop keeps a bit of extra polish: blur behind the menu, same recipe as
   #ai-menu-scrim. */
@media (orientation: landscape) and (pointer: fine) {
  #item-menu, #trip-menu, #chat-menu, #cost-row-menu {
    background: rgba(0, 0, 0, 0.38);
    -webkit-backdrop-filter: blur(14px);
    backdrop-filter: blur(14px);
  }
}

/* The floating glass CARD every pop-up shares */
#add-card, .sheet-card {
  width: 100%;
  max-width: 360px;
  display: flex;
  flex-direction: column;
  padding: 20px;
  border-radius: var(--r-card);
}
#add-card h2, .sheet-card h2 { font-size: 18px; margin-bottom: 6px; }

/* The text fields / dropdowns inside the pop-ups — the card behind them is
   already glass, so these are just a slightly lighter rectangle on it */
#add-card input, .sheet-card input,
#flight-time-ampm, .ampm {
  background: rgba(var(--fg-rgb), 0.07);
  border: 1px solid rgba(var(--fg-rgb), 0.10);
  border-radius: var(--r-btn);
  padding: 11px 12px 11px 14px;
  font-size: 16px;                 /* 16px stops iPhone Safari zooming on focus */
}
/* A clear "this field is focused" ring */
#add-card input:focus, .sheet-card input:focus,
#flight-time-ampm:focus, .ampm:focus {
  outline: none;
  background: rgba(var(--fg-rgb), 0.11);
  box-shadow: 0 0 0 3px rgba(var(--fg-rgb), 0.14);
}
#add-card label, .sheet-card label {
  font-size: 12px;
  color: rgba(var(--fg-rgb), 0.5);
  margin-top: 12px;
  margin-bottom: 4px;
}

/* The two button styles every pop-up shares: a glass "cancel" + a bright "save" */
#add-cancel, .btn-cancel {
  color: var(--fg);
  font-size: 16px;
  padding: 12px;
  border-radius: var(--r-btn);
  box-shadow: none;                /* it sits IN the card, not above it */
}
#add-cancel, .btn-cancel {
  transition: transform var(--dur-tap) var(--ease-snap), background 0.15s ease;
}
#add-cancel:active, .btn-cancel:active { background: var(--press); transform: scale(0.97); }
/* The primary action is the ONE solid thing on screen, so it leads the eye */
#add-save, .btn-save {
  font-size: 16px;
  font-weight: 600;
  padding: 12px;
  border-radius: var(--r-btn);
  color: var(--invert-fg);
  background: var(--invert-bg);
  transition: transform var(--dur-tap) var(--ease-snap), opacity 0.2s ease;
}
#add-save:active, .btn-save:active { transform: scale(0.96); }
#add-save:disabled, .btn-save:disabled { opacity: 0.4; }

/* ---------- THE "ADD A FLIGHT" POP-UP ---------- */
/* One-way / Round trip / Multi-city — a segmented control, only shown when
   adding fresh (editing one existing leg hides it, see openAddForm). */
#trip-type-row { display: flex; gap: 8px; margin-top: 12px; }
.trip-type-btn {
  flex: 1;
  padding: 9px;
  font-size: 13px;
  font-weight: 600;
  color: rgba(var(--fg-rgb), 0.6);
  background: rgba(var(--fg-rgb), 0.07);
  border: 1px solid rgba(var(--fg-rgb), 0.10);
  border-radius: var(--r-btn);
  transition: transform var(--dur-tap) var(--ease-snap), background 0.15s ease, color 0.15s ease;
}
.trip-type-btn:active { transform: scale(0.97); }
.trip-type-btn.active { color: var(--invert-fg); background: var(--invert-bg); }

/* One block per flight segment — point A, point B, date, airline+number. */
.flight-leg {
  padding-top: 14px;
  margin-top: 14px;
  border-top: 1px solid rgba(var(--fg-rgb), 0.08);
}
.flight-leg:first-child { border-top: none; margin-top: 4px; padding-top: 4px; }
.flight-leg-heading {
  font-size: 12px;
  font-weight: 600;
  color: rgba(var(--fg-rgb), 0.5);
  margin-bottom: 8px;
}
.flight-leg .route-row { display: flex; gap: 10px; align-items: flex-start; }
.flight-leg .flight-leg-date { width: 100%; margin-top: 10px; }
/* The DIRECT-CHILD airline field only — NOT the From/To fields nested one
   level deeper inside .route-row. ":last-child" alone matched both (each is
   "last" within its own parent), which is what put From and To on different
   baselines: the To field (last inside .route-row) got this same top margin
   the From field never did. */
.flight-leg > .airport-field { margin-top: 10px; }

#add-leg-btn {
  margin-top: 12px;
  padding: 10px;
  font-size: 14px;
  font-weight: 600;
  color: var(--fg);
  background: rgba(var(--fg-rgb), 0.07);
  border: 1px dashed rgba(var(--fg-rgb), 0.3);
  border-radius: var(--r-chip);
  transition: transform var(--dur-tap) var(--ease-snap), background 0.15s ease;
}
#add-leg-btn:active { transform: scale(0.98); background: var(--press); }

/* The list of airlines that matches what you typed */
.airline-option {
  padding: 10px;
  border-radius: 10px;
  font-size: 14px;
  cursor: pointer;
  transition: transform var(--dur-tap) var(--ease-snap), background 0.15s ease;
}
.airline-option:active { background: rgba(var(--fg-rgb), 0.12); transform: scale(0.99); }
.airline-option .code { color: rgba(var(--fg-rgb), 0.55); margin-right: 8px; }

/* ---------- THE TRANSFER / HOTEL "sheet" pop-ups ---------- */
.sheet-actions { display: flex; gap: 10px; margin-top: 18px; }
.sheet-actions button { flex: 1; }

/* Native date inputs styled to match the rest of the form */
.sheet-card input[type="date"], #add-card input[type="date"] {
  font-family: inherit;
  min-height: 44px;
}

/* "Drop off at a booked hotel?" quick-pick in the transfer form */
#tr-hotels { display: flex; flex-direction: column; gap: 6px; }
#tr-hotels:empty { display: none; }
.tr-hotels-label { font-size: 12px; color: rgba(var(--fg-rgb), 0.5); margin-top: 4px; }
.tr-hotel-suggest {
  display: flex;
  align-items: center;
  gap: 6px;
  text-align: left;
  font-size: 14px;
  color: rgba(var(--fg-rgb), 0.9);
  border-radius: var(--r-chip);
  box-shadow: none;
  padding: 9px 11px;
  transition: transform var(--dur-tap) var(--ease-snap), background 0.15s ease;
}
.tr-hotel-suggest:active { transform: scale(0.98); background: var(--press); }
.tr-hotel-suggest svg { width: 14px; height: 14px; flex-shrink: 0; }

/* The From / To row: two airport pickers side by side (see .flight-leg .route-row) */
.airport-field { flex: 1; position: relative; }   /* the dropdown hangs off this */
.airport-field input { width: 100%; }
/* The matching-places dropdown floats below its box — darker than the usual
   glass so a list of options stays readable over anything */
.airport-list {
  position: absolute;
  left: 0;
  right: 0;
  top: 100%;
  z-index: 5;                      /* sit above the buttons below it */
  margin-top: 4px;
  max-height: 180px;
  overflow-y: auto;
  overflow-x: hidden;
  background: rgba(12, 12, 16, 0.88);
  border-radius: var(--r-chip);
}
.airport-option {
  padding: 10px;
  font-size: 14px;
  cursor: pointer;
  transition: transform var(--dur-tap) var(--ease-snap), background 0.15s ease;
}
.airport-option:active { background: rgba(var(--fg-rgb), 0.12); transform: scale(0.99); }
.airport-option .code { color: rgba(var(--fg-rgb), 0.55); margin-right: 8px; }

#add-actions { display: flex; gap: 10px; margin-top: 18px; }
#add-actions button { flex: 1; }

/* ---------- ADD BOOKINGS PANEL (inside globe menu) ---------- */
#globe-menu-card .addmenu-cat {
  background: var(--glass-bg);
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
  border: 1px solid var(--glass-border);
  box-shadow: none;
}
#globe-menu-card .addmenu-cat:active {
  background: rgba(var(--fg-rgb), 0.15);
}
#globe-menu-card .addmenu-label {
  color: rgba(var(--fg-rgb), 0.62);
  letter-spacing: 0.04em;
}
#globe-menu-card #addmenu-status { color: rgba(var(--fg-rgb), 0.88); }

/* HERO: the headline upload action — a luxurious glass panel with a soft glow,
   an icon in its own lit circle, and a two-line label. Tap to pick, or drop here. */
/* Matches the bottom menu rows (.globe-menu-row, e.g. Sign out): same corner
   radius (--r-chip), same flat surface, border and (no) shadow — so the whole
   menu reads as one congruent set. Only the two-line hero layout is its own. */
/* HERO row: the upload button (grows to fill) + the profile avatar bubble
   beside it. Extra margin-bottom opens clear air before "Copy forwarding
   email" so the two actions don't feel stacked on top of each other. */
.addmenu-hero-row {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 14px;              /* space before the email-forward row */
}
.addmenu-hero-row #addmenu-import { margin-bottom: 0; flex: 1; min-width: 0; }
#addmenu-import {
  display: flex;
  align-items: center;
  gap: 0.85em;
  width: 100%;
  text-align: left;
  color: var(--fg);
  border-radius: var(--r-chip);
  padding: 1em 1.05em;
  margin-bottom: 0;
  background: var(--glass-bg);
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
  border: 1px solid var(--glass-border);
  box-shadow: none;
  transition: transform var(--dur-tap) var(--ease-snap), background 0.15s ease, border-color 0.2s ease;
}
/* The signed-in user's Google profile picture — same 44px circle language as
   the globe's own menu button, just sitting flush in the hero row instead of
   floating over the map. */
#profile-avatar {
  flex-shrink: 0;
  width: var(--globe-btn);
  height: var(--globe-btn);
  border-radius: 50%;
  overflow: hidden;
  background: var(--glass-bg);
  border: 1px solid var(--glass-border);
  box-shadow: var(--flat-shadow);
}
#profile-avatar[hidden] { display: none; }
#profile-avatar-img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
#addmenu-import:active,
#addmenu-import.drag {
  transform: scale(0.98);
  background: var(--press);
}
/* Just the icon, inline with the text — no chip behind it (a circle back there
   read as its own separate tappable button next to the real one). */
.addmenu-import-icon {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--fg);
}
.addmenu-import-icon svg { width: 1.5em; height: 1.5em; }
.addmenu-import-text { display: flex; flex-direction: column; gap: 0.15em; min-width: 0; }
.addmenu-import-title { font-size: 1.02em; font-weight: 600; letter-spacing: -0.01em; }
.addmenu-import-sub { font-size: 0.82em; color: rgba(var(--fg-rgb), 0.6); line-height: 1.25; }
#addmenu-status { font-size: 0.85em; color: rgba(var(--fg-rgb), 0.8); text-align: center; margin-top: 8px; white-space: pre-line; }
#addmenu-status:empty { display: none; }           /* no empty gap when there's nothing to say */

/* The "working" sign while the AI reads your files: a little world with a
   plane flying around it. The world holds still; the plane's invisible
   wrapper spins, carrying the plane in a circle. Pure CSS, nothing heavy. */
#import-spinner {
  position: relative;
  width: 54px;
  height: 54px;
  margin: 12px auto 0;
}
.spinner-globe {
  position: absolute;
  inset: 12px;                       /* the world sits small in the middle... */
  color: rgba(var(--fg-rgb), 0.85);
}
.spinner-orbit {
  position: absolute;
  inset: 0;                          /* ...and the orbit fills the whole box */
  animation: orbit 1.4s linear infinite;
}
.spinner-plane {
  position: absolute;
  top: -2px;
  left: 50%;
  width: 14px;
  height: 14px;
  margin-left: -7px;                 /* center the plane on the orbit's top */
  transform: rotate(90deg);          /* nose forward, the way it's flying */
  color: var(--fg);
}
@keyframes orbit { to { transform: rotate(360deg); } }
/* "Reduce motion" users get the message text only, no spinning */
@media (prefers-reduced-motion: reduce) {
  .spinner-orbit { animation: none; }
}
/* The divider is itself a sliver of light, not a grey line */
.addmenu-divider {
  height: 1px;
  margin: 14px 0;
  background: linear-gradient(90deg, transparent, rgba(var(--fg-rgb), 0.25), transparent);
}
.addmenu-label { font-size: 0.85em; color: rgba(var(--fg-rgb), 0.5); text-align: center; margin-bottom: 8px; }
/* Manual-category icons: a tighter grid (smaller gap + slightly smaller tiles)
   so the row reads compact, with just enough air that icons don't touch. */
#addmenu-manual {
  display: flex;
  justify-content: center;
  gap: 0.35em;                      /* was 0.6em — more compact, still breathable */
  flex-wrap: wrap;
  margin-bottom: 12px;
  row-gap: 0.35em;
}
/* BOTTOM: each manual category is an icon in a small square of glass (no words).
   Sized in em so the squares grow with the menu's text on iPad/desktop. */
.addmenu-cat {
  width: 3.05em;                    /* was 3.4em — denser row without feeling cramped */
  height: 3.05em;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--r-chip);
  color: var(--fg);
  box-shadow: none;
  transition: transform var(--dur-tap) var(--ease-snap), background 0.15s ease;
}
.addmenu-cat:active { background: var(--press); transform: scale(0.94); }
.addmenu-cat svg { width: 1.45em; height: 1.45em; }
.addmenu-cat.disabled { opacity: 0.35; cursor: default; }   /* form not built yet */

/* ---------- LONG-PRESS MENU (Edit / Delete) ---------- */
#item-menu-card, #trip-menu-card, #chat-menu-card, #cost-row-menu-card {
  width: 100%;
  max-width: 360px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
/* Budget category picker — one glass card, flat option rows inside. */
#cost-type-menu-card {
  width: 100%;
  max-width: 280px;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 6px;
  border-radius: var(--r-card);
}
.cost-type-option {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  padding: 12px 14px;
  border: none;
  border-radius: var(--r-btn);
  background: transparent;
  color: rgba(var(--fg-rgb), 0.88);
  font: inherit;
  font-size: 15px;
  font-weight: 500;
  text-align: left;
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease;
}
.cost-type-option svg {
  width: 20px;
  height: 20px;
  flex-shrink: 0;
  display: block;
}
.cost-type-option:hover,
.cost-type-option:focus-visible {
  background: rgba(var(--fg-rgb), 0.07);
  outline: none;
}
.cost-type-option[aria-current="true"] {
  background: rgba(var(--fg-rgb), 0.12);
  color: var(--fg);
}
.cost-type-option:active { background: var(--press); transform: scale(0.98); }
/* The trip's own name above the Delete/Cancel rows. Deliberately NOT a glass
   pane like the buttons under it — it isn't pressable, and giving it the same
   material would read as a third button. Quiet, centered, and it joins the
   same staggered pop-in as the rows (it leads; they follow at 0.05s/0.08s). */
#trip-menu-title {
  margin: 0 0 2px;
  padding: 0 4px;
  text-align: center;
  font-size: 0.95em;
  font-weight: 600;
  color: rgba(var(--fg-rgb), 0.72);
  text-shadow: 0 1px 12px rgba(0, 0, 0, 0.8);
  animation: pop-in 0.24s var(--ease-out) 0.02s both;
}
#trip-menu-title:empty { display: none; }

/* A quiet line above a form's buttons naming what's still missing, shown only
   while Save is disabled. Monochrome and small — it's guidance, not an error. */
.sheet-hint {
  margin: 0 0 2px;
  font-size: 0.85em;
  line-height: 1.35;
  color: rgba(var(--fg-rgb), 0.55);
}
.sheet-hint[hidden] { display: none; }

/* A CITY heading nested under its country's heading (see renderCostList).
   Indented, smaller and quieter than the country above it, and with no rule
   under it — the country's own line already separates the section, and a
   second full-width rule two lines down just made the sheet look busy. */
.cost-city-heading-sub {
  margin-top: 2px;
  padding-left: 1.1em;
  border-bottom: none;
}
.cost-city-heading-sub .cost-city-name {
  font-size: 0.72em;
  letter-spacing: 0.06em;
  color: rgba(var(--fg-rgb), 0.55);
}
.cost-city-heading-sub .cost-city-subtotal {
  font-size: 0.82em;
  color: rgba(var(--fg-rgb), 0.45);
}
@media (prefers-reduced-motion: reduce) {
  #trip-menu-title { animation: none; }
}
/* Each menu row is its own floating pane of glass */
#item-menu-card button, #trip-menu-card button, #chat-menu-card button, #cost-row-menu-card button {
  padding: 15px;
  border-radius: 18px;
  font-size: 17px;
  color: var(--fg);
  transition: transform var(--dur-tap) var(--ease-snap), background 0.15s ease;
}
#item-menu-card button:active, #trip-menu-card button:active, #chat-menu-card button:active, #cost-row-menu-card button:active { background: var(--press); transform: scale(0.98); }
#item-delete, #trip-delete, #chat-delete, #cost-row-delete { color: rgba(var(--fg-rgb), 0.65); }
#item-cancel, #trip-cancel, #chat-cancel, #cost-row-cancel { font-weight: 600; }

/* ---------- Delete affordance on the trip flag bubbles ----------
   Desktop: right-click opens the delete confirm (see #trip-menu, wired in
   app.js). Nothing appears on hover — the owner tried a hover-revealed
   Delete pill here and asked for it gone; right-click alone is the interaction. */

/* ---------- THE MONTH GRID (kept lightweight — NO blur on the ~60 cells:
     they must scroll like silk on an iPhone) ---------- */
/* The weekday labels (S M T W T F S) stay fixed above the scrolling weeks */
#weekdays {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 4px;
  font-size: 11px;
  color: rgba(var(--fg-rgb), 0.5);
  text-align: center;
  margin-bottom: 6px;
}
/* The days area scrolls continuously through every month, snapping by week.
   Vertical only — never pan sideways (trackpad / shift-wheel / overflow).
   scroll-padding-top: 0 keeps each week row's top edge flush with this box so
   day-box outlines are fully visible (never half-cropped under the weekdays). */
#days {
  flex: 1;
  min-height: 0;
  min-width: 0;
  position: relative;                  /* week.offsetTop is relative to this scroller */
  overflow-y: auto;
  overflow-x: hidden;
  overscroll-behavior-x: none;
  /* mandatory (matches the deployed app exactly): the browser settles onto a
     week row as part of the scroll's own deceleration, in one motion. The
     proximity + JS-forced-resnap combo this replaced let a fast flick travel
     further, but then visibly yanked the grid back to a row a beat AFTER the
     scroll had already stopped — a separate, delayed correction that read as
     "locking" rather than settling. Removed on request; see the scroll
     listener below (snapDaysToNearestWeek is no longer called from there). */
  scroll-snap-type: y mandatory;
  scroll-padding-top: 0;
  -webkit-overflow-scrolling: touch;   /* smooth momentum scrolling on iPhone */
  scrollbar-width: none;               /* no scrollbar bar on PC — wheel still scrolls */
}
#days::-webkit-scrollbar { display: none; }
/* Trip jump — hold CSS snap off during the program scroll (phone + desktop)
   so the animation can land exactly on the first-week target without a second
   snap pass yanking it mid-row. */
#days.is-centering {
  scroll-snap-type: none !important;
  -webkit-overflow-scrolling: auto;
}
#days.is-centering .week {
  scroll-snap-align: none !important;
  scroll-snap-stop: normal !important;
}
/* Flag/trip jump in progress (see animateScrollTop in app.js). Blocks taps
   mid-flight so a stray touch doesn't yank the glide. We deliberately do NOT
   toggle content-visibility or hide icons here — flipping those at the end of
   the scroll forced Safari to re-layout ~260 weeks in one frame, which read as
   a stutter/snap right as the ease-out finished, and delayed the bright/dim
   month paint by up to a second on iPhone.
   Opacity transition on .day is also off during the jump so month highlight
   tracks the scroll frame-by-frame instead of lagging 0.25s behind each change. */
#days.is-program-scroll {
  pointer-events: none;
}
#days.is-program-scroll .day {
  transition: none;
}
/* One week = a row of 7 day cells */
.week {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 4px;
  margin-bottom: 4px;
  scroll-snap-align: start;            /* top of day boxes = top of #days */
  scroll-snap-stop: always;            /* don't skip past a week on a fast flick */
  scroll-margin-top: 0;
}
/* Day cells: PERFECT SQUARES of see-through glass with rounded corners.
   aspect-ratio means height always equals width — so on a wide desktop
   column the squares grow big automatically, on a phone they stay snug. */
.day {
  position: relative;                   /* so the icon row can sit at the bottom */
  container-type: inline-size;          /* lets the icons size themselves off the cell width */
  aspect-ratio: 1;                      /* a perfect square, at every screen size */
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: 1px solid rgba(var(--fg-rgb), 0.10);
  border-radius: var(--r-chip);         /* clearly rounded corners on every cell */
  font-size: var(--cal-day-fs);         /* locked to the title via --cal-ratio (1.44) */
  cursor: pointer;
  transition: transform var(--dur-tap) var(--ease-snap), opacity 0.25s ease, background 0.15s ease;
}
.day:active { background: rgba(var(--fg-rgb), 0.10); transform: scale(0.96); }
/* Month grid ↔ globe pairing: hovering a day lights its city's pin, and hovering that
   PIN lights every day cell in that city back (.day-pin-hover, from
   highlightMonthCellsForCity). Hover only — a phone has no pointer to hover with. */
@media (hover: hover) {
  .day[data-key]:hover { background: rgba(var(--fg-rgb), 0.07); }
}
.day.day-pin-hover {
  background: rgba(var(--fg-rgb), 0.10);
  border-color: rgba(var(--fg-rgb), 0.45);
}
/* Tiny, centered, monochrome event icons along the bottom of a day cell */
.day-icons {
  position: absolute;
  left: 0;
  right: 0;
  bottom: clamp(2px, 5cqw, 6px);        /* sits a touch above the cell's bottom edge */
  display: flex;
  align-items: center;                  /* keeps the idea ring centered against plain glyphs */
  justify-content: center;
  gap: clamp(2px, 3cqw, 3px);
  color: rgba(var(--fg-rgb), 0.55);     /* subtle white — present, not loud */
  pointer-events: none;                 /* taps fall through to the day cell */
  /* A day shows at most three glyphs (see DAY_ICON_CAP), and they are sized so
     three ALWAYS clear the cell's rounded corners with room to spare. Before
     this, three dashed idea rings measured 86px inside a 94px cell — under 4px
     of air on each side, which read as crowded even when it technically fit,
     and a fourth glyph spilled straight over the border into the next day.
     Nothing here is a magic number: the sizes below are container-query units,
     so the row breathes the same way at every cell size and screen. */
}
/* Icons scale with the cell width (cqw = 1% of the cell): they shrink in tight
   landscape cells so they fit and never overlap the day number, and grow on big
   desktop cells — capped so they never get oversized. Works at every screen size. */
.day-icons svg { display: block; width: clamp(7px, 17cqw, 15px); height: clamp(7px, 17cqw, 15px); }
/* Every glyph's own wrapper (see setCellIcons) — what makes ONE event icon
   individually hoverable instead of only the day cell as a whole. Reopens
   pointer-events just for this tiny box; .day-icons itself stays
   click-through so empty space between glyphs still falls to the day cell. */
.day-icon-glyph {
  display: inline-flex;
  pointer-events: auto;
  border-radius: 50%;
  transition: transform 0.15s var(--ease-out), filter 0.15s ease, color 0.15s ease;
}
/* Fourth leg of the day-view-card / globe-pin / budget-row hover pairing
   (see highlightLinkedEntry / highlightMonthIcon): hovering any ONE of those
   three lights this glyph back too, and hovering the glyph itself lights the
   other three. Small and quiet, same restrained monochrome language as the
   others — a lift would be lost at this size, so it's a brighten + a tight
   glow instead. */
.day-icon-glyph.day-icon-linked-hover {
  color: var(--fg);
  transform: scale(1.35);
}
/* An idea/draft's icon (same type-specific glyph a confirmed booking uses —
   see baseIconForType) sits inside a small dashed ring, same visual language
   as .globe-trip-bubble-idea's dashed ring elsewhere in the app: solid =
   booked, dashed = "not yet." The ring is sized off the glyph itself (em, not
   a fixed px) so it scales with the same clamp() the plain icons use above
   and never grows past "a little bigger than the icon it circles." */
.day-icon-idea {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  position: relative;
  border-radius: 50%;
  /* The dashed ring below pokes out past this box, which the flex gap on
     .day-icons doesn't know about — without this margin, two idea rings sitting
     side by side overlap/touch instead of just their icons. It SCALES with the
     cell (cqw) like the glyphs do: a fixed margin left three rings only 3.7px
     of air in a narrow phone cell, because the glyphs shrank and the rings
     didn't. --ring is shared with ::after below so the gap and the ring it is
     compensating for can never drift apart. */
  --ring: clamp(1.5px, 2cqw, 2.5px);
  margin: 0 var(--ring);
}
.day-icon-idea::after {
  content: '';
  position: absolute;
  inset: calc(-1 * var(--ring));
  border-radius: 50%;
  border: 1px dashed rgba(var(--fg-rgb), 0.72);
}
/* DESKTOP ONLY: the day cells are much bigger here, so the same icons that read fine
   on a phone get lost. Bigger + near-full-white + a soft glow makes them stand out
   without breaking the app's black-and-white rule (only the globe gets real color —
   see CLAUDE.md) by introducing an unrelated accent color into the calendar. */
@media (orientation: landscape) and (min-width: 1024px) and (pointer: fine) {
  .day-icons svg {
    width: clamp(7px, 16cqw, 16px);
    height: clamp(7px, 16cqw, 16px);
    color: rgba(var(--fg-rgb), 0.92);
    /* A tighter, dimmer halo than before: at this smaller size the old 3px
       glow bled into its neighbour and made three icons read as one smudge. */
    filter: drop-shadow(0 0 2px rgba(var(--fg-rgb), 0.32));
  }
  .day-icon-idea::after { border-width: 1px; }
  .day-icon-idea { --ring: 3px; }
}
/* Desktop: lift the drive/Uber bubbles off the panel's bottom edge so they
   aren't cropped. */
@media (orientation: landscape) {
  #detail-view .detail-nav { margin-bottom: 14px; }
  #detail-body { padding-bottom: 4px; }
}
/* Folded iPhone Ultra FRONT screen — a small, near-square portrait pane
   (~474×696pt, roughly 5:4 / 4:3). Keep the same 50/50 split as every other
   phone; only the day cells get shorter so a full month still fits in the
   calendar half without forcing the globe smaller. */
@media (orientation: portrait) and (min-aspect-ratio: 3 / 5) and (max-width: 768px) {
  .week { margin-bottom: 3px; }
}
/* Days belonging to a month other than the one in focus recede into the space.
   A trip jump now CENTERS the focus month in #days (see monthScrollTarget), so
   the rows directly above and below it are always a neighbouring month — at the
   old 0.12 they were so close to invisible that the centered month looked like
   it was floating over empty space, which is the thing centering was meant to
   fix. Dim enough to read instantly as "not this month", present enough to look
   like calendar. Opacity only, deliberately no blur filter: this can apply to
   ~40 cells at once and a real filter on that many boxes wrecks scrolling on an
   iPhone (same reason the day cells carry no backdrop-filter — see CLAUDE.md). */
.day.off-month { opacity: 0.32; border-color: rgba(var(--fg-rgb), 0.05); }
.day.muted { opacity: 0.2; cursor: default; }   /* days outside 2026–2030 */
/* Today: the same see-through glass as every other cell — it just has
   a brighter outline (and bolder number) so your eye finds it */
.day.today {
  font-weight: 600;
  border-color: rgba(var(--fg-rgb), 0.65);
}

/* ============================================================
   iPad & DESKTOP — keep the month readable (phone layout ≤768px untouched).
   On a wide screen, 7 squares stretched edge-to-edge become huge, so only
   3–4 week-rows fit. Like Apple's Calendar, we instead CAP the grid width and
   CENTER it with side padding — squares stay a comfortable size and a whole
   month stays in view. #weekdays + #days share the cap so they line up.
   ============================================================ */

/* iPad PORTRAIT (stacked; calendar is the bottom ~half). Height-driven cap so
   ~5 week-rows always fit, from the chubby iPad mini up to the 12.9". The 70vh
   factor = 7 columns × (half-screen height ÷ 5 rows); −126px is the header +
   weekday row + paddings that aren't part of the scrolling squares. */
/* A PHONE LAYOUT WIDER THAN A PHONE.

   The phone layout now runs to 768px so an unfolded foldable iPhone gets it
   (see isPhoneBarLayout). Between a real phone and that ceiling the day grid
   is still full-bleed, and seven squares stretched across 768px are huge -
   the exact complaint that made the iPad rule below cap its width. Same cure,
   scoped to the new band only: cap the grid and centre it, so the squares
   stay a comfortable size and more of the month stays in view. Phones at
   600px and under keep the full-bleed grid they were designed with, untouched. */
@media (orientation: portrait) and (min-width: 601px) and (max-width: 768px) {
  #weekdays, #days {
    width: 100%;
    max-width: 600px;
    margin-inline: auto;
  }
  /* Four events across instead of three. The day's cards are squares sized
     from their own column width, so on this much wider screen three of them
     become big enough that a busy day needs scrolling to show what a phone
     fits on one screen. A fourth column spends the extra width on MORE of
     the day being visible at once rather than on bigger cards. Nothing in
     JavaScript needs telling: sizeDayItems reads the column count back off
     the computed grid (see its own comment on why the row height cannot be
     left to "auto"), so the squares stay square and the rows stay clear of
     each other by themselves. */
  .day-flow { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}

@media (orientation: portrait) and (min-width: 769px) {
  /* One shared width so the day grid AND the reservation detail page share the
     exact same side padding on iPad portrait (see the detail view below). */
  :root { --cal-maxw: min(calc(70vh - 126px), 720px); }
  #weekdays, #days {
    width: 100%;
    max-width: var(--cal-maxw);
    margin-inline: auto;
  }
}

/* LANDSCAPE (side-by-side; calendar is the right half) — show a fixed number of
   week-rows per device, centred with padding on every side. We pick a square size
   --sq that's the SMALLER of the height-fit (so exactly --cal-rows fill the half's
   height) and the width-fit (so it never overflows the half's width); the grid is
   capped to 7·--sq wide and --cal-rows tall, then auto-margins centre the whole
   weekday+grid block (top/bottom padding) and margin-inline centres it (side
   padding). One fluid recipe; only the row count changes per device below.
     5 rows iPhone landscape · 7 rows iPad landscape · 9 rows desktop. */
@media (orientation: landscape) {
  :root {
    --cal-rows: 5;                                              /* iPhone landscape */
    --sq: min(calc((86vh - 70px) / var(--cal-rows)),           /* height-fit: N rows fill the half */
              calc((50vw - 56px) / 7));                         /* width-fit: never wider than the half */
  }
  #weekdays, #days {
    width: 100%;
    max-width: calc(var(--sq) * 7 + 24px);                     /* 7 squares + the 6 column gaps */
    margin-inline: auto;                                        /* side padding */
  }
  /* Centre the whole title + weekday + grid block as one group, so the month
     title sits just above the calendar (not pegged to the very top) with equal
     padding above and below. margin-top:auto on the FIRST item (the title) and
     margin-bottom:auto on the LAST (the grid) keeps them adjacent and centred. */
  #calendar-header { margin-top: auto; }
  #days {
    flex: 0 1 auto;                                             /* don't grow — leave room to centre */
    max-height: calc((var(--sq) + 4px) * var(--cal-rows));      /* exactly --cal-rows tall (incl. gaps) */
    margin-bottom: auto;
  }
  /* Nudge ONLY the title up a touch (the calendar grid stays centred — transform
     doesn't affect layout), so it reads a little closer to the globe's menu button
     without dragging the calendar with it. */
  #calendar-title { transform: translateY(-20px); }
}
/* iPad landscape (a touch device taller than a phone in landscape): 7 rows. */
@media (orientation: landscape) and (pointer: coarse) and (min-height: 601px) {
  :root { --cal-rows: 7; }
}
/* Desktop (mouse, 60/40 split): exactly 6 week-rows so every month fits.
   Square size is the SMALLER of height-fit (6 rows in the calendar pane)
   and width-fit (7 columns in the right 40%) — cells stay 1:1, just bigger. */
@media (orientation: landscape) and (min-width: 1024px) and (pointer: fine) {
  :root {
    --cal-rows: 6;
    /* ~104px = 8px top pad + title + 10px below + weekday + gaps + 16px bottom. */
    --sq: min(
      calc((55dvh - 104px) / var(--cal-rows)),
      calc((40vw - 48px) / 7)
    );
  }
}
/* On a mouse/trackpad, the forced week-snap "captures" momentum and flings the
   calendar many weeks at once. Desktop scrolls FREELY instead — normal, smooth
   scrolling. Touch keeps its clean week-by-week snap (unchanged). */
@media (pointer: fine) {
  #days { scroll-snap-type: none; }
}

/* Desktop: make the calendar day NUMBERS and the weekday LETTERS bigger.
   This block sits at the very end on purpose — the base .day / #weekdays rules
   appear later in the file, and media queries don't add specificity, so an
   earlier override would lose to them. Placed last, it wins. */
@media (orientation: landscape) and (pointer: fine) {
  /* Day numbers locked to 16px on desktop; weekday labels a step down (13px),
     the usual "muted label" treatment. Title is 23px (see the desktop block
     above) — 16 × 1.44, the app's own type ratio / an augmented-fourth step. */
  .day { font-size: 16px; }
  #weekdays { font-size: 13px; }
  /* Desktop: drop the whole calendar pane a little so the day-view back
     button and header clear the globe's right edge / top chrome completely. */
  #calendar {
    padding-top: 28px;
    padding-left: 20px;
    padding-right: 20px;
  }
}

/* Desktop 60/40: six square rows fill most of the pane. Quiet section-label
   title above weekdays; small fixed air under the last week (+ #calendar pad). */
@media (orientation: landscape) and (min-width: 1024px) and (pointer: fine) {
  #calendar {
    padding: 8px 16px 16px;
    justify-content: stretch;
  }
  /* Cost open: trim side/bottom pad a little so the wider sheet can use leftover air. */
  body.cost-open #calendar {
    padding: 8px 8px 12px;
  }
  /* After the base #day-header rule so it actually wins on desktop. */
  #day-header { margin-top: 10px; }
  #month-view {
    flex: 1;
    min-height: 0;
  }
  /* Compact title band — do not grow into leftover height. */
  #calendar-header {
    flex: 0 0 auto;
    min-height: 0;
    margin: 0 0 10px;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  #calendar-title {
    font-size: 20px;
    font-weight: 500;
    letter-spacing: 0.06em;
    line-height: 1.25;
    color: rgba(var(--fg-rgb), 0.88);
    transform: none;
  }
  #weekdays { margin-bottom: 8px; flex-shrink: 0; }
  #days {
    flex: 0 0 auto;
    height: calc((var(--sq) + 4px) * var(--cal-rows) - 4px);
    max-height: calc((var(--sq) + 4px) * var(--cal-rows) - 4px);
    margin-bottom: 0;
  }
  #month-view::after {
    content: "";
    flex: 0 0 auto;
    height: 0;
    min-height: 0;
  }
}

/* REMOVED: the "iPad landscape (touch) ONLY" calendar type step-up.

   Its media query was this file's rail layout exactly — landscape, coarse,
   1024 and up — and it was written when that meant a calendar filling half a
   big screen: 26px titles, 18px day numbers, 15px weekday letters, and the
   title lifted 34px. That layout now stacks the globe over the calendar in
   ONE phone-width column, so every one of those numbers was a size for a pane
   that no longer exists. Three of the four were already being overridden back
   down in the rail block; the weekday letters were not, which is why they
   still stood at 15px against the portrait iPhone's 11.

   Deleted rather than re-tuned: the rail block states this layout's calendar
   type in one place now, and a second block whose only job was to disagree
   with it is a trap, not a setting. */

/* iPad PORTRAIT only: make just the month TITLE bigger (nothing else). */
@media (orientation: portrait) and (min-width: 769px) {
  #calendar-title { font-size: 24px; }
}

/* iPhone PORTRAIT only: the month title a little bigger, and centred in the gap
   between the globe and the weekday letters (12px padding above #calendar-header,
   18px margin below it, no gap elsewhere — splitting that 30px evenly needs +3px,
   not +6px, which used to sit it closer to the weekday row than to the globe). */
@media (orientation: portrait) and (max-width: 768px) {
  #calendar-title { font-size: 21px; transform: translateY(3px); }
  /* Day cells stay PERFECT SQUARES here too (base .day aspect-ratio: 1), so the
     week-row height follows the column width. #days scrolls, and week-snap keeps
     a row landing flush at the top, so we never squash a row to force 5 in view. */
  .week { margin-bottom: 4px; }
  /* Flag bubbles: same size as the menu button, centred on the button's axis
     and stacked directly beneath it (same treatment as desktop). */
  #globe-trips-bubbles {
    right: 16px; left: auto; align-items: center; width: var(--globe-btn);
  }
  /* Collapsed layover = smaller nested flag; revealed = same size as the other
     trip flags on the phone (--globe-btn, same as the menu button). */
  .globe-trip-bubble-layover {
    width: calc(var(--globe-btn) * 0.75);
    height: calc(var(--globe-btn) * 0.75);
    margin-top: calc(-1 * (var(--globe-btn) * 0.75 + 10px));
  }
  .globe-trip-bubble-layover.is-shown {
    width: var(--flag-size);
    height: var(--flag-size);
    margin-top: 0;
  }
}

/* iPhone LANDSCAPE only (touch, narrower than iPad): move the month title a
   little lower. Base landscape lifts it -20px; this brings it back down. */
@media (orientation: landscape) and (pointer: coarse) and (max-width: 1023px) {
  #calendar-title { transform: translateY(-6px); font-size: 22px; }
}

/* iPhone only (portrait phone OR landscape phone — not iPad/desktop): pull the
   top chrome (account, email spinner, flag stack) closer to the status bar.
   Was safe-area + 16px — felt like a large empty band under the time/battery.
   Keep safe-area for the notch + 8px of air so it still breathes. */
@media (orientation: portrait) and (max-width: 768px),
       (orientation: landscape) and (pointer: coarse) and (max-width: 1023px) {
  #globe-account-btn,
  .globe-processing-icon {
    top: calc(env(safe-area-inset-top, 0px) + 8px);
  }
  #globe-trips-bubbles {
    top: calc(env(safe-area-inset-top, 0px) + 8px + var(--globe-btn) + 10px);
  }
}

/* ---- Ballesta ideas (AI recommendations) ---- */
/* The "Ideas in <city>" card, and any placed idea (hotel/tour/restaurant not yet
   booked): same glass box as real entries, but a dashed outline so it reads as a
   suggestion, not a booking. */
.item-recs, .item-idea {
  border-style: dashed;
  border-width: 2px;                       /* was 3px on photo cards — full white at 3px read as too heavy/stark */
  border-color: rgba(var(--fg-rgb), 0.7);
  opacity: 0.9;
}
/* A photo card (see fetchHotelImage/fetchTransferImage) puts a colorful,
   often LIGHT image right under the dashed border above, and it disappeared
   against a bright patch of sky or a pale wall. Fixing it by darkening/boxing
   the border directly (tried once already) put a hard ring ON the photo and
   fought with .item-pin-hover's own inset ring on hover — looked worse, not
   better. Instead, inset the photo a few px so the dashes sit on the card's
   plain glass background (base .item's background-color, which this rule
   doesn't touch) instead of on top of the image — same fix the eye reads as
   "the picture got a tiny bit smaller", nothing sits ON the photo at all. */
.item-recs.item-has-photo, .item-idea.item-has-photo {
  padding: 4px;
  background-clip: padding-box;
  /* The 0.7-alpha border above, further multiplied by this card's own
     opacity: 0.9, nets out around 0.63 — and blended over the dark glass
     background sitting in the padding-box gap, that reads as gray, not
     white. Photo cards get undimmed white here (opacity: 0.9 stays for the
     plain, no-photo idea card, which doesn't have this problem), but at a
     slightly softened alpha + thinner width than the first pass — full
     3px solid white was clearly visible but too heavy/stark next to a photo. */
  opacity: 1;
  border-width: 2.5px;
  border-color: rgba(var(--fg-rgb), 0.85);
}
/* THE BUG the comment above only half-fixed: insetting the photo stopped the
   dashed border from disappearing against a light photo, but never dealt
   with .item-pin-hover's own inset ring sitting at that SAME edge. An idea
   card already wears a near-white dashed border at rest (0.7-0.85 alpha
   above); .item-pin-hover's hover ring is a THINNER, similarly-pale white
   line stacked on the identical edge, 1.5px inside a border that's already
   2-2.5px of barely-fainter white — hovering a still-tentative reservation
   (any restaurant/tour/transfer/hotel not yet booked, on any day, for any
   traveler) added a class and fired every linked highlight exactly like a
   confirmed one does — the pin lit, the budget line lit, the month glyph
   lit — but the card itself showed no visible change, because the one part
   of that highlight a person actually looks at was indistinguishable from
   the border already sitting there. Confirmed pins/cards (no dashed border)
   never had this problem, which is why hovering a hotel card "worked" and
   hovering the restaurant right next to it silently didn't.
   Fixed at the source of the collision: hovering an idea card brightens its
   OWN border to full solid white instead of trying to add a second, fainter
   ring beside an almost-identical one — a real, visible state change on
   exactly the element the eye is already reading, on the same border the
   card is already known by. */
.item-recs.item-pin-hover, .item-idea.item-pin-hover {
  border-color: var(--fg);
}

/* Saily eSIM + NordVPN affiliate boxes (international flights only — see
   isInternationalLeg in app.js) ride together in ONE combined slot so they
   read as a single column entry instead of two boxes scattered across
   different columns. .item-affiliate-pair takes the same perfect-square
   footprint as a normal .item (one full block); the two boxes stack inside
   it, each its own half-block container, with a small gap between. */
.item-affiliate-pair {
  display: flex;
  flex-direction: column;
  gap: 8px;
  width: 100%;
  aspect-ratio: 1;             /* same one-block footprint as a normal .item */
  height: auto;
  flex: none;
}
.item-sale, .item-vpn {
  aspect-ratio: auto;
  height: auto;
  flex: 1 1 0;                 /* each box = half the pair's height, minus the gap */
  min-height: 0;
  width: 100%;
  border-style: dashed;
  border-width: 2px;
  border-color: rgba(var(--fg-rgb), 0.4);
}

/* An affiliate offer, not a calendar entry — so it's built as its own small
   centered card instead of borrowing the entry layout (time chip on the left,
   icon beside wrapping text) that made these read as broken bookings.
   Everything is centered on one axis: brand, then one line of copy, then a
   quiet call to action. Sized in em and clamped so the whole thing still fits
   the half-block it lives in on a phone, where the pair is at its smallest. */
.item-partner {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  /* Tight by default: on a phone this box is only ~50px tall (it's half of a
     square that is itself one of three day columns), so the spacing has to
     buy room for the copy rather than spend it on air. */
  gap: 0.2em;
  padding: 0.45em 0.5em;
  text-align: center;
  overflow: hidden;
}
.partner-brand {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.4em;
  min-width: 0;
}
/* The real logo, once one is dropped into public/logos/ — capped so a wide
   wordmark and a square mark both sit comfortably in the same slot. */
.partner-logo {
  max-height: 1.5em;
  max-width: 100%;
  width: auto;
  object-fit: contain;
}
/* Icon size/colour also come from .item-icon, so the glyph matches the icon
   on every reservation card beside it. */
.partner-glyph { flex: 0 0 auto; }
/* Size comes from .item-primary / .item-detail / .item-time, which these
   elements also carry — the same type scale every reservation card uses, at
   every screen size. Only the bits that are specific to a centered offer box
   are set here. */
.partner-word {
  white-space: nowrap;
  min-height: 0;               /* the card rule reserves a line; not needed here */
}
/* Plain block on purpose. A -webkit-box line clamp here treated the inline
   "eSIM for" span as a line of its own, so "eSIM for Istanbul" broke across
   two lines even with room to spare. The box's own overflow:hidden is enough
   to keep a very long city name from spilling. */
.partner-line {
  min-height: 0;
  overflow: hidden;
  /* The column centres its children, which makes a block child shrink to fit
     its own content box rather than the space available — the copy was being
     squeezed into 55px of a 173px box and wrapping for no reason. Stretching
     it gives the text the full width; text-align on the box still centres it. */
  align-self: stretch;
}
/* HIDDEN on phones by default. Three stacked lines cannot fit ~50px, and of
   the three the call to action is the one worth losing: the brand and the one
   line saying what it's for still carry the offer, and the whole box is
   tappable anyway. Restored on the desktop layout below, where there's room. */
/* Hidden on a phone column for the same reason as the call to action: there
   isn't room, and the brand above already carries the meaning. */
.partner-what { display: none; }

.partner-cta {
  display: none;
  white-space: nowrap;
  /* .item-time reserves space under itself for the title that normally
     follows it on a card; here it's the last line, so that space is dropped. */
  margin-bottom: 0;
}
@media (orientation: landscape) and (min-width: 1024px) and (pointer: fine) {
  .item-partner { gap: 0.3em; padding: 0.7em 0.6em; }
  .partner-what { display: inline; }
  .partner-cta { display: block; }
  /* A mouse gets a hint the arrow leads off-app. */
  .item-partner:hover .partner-cta { color: rgba(var(--fg-rgb), 0.85); }
}

/* The ideas page fills the calendar quadrant (inside #detail-body). */
.recs-head { display: flex; gap: 10px; align-items: center; margin-bottom: 12px; flex-wrap: wrap; }
.recs-head .rec-icon svg { width: 24px; height: 24px; }
/* "Fill in ideas": desktop only for now (there's room there; phone can come later) —
   same breakpoint the day view already uses to detect a desktop layout. */
.fill-ideas-btn {
  display: none;
  margin-left: auto;
  padding: 8px 14px;
  border-radius: var(--r-btn);
  font-size: 13px;
  font-weight: 600;
  background: var(--glass-bg);
  border: 1px solid var(--glass-border);
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
  transition: transform var(--dur-tap) var(--ease-snap), background 0.15s ease;
}
.fill-ideas-btn:active { background: var(--press); transform: scale(0.96); }
@media (orientation: landscape) and (pointer: fine) {
  .fill-ideas-btn { display: inline-flex; }
}
.recs-title { font-size: 17px; font-weight: 600; }
.recs-sub { font-size: 13px; opacity: 0.6; margin-top: 1px; }
.rec { display: flex; gap: 10px; padding: 10px 2px; border-bottom: 1px solid rgba(var(--fg-rgb),0.08);
       color: inherit; text-decoration: none; }
.rec:last-child { border-bottom: none; }
.rec-text { display: block; }
.rec-icon { flex: none; margin-top: 2px; }
.rec-icon svg { width: 18px; height: 18px; }
.rec-name { display: block; font-weight: 600; }
.rec-meta { display: block; font-size: 12px; opacity: 0.6; margin-top: 1px; }
.rec-why { display: block; font-size: 13px; opacity: 0.85; margin-top: 3px; }
.rec-note { font-size: 14px; opacity: 0.75; padding: 6px 0; }
.recs-divider { font-size: 12px; opacity: 0.55; margin: 14px 0 2px; padding-top: 10px;
                border-top: 1px solid rgba(var(--fg-rgb),0.1); }

/* The idea's pitch (openIdeaDetail's .idea-detail-why) and "find the booking"
   link back to a shared source (appendSourceBookingLink, confirmed AND
   tentative reservations alike) — the header row above (setDetailHeader,
   #detail-header) carries the name/date/Book button for every reservation
   now, so this panel only ever holds the parts that are genuinely per-item. */
.idea-detail-why { margin: 4px 2px 14px; }
.idea-book-link { border-top: 1px solid rgba(var(--fg-rgb),0.08); border-bottom: none; }

/* A one-line historical/cultural aside under a recommendation — quieter and
   smaller than the "why", so it reads as a bonus, not competing information. */
.rec-fact { display: block; font-size: 12px; opacity: 0.55; font-style: italic; margin-top: 3px; }

/* ---- Side trips (Ideas panel): nearby cities worth a detour during a long stay ---- */
.sidetrip-list { display: flex; flex-direction: column; gap: 14px; margin-top: 10px; }
.sidetrip-card {
  padding: 12px;
  border-radius: var(--r-card, 16px);
  background: var(--glass-bg);
  border: 1px solid var(--glass-border);
}
.sidetrip-head { display: flex; gap: 10px; align-items: flex-start; }
.sidetrip-name { font-size: 15px; font-weight: 600; }
.sidetrip-why { font-size: 13px; opacity: 0.8; margin-top: 2px; }
.sidetrip-transport { font-size: 13px; opacity: 0.85; margin: 8px 0 0; }
.sidetrip-acts { margin-top: 8px; display: flex; flex-direction: column; gap: 6px; }
.sidetrip-act { font-size: 13px; }
.sidetrip-act-name { font-weight: 600; }
.sidetrip-act-why { opacity: 0.75; margin-left: 4px; }
.sidetrip-add-btn {
  margin-top: 10px;
  width: 100%;
  padding: 9px;
  border-radius: var(--r-chip);
  font-size: 13px;
  font-weight: 600;
  background: rgba(var(--fg-rgb), 0.07);
  border: 1px solid rgba(var(--fg-rgb), 0.12);
  box-shadow: var(--flat-shadow);
  transition: transform var(--dur-tap) var(--ease-snap), background 0.15s ease;
}
.sidetrip-add-btn:active { background: var(--press); transform: scale(0.98); }
.sidetrip-add-btn:disabled { opacity: 0.6; }

/* Desktop-only type bump for the ideas/recommendations panel (X.com-style
   hierarchy: a handful of discrete steps — 15 / 14 / 13px — with tiers told
   apart mostly by weight and opacity, not by shrinking size further; X almost
   never drops real reading content below 13px, reserving anything smaller for
   throwaway footer text). The base sizes above are phone-tuned; on a real
   desktop (mouse, roomy screen) they read as too small — especially the "why"
   explanation under each idea, which is genuine reading text and deserves the
   recommendation's own body size, not a shrunken caption. */
@media (orientation: landscape) and (min-width: 1024px) and (pointer: fine) {
  .recs-title { font-size: 19px; }
  .recs-sub { font-size: 14px; opacity: 0.65; }
  .rec-meta { font-size: 13px; }
  .rec-why { font-size: 15px; opacity: 0.88; line-height: 1.4; }
  .rec-note { font-size: 15px; }
  .rec-fact { font-size: 13px; }
  .recs-divider { font-size: 13px; }
  .sidetrip-name { font-size: 16px; }
  .sidetrip-why { font-size: 15px; opacity: 0.85; line-height: 1.4; }
  .sidetrip-transport { font-size: 14px; }
  .sidetrip-act { font-size: 14px; }
  .sidetrip-add-btn { font-size: 14px; }
}


/* ---------------------------------------------------------------------------
   THE THINKING ORB — what the AI shows instead of the word "Thinking…"

   A little globe made of white dots, turning slowly. It is deliberately the
   SAME idea as the big globe this whole app is built around: white points of
   light on black, nothing coloured, nothing busy. The alternative in the
   reference (a sphere of dense vertical lines) reads heavier and more
   mechanical, which fights the "subtle, never distracting" rule.

   Built from plain divs and one CSS animation — no library, no canvas, no
   JavaScript running every frame. The browser rotates ONE element on the GPU
   and the dots come along for the ride, which is what keeps it smooth on a
   phone and free of battery cost.
   --------------------------------------------------------------------------- */
.ai-thinking {
  display: flex;
  align-items: center;
  gap: 9px;
}
.ai-orb {
  position: relative;
  width: 20px;
  height: 20px;
  flex: 0 0 auto;
  /* Shallow perspective — enough for the front dots to read as nearer without
     the sphere looking like a funnel. */
  perspective: 64px;
}
/* The faint bloom the dots sit in, so the orb reads as a lit object rather
   than a scatter of specks. Same restraint as the stars on the globe. */
.ai-orb::before {
  content: '';
  position: absolute;
  inset: -3px;
  border-radius: 50%;
  background: radial-gradient(circle at 50% 50%, rgba(var(--fg-rgb), 0.11), transparent 68%);
}
.ai-orb-sphere {
  position: absolute;
  inset: 0;
  transform-style: preserve-3d;
  animation: ai-orb-spin 4.2s linear infinite;
}
.ai-orb-dot {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 1.8px;
  height: 1.8px;
  margin: -0.9px 0 0 -0.9px;
  border-radius: 50%;
  background: var(--fg);
  /* Bright enough to stay a POINT at 20px. Dimmer than this and the dots turn
     grey and smear together into a fuzzy ball. */
  opacity: 0.9;
}
/* Tilted a little off the vertical so it turns like a planet rather than a
   spinning coin. */
@keyframes ai-orb-spin {
  from { transform: rotateX(-16deg) rotateY(0deg); }
  to   { transform: rotateX(-16deg) rotateY(360deg); }
}
.ai-orb-label {
  color: rgba(var(--fg-rgb), 0.62);
  animation: ai-orb-breathe 2.6s ease-in-out infinite;
}
@keyframes ai-orb-breathe {
  0%, 100% { opacity: 0.52; }
  50%      { opacity: 0.92; }
}
/* Someone who has asked their phone to stop animating things means it. The
   orb stays — it's the thing telling them we're working — it just stops
   drawing attention to itself. */
@media (prefers-reduced-motion: reduce) {
  .ai-orb-sphere { animation-duration: 18s; }
  .ai-orb-label { animation: none; opacity: 0.7; }
}

/* ---------- Desktop left sidebar (Grok-style) ---------- */
#app-sidebar { display: none; }
@media (orientation: landscape) and (min-width: 1024px) and (pointer: fine) {
  #app-sidebar:not([hidden]) {
    display: flex;
    flex-direction: column;
    width: var(--sidebar-w);
    min-width: 0;
    max-width: var(--sidebar-w);
    height: 100%;
    background: var(--bg);
    border-right: 1px solid var(--glass-border);
    color: var(--fg2);
    z-index: 6;
    overflow: hidden;
    box-sizing: border-box;
    transition: width 0.2s ease, min-width 0.2s ease;
    padding: 8px 8px 10px;
  }
  .sidebar-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    height: 40px;
    margin-bottom: 8px;
    flex-shrink: 0;
    padding: 0 4px;
  }
  .sidebar-logo {
    width: 28px;
    height: 28px;
    object-fit: contain;
    flex-shrink: 0;
  }
  #sidebar-toggle {
    width: 32px;
    height: 32px;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: var(--r-btn);
    color: rgba(var(--fg-rgb), 0.7);
    flex-shrink: 0;
  }
  #sidebar-toggle:hover { background: rgba(var(--fg-rgb), 0.07); color: var(--fg); }
  #sidebar-toggle svg {
    width: 18px;
    height: 18px;
    transition: transform var(--dur-tap) var(--ease-snap);
    transform-origin: center center;
  }
  #sidebar-toggle:hover svg { transform: scale(1.18); }
  body:not(.sidebar-expanded) .sidebar-head {
    justify-content: center;
    padding: 0;
  }
  body:not(.sidebar-expanded) .sidebar-logo { display: none; }
  body:not(.sidebar-expanded) .sidebar-row,
  body:not(.sidebar-expanded) .sidebar-profile {
    justify-content: center;
    padding: 0;
    gap: 0;
  }

  .sidebar-row {
    display: flex;
    align-items: center;
    gap: 12px;
    width: 100%;
    height: 36px;
    padding: 0 8px;
    border-radius: var(--r-btn);
    color: var(--fg2);
    font-size: 14px;
    font-weight: 550;
    letter-spacing: -0.01em;
    text-align: left;
    flex-shrink: 0;
    overflow: visible;
    white-space: nowrap;
  }
  .sidebar-row:hover { background: rgba(var(--fg-rgb), 0.07); }
  .sidebar-row.active { background: rgba(var(--fg-rgb), 0.1); }
  #sidebar-history[aria-pressed="true"] { background: rgba(var(--fg-rgb), 0.1); }
  #sidebar-budget[hidden] { display: none; }
  .sidebar-icon {
    width: 20px;
    height: 20px;
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: transform var(--dur-tap) var(--ease-snap);
    transform-origin: center center;
  }
  .sidebar-icon svg, .sidebar-icon img {
    width: 18px;
    height: 18px;
    object-fit: cover;
  }
  .sidebar-icon img { border-radius: 3px; }
  .sidebar-row:hover .sidebar-icon { transform: scale(1.2); }
  .sidebar-label {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  body:not(.sidebar-expanded) .sidebar-label,
  body:not(.sidebar-expanded) .sidebar-profile-text { display: none; }

  .sidebar-flags {
    display: flex;
    flex-direction: column;
    gap: 2px;
    margin: 6px 0 8px;
    overflow-y: auto;
    overflow-x: hidden;
    flex: 1 1 auto;
    min-height: 0;
    scrollbar-width: none;
  }
  .sidebar-flags::-webkit-scrollbar { display: none; }
  .sidebar-flag .sidebar-icon img {
    width: 20px;
    height: 20px;
    border-radius: 50%;
  }
  .sidebar-flag.sidebar-flag-selected { background: rgba(var(--fg-rgb), 0.1); }
  .sidebar-flag-selected .sidebar-icon { transform: scale(1.2); }
  /* Tentative trip in the sidebar rail: same dashed-ring language as every
     other "idea" marker in the app (.globe-trip-bubble-idea, .day-loc-icon-idea)
     — a bright dashed ring just outside the flag, drawn on ::after so it rides
     along with the icon's own hover/selected scale instead of needing its own
     transition. inset/border sized down to match this rail's smaller 20px
     flag (vs. the globe's ~54px bubble). */
  .sidebar-flag-idea .sidebar-icon { position: relative; }
  .sidebar-flag-idea .sidebar-icon::after {
    content: '';
    position: absolute;
    inset: -3px;
    border-radius: 50%;
    border: 1.5px dashed rgba(var(--fg-rgb), 0.92);
    pointer-events: none;
  }

  .sidebar-grow { flex: 0 0 8px; }

  .sidebar-profile {
    display: flex;
    align-items: center;
    gap: 10px;
    width: 100%;
    height: 44px;
    padding: 0 6px;
    margin-top: 6px;
    border-radius: var(--r-btn);
    flex-shrink: 0;
    overflow: hidden;
    text-align: left;
  }
  .sidebar-profile:hover { background: rgba(var(--fg-rgb), 0.07); }
  .sidebar-profile-pic {
    width: 30px;
    height: 30px;
    border-radius: 50%;
    flex-shrink: 0;
    background: rgba(var(--fg-rgb), 0.12);
    background-size: cover;
    background-position: center;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 12px;
    font-weight: 600;
    color: var(--fg);
  }
  .sidebar-profile-text {
    display: flex;
    flex-direction: column;
    min-width: 0;
    line-height: 1.2;
  }
  .sidebar-profile-name {
    font-size: 14px;
    font-weight: 500;
    color: var(--fg2);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .sidebar-profile-email {
    font-size: 12px;
    color: rgba(var(--fg-rgb), 0.45);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
}

/* Account avatar dropdown — compact glass menu above the sidebar profile. */
#account-menu[hidden] { display: none !important; }
#account-menu {
  position: fixed;
  inset: 0;
  z-index: 70;
  background: transparent;
}
#account-menu-card {
  position: fixed;
  width: 224px;
  padding: 4px;
  display: flex;
  flex-direction: column;
  gap: 2px;
  background: var(--glass-bg);
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
  border: 1px solid var(--glass-border);
  border-radius: 16px;
  box-shadow: var(--glass-shadow);
}
/* Both only exist on the phone's full page (see the phone-bar block). The
   wrapper is display: contents everywhere else, which removes it from layout
   completely - the buttons stay the card's own children, so the popup is
   byte-identical to before it was wrapped. */
#account-page-head { display: none; }
#account-menu-items { display: contents; }
#account-menu-card button {
  width: 100%;
  display: flex;
  align-items: center;
  gap: 10px;
  text-align: left;
  font-size: 14px;
  font-weight: 500;
  padding: 8px 10px;
  min-height: 32px;
  border-radius: var(--r-btn);
  background: transparent;
  color: var(--fg2);
  border: none;
  cursor: pointer;
}
#account-menu-card .account-menu-icon {
  width: 16px;
  height: 16px;
  flex-shrink: 0;
  display: flex;
  opacity: 0.85;
}
#account-menu-card .account-menu-icon svg { width: 16px; height: 16px; }
#account-menu-card button:hover { background: rgba(var(--fg-rgb), 0.08); }

#account-sheet[hidden] { display: none !important; }
#account-sheet-card {
  max-width: 320px;
  gap: 10px;
}
#account-sheet-body {
  font-size: 14px;
  line-height: 1.45;
  color: rgba(var(--fg-rgb), 0.72);
  margin: 0;
  white-space: pre-line;
}
#account-sheet-close {
  align-self: flex-end;
  margin-top: 8px;
  padding: 8px 14px;
  border-radius: var(--r-btn);
  background: rgba(var(--fg-rgb), 0.08);
  color: var(--fg2);
  font-size: 14px;
  font-weight: 500;
}
#account-sheet-close:hover { background: rgba(var(--fg-rgb), 0.12); }

/* Light/dark switch, inside the Settings sheet — see openAccountSheet and
   setTheme in app.js. A plain iOS-style pill switch, same glass language as
   the rest of the app rather than a native <input type="checkbox">, so it
   matches everything else in the sheet. */
#theme-toggle-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding-top: 4px;
}
#theme-toggle-label {
  font-size: 14px;
  font-weight: 500;
  color: var(--fg2);
}
#theme-toggle {
  position: relative;
  flex: 0 0 auto;
  width: 44px;
  height: 26px;
  border-radius: var(--r-pill);
  background: rgba(var(--fg-rgb), 0.16);
  transition: background 0.18s var(--ease-out);
}
#theme-toggle.on { background: #34c759; }  /* iOS-green "on", same in both themes */
#theme-toggle-knob {
  position: absolute;
  top: 3px;
  left: 3px;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.35);
  transition: transform 0.18s var(--ease-out);
}
#theme-toggle.on #theme-toggle-knob { transform: translateX(18px); }

/* The whale logo (sidebar, home hero, auth screen) is a plain white PNG
   silhouette on transparent — built for the dark background it always sat
   on. brightness(0) turns every visible pixel solid black (alpha is
   untouched, so the silhouette shape survives) without needing a second
   image asset. */
[data-theme="light"] .sidebar-logo,
[data-theme="light"] #ai-empty-hero img,
[data-theme="light"] .mbar-icon-logo img,
[data-theme="light"] #auth-logo {
  filter: brightness(0);
}

/* ---------- New-conversation empty state (Grok-style) ---------- */
#ai-empty-hero {
  display: none;
  align-items: center;
  justify-content: center;
  gap: 14px;
  margin: 0 0 28px;
  flex-shrink: 0;
  pointer-events: none;
}
#ai-empty-hero img {
  width: 52px;
  height: 52px;
  object-fit: contain;
}
#ai-empty-hero span {
  font-size: 44px;
  font-weight: 500;
  letter-spacing: -0.04em;
  color: var(--fg2);
  line-height: 1;
}
#ai-menu.ai-empty-mode #ai-empty-hero { display: flex; }
#ai-menu.ai-empty-mode #ai-menu-thread,
#ai-menu.ai-empty-mode #ai-menu-top-row,
#ai-menu.ai-empty-mode #ai-history-list,
#ai-menu.ai-empty-mode #ai-history-toolbar,
#ai-menu.ai-empty-mode #ai-history-bulk-bar { display: none !important; }
#ai-menu.ai-empty-mode #ai-menu-title { display: none; }
#ai-menu.ai-empty-mode #ai-menu-header {
  position: absolute;
  top: 12px;
  right: 16px;
  left: auto;
  width: auto;
  padding: 0;
  z-index: 2;
}
#ai-menu.ai-empty-mode .globe-menu-card {
  justify-content: center;
  align-items: center;
  position: relative;
}
#ai-menu.ai-empty-mode #ai-menu-form {
  position: relative;
  width: min(560px, calc(100% - 48px));
  max-width: min(560px, calc(100% - 48px));
  margin: 0 auto;
  min-height: 60px;
  padding: 10px 8px;
  border-radius: var(--r-chip);
  align-items: center;
  flex-wrap: wrap;
  background: var(--card-bg);
  border: 1px solid rgba(var(--fg-rgb), 0.1);
  box-shadow: 0 8px 28px rgba(0, 0, 0, 0.35);
  transition: padding 0.22s var(--ease-out);
}
#ai-menu.ai-empty-mode .ai-plus-wrap { align-self: center; }
#ai-menu.ai-empty-mode #ai-menu-input {
  min-height: 36px;
  max-height: 5.5em;
  padding: 6px 12px 6px 6px;
  font-size: 16px;
  line-height: 24px;
  caret-color: var(--fg2);
  transition: height 0.18s var(--ease-out);
}
/* Whichever mode is showing, the native placeholder attribute (a fallback
   for a moment before JS ever runs, and for a chat with no rotating list of
   its own) must not visibly double up with the animated one once the
   overlay is actually on screen. */
#ai-ph-track:not([hidden]) ~ #ai-menu-input::placeholder { color: transparent; }
#ai-menu.ai-empty-mode #ai-menu-mic {
  width: 40px;
  height: 40px;
  border-radius: var(--r-btn);
  transition: margin-top 0.22s var(--ease-out), transform var(--dur-tap) var(--ease-snap), background 0.15s ease;
}
/* Sits inside .ai-input-wrap (see its own comment) — `inset: 0` lands this
   exactly on the textarea's own box in EVERY mode/breakpoint, empty-hero
   and the compact/reservation-chat composer alike, with no left/right px
   math to keep in sync with the buttons around it. */
.ai-ph-track {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  overflow: hidden;
  pointer-events: none;
  z-index: 1;
  /* Matches the textarea's OWN left padding in each mode (6px empty-hero,
     0.5em compact, 8px desktop-landscape — see the three #ai-menu-input
     padding rules), plus a SMALL beat of extra room so the flashing caret
     sits in a narrow gap rather than right on top of the first letter —
     kept tight on purpose (both this gap and the input's own left padding
     above it were trimmed down together) so the caret sits close to both
     the plus icon on its left and the placeholder text on its right. */
  padding-left: 9px;
  box-sizing: border-box;
}
#ai-menu:not(.ai-empty-mode) .ai-ph-track { padding-left: calc(0.5em + 3px); }
@media (orientation: landscape) and (min-width: 1024px) and (pointer: fine) {
  /* Matches the desktop-landscape #ai-menu-input padding-left (8px) above. */
  #ai-menu:not(.ai-empty-mode) .ai-ph-track { padding-left: 11px; }
}
.ai-ph-item {
  font-size: 16px;
  line-height: 24px;
  color: var(--fg2-dim);
  white-space: nowrap;
  transform: translateY(0);
  opacity: 1;
  transition: transform 0.45s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.35s ease;
}
.ai-ph-item.is-leave {
  transform: translateY(-120%);
  opacity: 0;
}
.ai-ph-item.is-enter {
  transform: translateY(120%);
  opacity: 0;
}
@media (prefers-reduced-motion: reduce) {
  .ai-ph-item { transition: opacity 0.2s ease; }
  .ai-ph-item.is-leave, .ai-ph-item.is-enter { transform: none; }
}

/* ============================================================
   PHONE BOTTOM BAR — the whole navigation on a portrait phone.
   Markup + why it exists: see #mobile-bar in index.html.

   ONE media query gates everything below, and it is deliberately narrow:
   portrait AND ≤768px: a phone held upright, including an unfolded
   foldable iPhone, which reports a portrait iPad mini's viewport.
     - A phone in landscape never gets here — #portrait-lock covers it.
     - iPad portrait (≥601px) and every landscape/desktop layout keep the
       globe-corner account button, the globe's trip-flag bubbles and the
       desktop sidebar exactly as they were. This bar is not a second way to
       do what those already do; it is the only way, on the one layout it
       appears on.
   601px is the same phone/tablet line the calendar sizing already draws
   further up this file — one boundary in the app, not two.
   ============================================================ */

/* Bar height WITHOUT the home-indicator inset. The bar adds that inset as
   padding (below), and the fullscreen AI panel subtracts the same total —
   so this knob moves all three together and they cannot drift apart. */
:root { --mbar-h: 58px; }

/* THE BAND BEHIND THE KEYBOARD.

   Lifting the AI sheet's floor onto the keyboard (see #ai-menu.open's bottom
   in the portrait phone block) uncovers everything that was under it — the
   calendar, the globe, and the bar right below — across the band the
   keyboard now occupies. An iPhone keyboard is translucent and the bar is
   glass, so none of that is actually hidden by the keys sitting on top: the
   calendar reads straight through them. This paints that band, and only that
   band, in the app's own background so there is nothing to read through.

   Opaque --bg, never the glass the rest of the app is made of: the whole
   point is that NOTHING shows through, and a blurred pane would still carry
   the calendar's shape.

   It is a real element rather than a pseudo on the sheet because it has to
   paint OVER #mobile-bar (z-index 30, just below), and a pseudo-element
   cannot leave its parent's stacking context — #ai-menu sits at 5, so no
   z-index on a pseudo of it could ever clear the bar. 40 puts this above the
   bar and still far below the panels at 70+ and #portrait-lock at 9999.

   Height is the measured keyboard and nothing else, so with no keyboard up
   it is a zero-height box painting nothing; display:none here keeps it out
   of every layout but the portrait phone one, where the media query turns it
   on. It shares the floor's transition and reads the same --kb, so its top
   edge and the sheet's bottom edge are always the same line and no seam can
   open up between them while either moves. */
#kb-fill {
  display: none;
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  height: var(--kb, 0px);
  background: var(--bg);
  z-index: 40;
  pointer-events: none;
  transition: height 0.22s cubic-bezier(0.22, 0.61, 0.36, 1);
}
@media (prefers-reduced-motion: reduce) {
  #kb-fill { transition: none; }
}

#mobile-bar {
  display: none;                 /* the media query below is the only thing that shows it */
  flex: 0 0 auto;
  z-index: 30;                   /* above the globe (incl. the open AI panel) — the bar
                                    is how you get back out, so it is never covered */
  align-items: stretch;
  height: calc(var(--mbar-h) + env(safe-area-inset-bottom, 0px));
  /* The home indicator's strip, given to the bar as padding rather than left
     for the buttons to sit under. This is the ONE element that clears the
     bottom inset now, which is why #calendar gets to stop trying to. */
  padding: 0 4px env(safe-area-inset-bottom, 0px);
  background: var(--glass-bg);
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
  /* A hairline above, no side/bottom border and no rounding: the bar is
     edge-to-edge chrome, not one of the app's floating glass cards. */
  border-top: 1px solid var(--glass-border);
}
#mobile-bar[hidden] { display: none !important; }

.mbar-item {
  flex: 1 1 0;                   /* equal columns, so 3 items and 4 items both sit evenly */
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 6px 2px;
  border-radius: var(--r-btn);
  color: var(--fg);
  /* Resting items are dimmed and the current one is brought to full white —
     the monochrome stand-in for the coloured active tab in a normal tab bar. */
  opacity: 0.55;
  transition: opacity var(--dur-tap, 0.18s) ease, transform var(--dur-tap, 0.18s) ease,
              background var(--dur-tap, 0.18s) ease;
}
.mbar-item[hidden] { display: none; }
.mbar-item.is-active { opacity: 1; }
.mbar-item:active { transform: scale(0.92); background: var(--press); }

/* Bigger now that the labels are gone: the glyph is the only thing naming the
   button, so it does the whole job on its own, and the room the text used to
   take is exactly what pays for it. The bar's own height is unchanged, so this
   costs nothing elsewhere. */
.mbar-icon {
  width: 28px;
  height: 28px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.mbar-icon svg { width: 100%; height: 100%; display: block; }

/* The whale is a plain white PNG, so it is tinted the same way the sidebar
   and auth-screen copies are (see the logo rule further up this file). */
.mbar-icon-logo img {
  /* Slightly over-filling its box on purpose: the whale artwork carries its
     own transparent margin, so at a straight 100% it reads noticeably
     lighter than the stroked SVG glyphs sitting either side of it. */
  width: 118%;
  height: 118%;
  object-fit: contain;
  display: block;
}

.mbar-profile-pic {
  width: 28px;
  height: 28px;
  border-radius: 50%;
  background-size: cover;
  background-position: center;
  background-color: rgba(var(--fg-rgb), 0.16);
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  font-size: 10px;
  font-weight: 600;
  line-height: 1;
}
.mbar-profile-pic svg { width: 16px; height: 16px; opacity: 0.8; }

@media (orientation: portrait) and (max-width: 768px) {
  #mobile-bar { display: flex; }

  /* The trip flags belong to the LANDSCAPE rail only, where they sit in a
     column under the logo. Down here the bar is globe - money - Ballesta -
     profile and nothing else; the AI panel's own #ai-menu-flags row is the
     trip switcher on this layout. renderRailFlags fills #mbar-trips whatever
     the layout, so the flags are hidden here rather than not built.
     display: contents on the container, not display: none, because the money
     button is parked INSIDE it once a trip is selected - none would take the
     money tab with it; contents drops the wrapper and leaves the money button
     as a normal item of the bar. */
  #mbar-trips { display: contents; }
  #mbar-trips > .mbar-trip { display: none; }

  /* The two halves give up the bar's height. 44% for the globe is the
     Airbnb proportion — a hero that clearly leads the screen without
     becoming the screen — and it leaves the calendar the slightly larger
     share, which it needs because it has to show whole week rows. */
  #globe {
    flex: 0 0 44%;
    height: auto;
    max-height: none;
  }
  #calendar {
    flex: 1 1 auto;
    height: auto;
    max-height: none;
    /* The bar sits below this now and clears the home indicator itself, so
       the old fractional safe-area padding here would just be a second,
       smaller gap stacked on the first. Plain air instead. */
    padding-bottom: 8px;
  }

  /* Retired on this layout — the bar carries the account, and the AI panel's
     own #ai-menu-flags row carries trip switching. Nothing floats on the
     globe any more. */
  #globe-account-btn,
  #globe-trips-bubbles {
    display: none !important;
  }

  /* ============================================================
     THE ACCOUNT IS A PAGE HERE, NOT A POPUP. A 224px card floating off the
     bar is a desktop shape; on a phone the account gets the whole screen,
     the same full sheet the AI panel uses and down to the same bar, so it
     reads as somewhere you went rather than something hovering.
     Same markup and same three buttons as every other layout - only the
     shape changes - so there is one account menu in this app, not two that
     have to be kept saying the same thing.
     The card is `fixed` and gets hand-set left/top from positionAccountMenu
     on the layouts that DO anchor it to a trigger; that function returns
     early here and clears them, or a stale offset would slide this page off
     the screen edge. */
  /* The OVERLAY has to stop above the bar as well, not just the card inside
     it. It is z-index 70 against the bar's 30, so while it spanned the whole
     screen it sat invisibly on top of the bar and swallowed every tap there -
     and since the card now fills the screen there is no "outside" left to tap
     either, which left the account page with no way out at all. Same bottom
     as the card, so the bar is genuinely exposed rather than merely visible. */
  #account-menu {
    bottom: calc(var(--mbar-h) + env(safe-area-inset-bottom, 0px));
  }
  #account-menu-card {
    /* The bar offset is repeated here rather than inherited from the overlay
       above: this card is `position: fixed`, so its inset resolves against the
       viewport, not against its parent. */
    inset: 0 0 calc(var(--mbar-h) + env(safe-area-inset-bottom, 0px)) 0;
    width: auto;
    /* Square against the screen edges and the bar, like the AI sheet: a
       rounded card inside a full-bleed page reads as a mistake. */
    border-radius: 0;
    border-left: none;
    border-right: none;
    border-bottom: none;
    padding: max(10px, env(safe-area-inset-top, 0px)) 12px 12px;
    gap: 4px;
  }
  /* The page is two bands: the picture owns the top third, the options own
     the rest. Both are plain flex shares of the card's own height (0 0 33%
     and 1 1 auto), so they follow whatever height the phone actually has
     rather than any assumed screen size.
     The options are centred as a GROUP in their band - which is what the
     wrapper is for. Centring them individually would only have spread them
     out; they are meant to read as one short list sitting in the middle of
     a lot of room. They stay full-width rows, left-aligned like every other
     row in this app, so the text starts at the page's gutter and the touch
     target still spans the screen. */
  #account-page-head {
    display: flex;
    flex: 0 0 33%;
    align-items: center;
    justify-content: center;
    min-height: 0;
  }
  #account-menu-items {
    display: flex;
    flex: 1 1 auto;
    flex-direction: column;
    justify-content: center;
    align-items: stretch;
    gap: 4px;
    min-height: 0;
  }
  #account-menu-card .account-page-pic {
    /* flex, not block, because of the fallbacks applyProfilePic can put in
       here instead of a photo — two initials, or a generic person glyph.
       Both have to sit centred at this much larger size, where the bar's own
       small-avatar rules don't reach. */
    display: flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
    width: 96px;
    height: 96px;
    border-radius: 50%;
    background-color: rgba(var(--fg-rgb), 0.08);
    background-size: cover;
    background-position: center;
    border: 1px solid var(--glass-border);
    font-size: 34px;
    font-weight: 600;
    color: var(--fg2);
    overflow: hidden;
  }
  #account-menu-card .account-page-pic svg {
    width: 54px;
    height: 54px;
    color: rgba(var(--fg-rgb), 0.55);
  }
  /* Full-width rows on a page, with a real touch target. */
  #account-menu-card button {
    min-height: 48px;
    border-radius: var(--r-chip);
  }

  /* THE FLAG ROW IS THE PHONE'S TRIP SWITCHER, so it gets its own sizing here
     rather than inheriting the globe stack's. Three deliberate differences
     from every other layout:
       1. Smaller circles. --globe-btn * 0.95 instead of the shared 1.08, so
          the row reads as a quiet switcher above the chat instead of the
          biggest thing on a phone screen. The selected one still lands at
          ~54px, comfortably past the 44px touch minimum, and an unselected
          one at ~42px is still an easy tap in a row that only scrolls
          sideways.
       2. No desaturation. Every flag is full colour whatever is selected.
          These are country flags — half of "which trip is this" is the
          colour, and greying out the ones you're not on made the row look
          broken/disabled rather than merely unselected.
       3. Size is therefore the ONLY selected cue, so it has to carry the
          whole job on its own: 1.32 here against the shared 1.22, which
          reads clearly now that colour no longer does any of the work.
     The row's gap widens to match — a bigger scale on a smaller circle needs
     the same visual air between neighbours, not the same number of pixels. */
  #ai-menu-flags { gap: 12px; }
  #ai-menu-flags .globe-trip-bubble { --flag-size: calc(var(--globe-btn) * 0.95); }
  #ai-menu-flags .globe-trip-bubble img { filter: none; }
  #ai-menu-flags .globe-trip-bubble-selected { transform: scale(1.32); }
  #ai-menu-flags .globe-trip-bubble-selected:active { transform: scale(1.24); }
  /* The selected rule above is an id + 2 classes, so it outranks the shared
     .globe-trip-bubble-selected.press-holding long-press feedback no matter
     what order the file is in — without this line, holding the selected flag
     in this row would do nothing at all. Same ~0.97 ease-down off the new
     bigger selected size that the shared pair uses off the old one. */
  #ai-menu-flags .globe-trip-bubble-selected.press-holding { transform: scale(1.28); }

  /* The AI is a full sheet from the top of the screen down to the bar,
     instead of a card confined to the globe's half. `fixed` is what escapes
     #globe's overflow:hidden; the z-index bump is what gets it over
     #calendar, which normally paints above the globe (see the z-index
     comments where those two are defined). */
  #globe.ai-open { z-index: 3; }
  /* THE KEYBOARD LIFTS THE SHEET'S FLOOR, NOT THE SHEET.

     An iPhone keyboard does not resize this app - the shell is fixed inset: 0
     precisely so it cannot (see CLAUDE.md) - it just slides up OVER the
     bottom of the screen, and the composer sits exactly where it lands. So
     the composer is raised by pulling the sheet's own bottom edge up by the
     keyboard's height, which --kb carries (set by syncKeyboardInset in
     app.js, 0 whenever no keyboard is up).

     Raising the FLOOR rather than moving the sheet is what gets the whole
     brief out of one property. The sheet's top never moves, so the flags,
     the plus and the title stay pinned exactly where they were. The card
     inside is height: 100%, so it shortens from the bottom; the composer is
     its last item and rides up on the new floor; and the thread between them
     is the flex item that gives up the height, so the conversation slides up
     under the header and out of sight. Nothing is positioned by hand and
     nothing needs to know how tall anything else is.

     max() rather than a sum, because the two floors are alternatives and not
     things to stack: the bar is UNDER the keyboard while one is up, so adding
     its height would hold the composer a bar's worth above the keys with a
     strip of nothing in between. Whichever floor is higher wins, which is the
     bar normally and the keyboard while it is up.

     The transition is what makes it read as one movement with the keyboard
     rather than a jump after it. Short and ease-out, matched to the keyboard's
     own feel: long enough to be a glide, short enough that the box is already
     there by the time you look for it. */
  #ai-menu.open {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: max(calc(var(--mbar-h) + env(safe-area-inset-bottom, 0px)), var(--kb, 0px));
    transition: bottom 0.22s cubic-bezier(0.22, 0.61, 0.36, 1);
  }
  /* #kb-fill, the band the raised floor leaves behind - see its base rule
     further up for what it is and why it is a real element. Here is the only
     place it is ever visible. */
  #kb-fill { display: block; }

  /* THE MONEY VIEW DROPS THE FLAGS ROW.

     That row is the trip switcher plus a new-chat plus — both about the
     CHAT. In the money view they are a row of controls for somewhere else,
     sitting above the one thing you came to read, and the flags row carries
     generous padding of its own for a selected flag's scale-up, so it costs
     far more vertical space than its height suggests. Dropping it lifts the
     title, the total and the first charges to the top of the card.

     Only here, and only in this view: the switcher is still the switcher on
     the chat side, which is where CLAUDE.md expects it on a phone. */
  #ai-menu.ai-budget-mode #ai-menu-top-row { display: none; }
  /* The header carried no top padding of its own because the flags row above
     always supplied that air. With the row gone the title and total sat
     almost against the card's edge, which reads as cramped rather than as
     lifted. This puts back a deliberate amount — enough to breathe, far less
     than the row it replaces, so the content is still up at the top where
     dropping the row was meant to put it. */
  #ai-menu.ai-budget-mode #ai-menu-header { padding-top: 16px; }
  /* A touch more presence for both lines now they are the whole header — the
     route from the chat's 20px, and the total with it. Scoped to this view so
     the chat's own title is untouched. */
  #ai-menu.ai-budget-mode .ai-menu-title-route { font-size: 22px; }
  #ai-menu.ai-budget-mode #ai-menu-budget-total-amount { font-size: 1.75em; }
  @media (prefers-reduced-motion: reduce) {
    #ai-menu.open { transition: none; }
  }
  #ai-menu.open .globe-menu-card {
    /* Square off against the screen edges and the bar — a rounded card
       floating in a full-bleed sheet reads as a mistake. The top corners
       stay round under the notch. */
    border-radius: 0 0 var(--r-card) var(--r-card);
    border-left: none;
    border-right: none;
    border-bottom: none;
    /* Named, because the landing screen's pinned history clock has to line up
       against this exact value - a pinned child sits against the card's edge
       and never sees this padding, so it has to add it back by hand. */
    --ai-sheet-pad-top: max(10px, env(safe-area-inset-top, 0px));
    --flag-row-pad-top: 14px;
    padding-top: var(--ai-sheet-pad-top);
  }
  /* Nothing to dim: the sheet covers everything above the bar, so the scrim
     would only ever tint pixels no one can see. */
  #ai-menu-scrim { display: none !important; }

  /* Both scrollers sit inside the card's 2em side padding, so on this narrow
     sheet their thin bar landed right against the message/row text and read
     as overlapping it. Bleed each one out to the screen's right edge and put
     the same 2em straight back as padding: the bar rides the edge, the text
     measure is exactly what it was. Same trick the desktop thread already
     uses (see #ai-menu-thread in the desktop media query).
     The card's 2em is 2 x --menu-fs (that variable IS the card's font-size),
     written out that way here because .cost-list sets a font-size of its own
     and a plain `2em` would bleed it by the wrong number of pixels. */
  #ai-menu-thread,
  #ai-menu.open .cost-list {
    margin-right: calc(-2 * var(--menu-fs));
    padding-right: calc(2 * var(--menu-fs));
  }

  /* Money section reads a touch smaller on a phone. One knob: everything in
     the budget view sizes off this, and the em-based bits below it (city
     name, subtotal, cents, the total row) come down with it. The amount box
     is pinned in px rather than em, so it needs saying again. */
  #ai-menu-budget-view { font-size: 15px; }
  .cost-item-amount-wrap { font-size: 15px; }
  /* Deliberately NOT shrunk: the two boxes you actually type in. `.cost-item
     input` holds them at 16px, and under that iPhone Safari zooms the whole
     page the moment one takes focus — the same guard the composer carries.
     The amount you READ (.cost-item-money-read) is a span, not the input, so
     it follows the wrap above down to 15px like everything else. */

  /* ============================================================
     THE LANDING SCREEN. This is the state the phone OPENS in: the Ballesta
     hero in the middle of the sheet, and one control in each top corner.
       TOP LEFT  — the reservation bubbles (#ai-menu-top-row), tentative
                   (dashed ring) and confirmed alike, all in their plain
                   unselected state, because the chat you land in isn't tied
                   to any of them yet. What keeps them unselected is JS, not
                   this file: see updateSelectedTripFlags, which is scoped to
                   the globe's own stack so the calendar's month can't reach
                   in and light one up here.
       TOP RIGHT — the history clock, alone. The new-chat plus is hidden on
                   this screen for the plain reason that this screen IS a new
                   chat; offering it here does nothing you're not already in.
                   It comes straight back the moment you're in a real
                   conversation, which is the only place it means anything.
     Both corners share one `top`, and it goes through env(safe-area-inset-top)
     rather than a bare 12px: these are absolutely positioned, so they are
     placed against the card's padding EDGE and simply do not see the card's
     own safe-area padding-top — a bare offset puts them under the Dynamic
     Island on a modern iPhone. The 64px right stop on the flag row is what
     keeps a long row of trips scrolling under its own edge instead of sliding
     beneath the clock.
     `[hidden]` still wins over the display below, so an account with no trips
     yet shows an empty corner rather than an empty box. */
  /* The row stays in NORMAL FLOW here - deliberately, and this is the whole
     fix for "the bubble jumps when I open it". Pinning it to the corner on
     the landing screen only, while every other state left it in flow, meant
     the two states disagreed about where the same bubble lives: flow puts it
     inside the card's padding, absolute puts it against the card's edge. So
     tapping a bubble slid it down and to the right on its way into the chat.
     One position in both states is the only way that shift cannot exist -
     the bubble now does nothing on open but scale up where it stands. */
  #ai-menu.ai-empty-mode #ai-menu-top-row:not([hidden]) { display: flex !important; }
  /* AND the card must not re-center its children on this screen, which is the
     actual reason a bubble jumped when you opened it. The shared empty-mode
     rule centers everything in the card so the Ballesta hero sits in the
     middle; a side effect is that the flag row got centered too, then snapped
     back to the top-left corner the instant a chat opened and the card went
     back to its normal top-down stack. Same bubble, two different homes.
     So: keep the normal top-down stack here, and center the HERO by itself
     instead (margin: auto in a column flex eats the leftover space above and
     below it, which also keeps the composer pinned at the bottom where it
     already was). The flag row is then the first item in the stack in BOTH
     states — identical position, nothing left to shift. All the bubble does
     on open is scale up where it stands. */
  #ai-menu.ai-empty-mode .globe-menu-card {
    justify-content: flex-start;
    align-items: stretch;
  }
  #ai-menu.ai-empty-mode #ai-empty-hero { margin: auto; }
  /* On the phone the home screen's text bar is the SAME bar as the one in an
     open reservation chat, so it must be the same width. The shared empty-mode
     rule shrinks it (a 24px gutter each side) which looks right on a wide
     desktop card and just looks narrow on a phone. Here it fills the card's
     content width, exactly like the chat composer below the thread. */
  #ai-menu.ai-empty-mode #ai-menu-form {
    width: 100%;
    max-width: 100%;
  }
  /* THE ONE SLOT AT THE END OF THE BUBBLES' ROW. The home screen shows the
     clock in it; a reservation chat shows the plus. They are siblings in the
     same row with only one displayed at a time, so the icon is REPLACED in
     place — the glyph changes and nothing moves.
     That is why the pinned header is hidden here instead of being aligned to
     the row by hand. Pinning measured from the card's edge while the row's
     own button measured from the card's padding, so the two sat exactly one
     card-padding apart: the jump you saw crossing between the two screens.
     Two positioning systems can be lined up, but they cannot be guaranteed to
     stay lined up; one slot has nothing to keep in sync. The header keeps its
     hidden title and its own buttons for every other layout - only the phone
     takes this path. */
  #ai-menu.ai-empty-mode #ai-menu-header { display: none !important; }
  #ai-menu.ai-empty-mode #ai-menu-history-top { display: flex; }
  #ai-menu.ai-empty-mode #ai-new-chat-btn { display: none !important; }

  /* The shared open animation is a scale-pop from the card's centre, which
     is right for a small bubble anchored to a corner and wrong for a sheet
     that covers the screen — at this size it reads as the whole app lurching.
     A sheet rises from the bar it was summoned by, the way every other sheet
     on the phone does. */
  #ai-menu.open .globe-menu-card {
    animation: mbar-sheet-up 0.3s var(--ease-out) both;
  }
}
@keyframes mbar-sheet-up {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: translateY(0); }
}
/* Scoped to the same layout as the animation above, because the general
   reduced-motion rule for this card sits earlier in the file and would
   otherwise lose the cascade to it. */
@media (orientation: portrait) and (max-width: 768px) and (prefers-reduced-motion: reduce) {
  #ai-menu.open .globe-menu-card { animation: none; }
}

/* ---------- THE CONTEXT MENU (long-press or right-click) ----------
   ONE look for every context menu in the app. The overlays keep their own
   contents — Edit/Delete/Cancel for a calendar entry, Delete/Cancel for a line
   in the cost column, the named Delete-this-itinerary for a trip's flag — and
   this only changes their SIZE and WHERE THEY SIT, so the gesture answers the
   same way wherever you aim it. JS puts `.menu-anchored` on the overlay and
   hangs the card off whatever was acted on — the pointer for a mouse, the
   pressed element itself for a finger (openMenuAt / menuAnchor in app.js),
   which is how iOS and Android have drawn a long-press menu for years.

   Row sizes follow the INPUT, not the screen width: see the `pointer: coarse`
   block below. A phone gets 44px rows whatever its size, and a small desktop
   window keeps the compact ones, because what matters is whether the thing
   doing the pointing is a fingertip or a cursor.

   The overlay stays full-window so a click anywhere outside closes the menu. It
   just stops dimming and blurring, because a small menu at the cursor is not a
   modal and blacking out the app around it would read like one.

   Each rule names its three overlays rather than reading `.menu-anchored` alone:
   the sheet rules these override are themselves written with ids, and a bare
   class loses to an id however far down the file it sits. Naming them also
   says plainly which menus opt into this. */
#item-menu.menu-anchored, #cost-row-menu.menu-anchored, #trip-menu.menu-anchored {
  display: block;
  padding: 0;
  background: none;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  animation: none;
}
/* One pane of glass with flat rows inside — the same shape the budget category
   picker uses (#cost-type-menu-card) — instead of the sheets' separately
   floating panes, which at this size read as clutter. */
#item-menu.menu-anchored > div, #cost-row-menu.menu-anchored > div, #trip-menu.menu-anchored > div {
  position: absolute;
  width: max-content;
  min-width: 132px;
  max-width: 260px;
  gap: 1px;
  padding: 4px;
  border-radius: 12px;
  background: var(--glass-bg);
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
  border: 1px solid var(--glass-border);
  box-shadow: var(--glass-shadow);
  transform-origin: top left;   /* so it grows out of the click, not out of thin air */
  animation: pop-in 0.14s var(--ease-out) both;
}
#item-menu.menu-anchored button, #cost-row-menu.menu-anchored button, #trip-menu.menu-anchored button {
  padding: 7px 10px;
  border: none;
  border-radius: 8px;
  box-shadow: none;
  background: none;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  font-size: 13px;
  font-weight: 400;
  color: var(--fg);
  text-align: left;
  white-space: nowrap;
  animation: none;   /* the card pops in as one piece; rows staggering inside it looks fussy this small */
}
/* The sheets lean on weight and tone to separate their big buttons. At this
   size that reads as noise, so the rows are one plain list (the two rules
   above) and the pointer's hover does the highlighting instead. */
@media (pointer: fine) {
  #item-menu.menu-anchored button:hover, #cost-row-menu.menu-anchored button:hover, #trip-menu.menu-anchored button:hover {
    background: var(--hover);
    box-shadow: none;
  }
}
/* A trip's menu names the trip first (#trip-menu-title) — as a sheet that is a
   centred caption, here it is the quiet header a desktop menu puts above its
   commands, with a hairline under it so the title and the command it applies
   to don't read as two rows of the same list. */
#trip-menu.menu-anchored #trip-menu-title {
  margin: 2px 0 4px;
  padding: 2px 10px 6px;
  border-bottom: 1px solid var(--glass-border);
  text-align: left;
  font-size: 12px;
  text-shadow: none;
  animation: none;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
@media (prefers-reduced-motion: reduce) {
  #item-menu.menu-anchored > div, #cost-row-menu.menu-anchored > div, #trip-menu.menu-anchored > div { animation: none; }
}

/* Flipped above its anchor because there was no room below (placeMenuAnchored)
   — grow out of the bottom edge that now sits against it, not the top. */
#item-menu.menu-anchored > div.flip-up,
#cost-row-menu.menu-anchored > div.flip-up,
#trip-menu.menu-anchored > div.flip-up { transform-origin: bottom left; }

/* Cancel is a way out, not a third thing you might do to the reservation, so a
   hairline sets it apart from the commands. Drawn as its own layer rather than
   a border on the button, so the row's hover highlight doesn't swallow it. */
#item-menu.menu-anchored #item-cancel,
#cost-row-menu.menu-anchored #cost-row-cancel,
#trip-menu.menu-anchored #trip-cancel { margin-top: 5px; position: relative; }
#item-menu.menu-anchored #item-cancel::before,
#cost-row-menu.menu-anchored #cost-row-cancel::before,
#trip-menu.menu-anchored #trip-cancel::before {
  content: '';
  position: absolute;
  left: 6px; right: 6px; top: -3px;
  height: 1px;
  background: var(--glass-border);
}

/* ---------- FINGER SIZES ----------
   A cursor lands on a pixel; a fingertip covers about a centimetre. Apple asks
   for 44pt minimum, Android for a 48dp target — the 44px row plus the card's
   own padding clears both. Keyed on the POINTER rather than a width
   breakpoint, so a phone gets these at any size and a narrow desktop window
   does not. */
@media (pointer: coarse) {
  #item-menu.menu-anchored > div,
  #cost-row-menu.menu-anchored > div,
  #trip-menu.menu-anchored > div {
    min-width: 200px;
    max-width: min(300px, calc(100vw - 32px));
    padding: 6px;
    border-radius: 16px;
  }
  #item-menu.menu-anchored button,
  #cost-row-menu.menu-anchored button,
  #trip-menu.menu-anchored button {
    min-height: 44px;
    padding: 11px 16px;
    border-radius: 11px;
    font-size: 16px;
  }
  #trip-menu.menu-anchored #trip-menu-title {
    padding: 4px 16px 8px;
    font-size: 13px;
  }
}

/* ---------- WHILE THE PRESS IS BEING HELD ----------
   Half a second is a long time to hold something that does not react. iOS and
   Android both answer the press before the menu opens; an iPhone web app
   cannot buzz (Safari exposes no vibration API), so this is the answer: the
   thing under the finger eases down and comes back the moment the press ends,
   turns into a scroll, or opens the menu. See attachMenuPress in app.js. */
.press-holding {
  transform: scale(0.97);
  transition: transform 0.35s var(--ease-out);
}
@media (prefers-reduced-motion: reduce) {
  .press-holding { transform: none; transition: none; }
}
/* A selected trip flag is already enlarged (.globe-trip-bubble-selected), so it
   has to ease down FROM there — a bare scale(0.97) would shrink it to smaller
   than its unselected neighbours the moment you touched it. */
.globe-trip-bubble-selected.press-holding { transform: scale(1.18); }

/* Switching the AI chat / cost sheet to a different trip without reopening
   anything: the old content fades out, the new content fades in. One class,
   toggled by swapPanelContent() in app.js. */
.panel-swapping { opacity: 0; }
#ai-menu-thread, #cost-panel-list { transition: opacity .16s ease; }

/* Placed at the END of the file on purpose: the base #mobile-bar rule that
   hides it and sizes it for the bottom edge comes later in the source than
   where this logically belongs, and at equal specificity the later rule
   wins - the first version of this sat earlier and was silently overridden
   back to display: none. */
/* THE BAR STANDS UP: LANDSCAPE ON A BIG TOUCH SCREEN.

   iPad mini turned sideways. The same four-button nav the phone carries
   along the bottom stands as a vertical rail down the RIGHT edge, and the
   row reads globe | calendar | rail from left to right. That order needs no
   ordering: #app is already flex-direction: row in landscape and the three
   are in that order in the markup, so the rail simply joins the end.

   Phones in landscape are deliberately NOT here. They stay portrait locked
   (#portrait-lock, which stops at 926px), so this band starts above it and
   the two can never both apply.

   .mbar-item is flex: 1 1 0, so turning the axis is all it takes to get four
   evenly stacked buttons instead of four even columns - nothing per-button
   changes. The bar's own portrait rules assume the bottom edge, though, so
   the three that encode that (its height, its bottom-inset padding and its
   hairline on top) are each restated for a side: a width, the top/bottom
   insets, and the hairline on the left.

   The calendar keeps its 50% and the globe absorbs the rail's width, since
   the globe is the flexible half (flex: 1 1 50%) and the calendar the fixed
   one. */
@media (orientation: landscape) and (pointer: coarse) and (min-width: 1024px) {
  #mobile-bar {
    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: 4px;
    /* Was a height; on a side rail the same measure is a width, plus the
       inset for a home indicator or notch on the right edge. */
    height: auto;
    width: calc(var(--mbar-h) + env(safe-area-inset-right, 0px));
    padding: env(safe-area-inset-top, 0px) env(safe-area-inset-right, 0px)
             env(safe-area-inset-bottom, 0px) 0;
    /* The hairline moves from the top edge to the leading one, so it still
       reads as the seam between the nav and the content it sits beside. */
    border-top: none;
    border-left: 1px solid rgba(var(--fg-rgb), 0.12);
  }
  /* Four buttons up and down, each taking a quarter of the height - but
     capped, so on a short landscape screen they stay buttons rather than
     stretching into tall bands. 78, down from 88, with the rest of this
     layout's controls. */
  .mbar-item { max-height: 78px; }

  /* THE RAIL ITSELF COMES DOWN A NOTCH: 52px, from 58.

     --mbar-h is the one measurement the whole layout is drawn from - the
     rail's width, the grid's third column, where the AI panel starts and
     stops, where the email and photo panels stop, and (through --cal-sq) how
     big a day square is. So it is changed HERE and nowhere else, and every
     one of those follows on its own. The 6px it gives back go to the two
     content columns, three each. */
  :root { --mbar-h: 52px; }

  /* An even split of what the rail leaves. The shared landscape rule gives
     the calendar a fixed 50% of the WHOLE width and lets the globe take
     whatever is left, which was fine when they were the only two - with the
     rail taking its 58px off the end it came out 454 against 512. Both
     flexible from a zero basis instead, so they halve the remainder between
     them however wide the rail ends up being.

     A zero basis with flex-grow does NOT do it, though it looks like it
     should: the calendar carries 32px of horizontal padding and Chrome adds
     that on top of a zero flex-basis, so "equal shares of the free space"
     came out 467 against 499 - the calendar quietly keeping the difference.
     Both were reporting flex: 1 1 0 and min-width: 0 at the time, which is
     what makes it worth writing down.

     So the half is stated outright instead of being arrived at: the row
     minus the rail, divided by two. border-box means the calendar's padding
     sits inside its share rather than being added to it, and min-width: 0
     lets the columns hold that share even though seven day-squares would
     rather be wider. */
  /* FOUR REGIONS OUT OF THREE CHILDREN, WITHOUT MOVING ANY OF THEM.

     Wanted: globe over calendar down the left, the AI panel beside them, the
     rail hard right. #app's children are globe, calendar and the rail - the
     AI panel is not among them, it lives INSIDE #globe so it can overlay the
     globe on every other layout, and moving it in the markup would break all
     of those. So #app becomes a grid: two columns and two rows, the globe and
     the calendar stacked in column 1, the rail spanning both rows in column
     3, and column 2 left deliberately EMPTY for the AI panel to be placed
     over by position: fixed (below). A grid area nothing is assigned to
     simply stays blank, which is exactly the hole we want.

     This replaces the even-halves flex rule that was here: the split is now
     the grid's own 1fr 1fr, and the calendar's padding cannot skew it the
     way it skewed a zero flex-basis. */
  /* The three measurements the calendar's height is built from. Named
     rather than folded into one number so each can be checked against the
     rule that actually produces it:

       --cal-gap     the 4px between week rows, from #days' own grid gap.

       --cal-sq      the day square. The grid fills the column and .day is
                     aspect-ratio: 1, so the square IS the column's width
                     less its 32px of padding and the six 4px column gaps,
                     divided by seven. Not a size chosen here - that size
                     written down, so the height can be worked out from it.

       --cal-chrome  everything in the pane that is not the grid: the top
                     padding, the month title and the gap under it, the
                     weekday letters and the gap under them, and the floor.
                     Measured off the rendered pane (calendar.offsetHeight
                     minus days.clientHeight), and the only part of this
                     maintained by hand - if the title or the letters change
                     size, re-measure and change this with them. It came
                     down from 82 to 76 when the weekday letters went to the
                     portrait iPhone's 11px.

       --cal-guard   deliberate slack, and the reason it exists: --cal-sq is
                     fractional (61.43px at 1024 wide), so five rows plus
                     four gaps is a fractional height too, and a row asked
                     for its exact sum can lose its last pixel to rounding
                     and show four rows and a sliver. Six pixels is enough to
                     absorb that at any width this layout runs at, and it is
                     named rather than buried in --cal-chrome so nobody
                     "corrects" the chrome measurement back into a lie. */
  :root {
    --cal-gap: 4px;
    --cal-sq: calc(
      ((100vw - var(--mbar-h) - env(safe-area-inset-right, 0px)) / 2 - 32px - 24px) / 7
    );
    --cal-chrome: 76px;
    --cal-guard: 6px;
  }

  #app {
    display: grid;
    grid-template-columns: 1fr 1fr calc(var(--mbar-h) + env(safe-area-inset-right, 0px));
    /* FIVE WHOLE WEEK ROWS, ALWAYS - the one hard requirement of this pane.

       An even 1fr 1fr split does not deliver it. The day square is sized
       from the column's WIDTH (the phone recipe below), so how much HEIGHT
       five rows of it need is a number the split has no say in - at 1024
       wide the square lands at 61px, five rows want 321px, and half the
       screen leaves the scroller 302. Four and a bit rows, every time.

       So the calendar row is given exactly what five rows need and the
       globe takes the rest, rather than the two halving the screen and the
       calendar making do. Each part of that height is named below, so the
       expression stays readable and stays true if any one of them changes.

       The globe is the 1fr because it is the part that can absorb a change
       of a few px without anything breaking; the calendar cannot. */
    grid-template-rows: 1fr calc(
      var(--cal-chrome) + 5 * var(--cal-sq) + 4 * var(--cal-gap) + var(--cal-guard)
    );
  }
  #globe    { grid-column: 1; grid-row: 1; min-width: 0; min-height: 0; }
  #calendar { grid-column: 1; grid-row: 2; min-width: 0; min-height: 0; }
  #mobile-bar { grid-column: 3; grid-row: 1 / -1; width: auto; }

  /* The AI panel fills the empty middle column. Fixed rather than gridded
     because it is not a child of #app - see above - so it is positioned
     against the viewport instead: from the halfway line across to the rail,
     full height. Same trick the phone sheet already uses (#ai-menu.open is
     fixed there too), just a different box. */
  #ai-menu.open {
    position: fixed;
    top: 0;
    bottom: 0;
    left: calc((100% - var(--mbar-h) - env(safe-area-inset-right, 0px)) / 2);
    right: calc(var(--mbar-h) + env(safe-area-inset-right, 0px));
    width: auto;
    max-width: none;
    height: auto;
    align-items: stretch;
  }
  #ai-menu.open .globe-menu-card {
    width: 100%;
    height: 100%;
    max-width: none;
    max-height: none;
    border-radius: 0;
    border-top: none;
    border-bottom: none;
    border-right: none;
  }

  /* NOTHING ON THE GLOBE BUT THE GLOBE.

     On this layout the rail is the navigation, so the controls that float
     over the globe are a second copy of it sitting on top of the thing they
     are covering. The account button's job belongs to the rail's profile
     button and the trip bubbles' to the AI panel's own flag row, so nothing
     here is the only way to reach anything. The reservation pins go too:
     this pane is the Earth, and the day's stops are read in the calendar
     beside it. Scoped to this band - every other layout keeps all of it,
     which is why these are hidden rather than removed. */
  /* The floating CONTROLS come off the globe - their jobs belong to the
     rail's profile button and the AI panel's flag row, so nothing here is
     the only way to reach anything.

     The reservation PINS stay. They were taken out with the controls under
     "nothing on the globe but the globe", and that was too wide a cut: a pin
     is not chrome sitting on top of the map, it is the map saying where the
     reservation is. Tapping a reservation and watching the globe stay blank
     is the whole point of the pane failing. */
  #globe-account-btn,
  #globe-trips-bubbles { display: none; }

  /* No scrim. It exists to dim and blur whatever the panel is covering while
     the panel is a temporary overlay - here the panel covers nothing, it has
     its own column, so the scrim would only be frosting the globe next to it
     for no reason. Desktop drops it for the same reason. */
  #ai-menu-scrim { display: none !important; }

  /* No globe button on the rail either: it exists to bring the globe and
     calendar back to the front, and on this layout they are always in front -
     they have their own column and nothing ever covers them. */
  #mbar-home { display: none; }

  /* Profile at the foot, everything else centred above it. The auto margins
     go on the MIDDLE item, not the bottom one: margin-top: auto on the
     profile alone does pin it down, but it eats every scrap of free space on
     the way, which shoved the logo hard against the top of the rail. Auto on
     both sides of the logo instead splits that space evenly above and below
     it, so it floats in the middle of what the profile leaves.

     This is also the shape the rest of this belongs in: the saved
     reservations will sit in that same middle run, sharing it with the logo,
     and auto margins on the group will keep the whole run centred however
     many there are.

     The account button is taken OUT OF THE FLOW to do it. Left in, it is
     still a flex item, so the space the middle run gets centred in is the
     rail minus the button - the logo came out at 338 on a 768 rail, 46px
     high of true centre, visibly sitting above the middle. Absolute against
     the rail, it holds the same spot at the foot while contributing nothing
     to the centring, so justify-content has the FULL height to work with and
     the middle run lands on the rail's real centre. That is also why it
     needs no auto margin of its own any more. */
  /* ONE SQUARE, SHARED. The logo and every reservation under it are the
     same control in the same column, so they are given the same box rather
     than each being sized by its own contents - a flag glyph and a whale PNG
     would otherwise measure differently and the outlines would not match.
     --rail-btn is that box: an explicit width AND height with aspect-ratio
     as the backstop, so it is square whatever lands inside it.

     flex: 0 0 auto on top of .mbar-item's flex: 1 1 0 - the rail is a column
     now, and an item that grows would stretch into a tall rectangle. */
  #mbar-trips {
    /* 34, down from 38: this is a phone that happens to be unfolded, not a
       tablet, and at this pixel density a 38px flag read as a desktop-sized
       control. Everything in the column is drawn from this one box, so the
       logo and every flag step down together. */
    --rail-btn: 34px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 10px;
    margin-top: 10px;
  }
  #mbar-ai,
  .mbar-trip {
    flex: 0 0 auto;
    position: relative;
    width: var(--rail-btn, 38px);
    height: var(--rail-btn, 38px);
    max-height: var(--rail-btn, 38px);
    aspect-ratio: 1;
    padding: 0;
    align-self: center;
    /* No border, at rest or pressed. Desktop says a button here with a soft
       background fill and nothing else - .sidebar-row, .sidebar-flag-selected
       and #cost-fab[aria-pressed] all do it with rgba(fg, .07-.1) - and a
       white ring on the same control in the same app is a second dialect for
       the same idea. The fill is what says "tappable" now. */
    border: 0;
    border-radius: var(--r-btn);
  }
  #mbar-ai:active { background: rgba(var(--fg-rgb), 0.1); }
  /* :focus-visible only - a keyboard ring, which a finger never triggers, so
     nothing on this layout shows a ring in normal use. Kept rather than
     deleted because removing it would leave keyboard users with no idea
     where they are; desktop keeps its own for the same reason. */
  .mbar-trip:focus-visible {
    outline: 2px solid rgba(var(--fg-rgb), 0.45);
    outline-offset: 2px;
  }
  /* --rail-btn is declared on the trips container, which the logo is not
     inside, so it gets its own copy of the same number. One value, named
     once here, both readers pointed at it. */
  #mbar-ai { --rail-btn: 38px; }
  /* A tentative trip keeps its dashed ring at rest, and it is the one ring
     here that does. The rest of the outlines in this column are FEEDBACK -
     you pressed this, this has focus - and feedback has no business being
     on screen when nothing is happening. A dashed ring is not feedback: it
     is what the reservation IS, the same thing .globe-trip-bubble-idea and
     .sidebar-flag-idea say wherever else a tentative trip appears, and
     hiding it loses information rather than tidying the view. Quieter than
     the pressed state below, so a press still reads on top of it. */
  /* Tentative: the app's dashed ring, drawn the app's way - an ::after at
     inset 0 with a round border, exactly as .globe-trip-bubble-idea does it,
     so it reads as the same mark rather than a lookalike. The padding keeps
     the flag off that ring; overflow: hidden above is what makes drawing it
     inside the box the right move instead of on the edge. */
  .mbar-trip-idea { padding: 2px; }
  .mbar-trip-idea::after {
    content: '';
    position: absolute;
    inset: 0;
    border-radius: 50%;
    border: 1.75px dashed rgba(var(--fg-rgb), 0.92);
    pointer-events: none;
  }
  /* The selected trip is shown by SIZE alone now - a permanent ring on it
     would be exactly the always-there outline this change removes, and the
     dim is gone too. .mbar-item rests at opacity 0.55, which is right for
     the nav buttons but is a second off/on signal on a flag, so the flags
     opt out of it and every one of them sits at full strength. */
  .mbar-trip,
  .mbar-trip.is-active { opacity: 1; }
  /* A FLAG IS A CIRCLE, EVERYWHERE.

     The flag fills a round button, and a CONFIRMED flag carries no outline
     at all - the same rule .globe-trip-bubble follows. */
  .mbar-trip {
    border-radius: 50%;
    border-color: transparent;
    overflow: hidden;
  }
  .mbar-trip .mbar-icon { width: 100%; height: 100%; }
  .mbar-trip img,
  .mbar-trip svg {
    width: 100%;
    height: 100%;
    border-radius: 50%;
    object-fit: cover;
    display: block;
  }

  /* SIZE IS THE ONLY OFF/ON SIGNAL ON THIS RAIL. Every flag stays full
     colour at full opacity whether or not it is the one you are on - the
     grayscale/brightness wash and the resting opacity dim that used to mark
     the unselected ones were removed on purpose, so do not reintroduce
     them here. The selected flag is simply bigger.

     The SELECTED size is the fixed point here, not the resting one. 1.08 of
     the 34px box is ~36.7px, and that is the size that was signed off, so it
     is the number that must not move: any further tuning of how loud the
     off/on step reads is done by taking the RESTING flag down, never by
     pushing the active one up.

     Resting sits at 0.76, ~25.8px. It started level with the box at 1.0 and
     came down three times, because with colour and opacity no longer
     carrying any of the signal, size is carrying all of it - a step that
     would read as plenty when it was reinforcing a grey-to-colour change
     reads as nearly nothing on its own. ~11px between the two is where the
     selected flag actually announces itself.

     Both stay inside the 10px gap between flags, so nothing collides at
     either size. Transitioned, so picking a different trip hands the size
     from one to the other rather than snapping. */
  .mbar-trip { transition: transform 0.22s var(--ease-snap); }
  .mbar-trip { transform: scale(0.76); }
  .mbar-trip.is-active { transform: scale(1.08); }

  /* The cost button, parked under the selected flag by renderRailFlags.
     Same square as everything else in this column. */
  #mbar-trips > #mbar-money {
    flex: 0 0 auto;
    width: var(--rail-btn, 38px);
    height: var(--rail-btn, 38px);
    aspect-ratio: 1;
    padding: 0;
    border: 0;
    border-radius: var(--r-btn);
    /* NO ENTRANCE ANIMATION, ON PURPOSE - it was hiding the button.

       It used to open by growing, which is what carried the buttons below
       it. The trouble is that this row rebuilds often, every rebuild lifts
       this node out and puts it back, and re-inserting a node restarts its
       CSS animation. Restarted faster than its 0.26s could play, the button
       sat frozen on the animation's first frame - 30px wide and ZERO tall,
       which is to say invisible - and it stayed that way.

       An animation that can leave a control missing is worse than no
       animation. The slide is gone until the rebuild loop behind it is
       found and fixed; the button is simply there. Keeping the keyframes
       below, unused, so whoever fixes that has the movement to put back. */
  }
  /* Same fill the desktop cost fab uses for its own pressed state. */
  #mbar-trips > #mbar-money:active,
  #mbar-trips > #mbar-money[aria-pressed="true"] { background: rgba(var(--fg-rgb), 0.1); }
  @keyframes rail-cost-in {
    from { height: 0; margin-top: -10px; opacity: 0; transform: scale(0.8); }
    to   { height: var(--rail-btn, 38px); margin-top: 0; opacity: 0.55; transform: none; }
  }
  @media (prefers-reduced-motion: reduce) {
    .mbar-trip { transition: none; }
    #mbar-trips > #mbar-money { animation: none; }
  }

  /* A reservation's chat shows its title and nothing else up top.

     The row above it is the trip switcher and a new-chat plus. On this
     layout the switcher is already the column of flags on the rail, three
     inches to the right, so the panel's own copy is the same control twice
     on one screen - and it sits above the title of the very reservation you
     just picked with it. .ai-trip-chat is the panel's existing "this chat
     belongs to a reservation" class (the header's new-chat/history pair
     already keys off it), so a chat NOT opened from a reservation keeps its
     row here.

     Padding back on the header for the same reason as the money view: the
     header carries no top padding of its own because that row always
     supplied it, and without it the title sits against the card's edge. */
  #ai-menu.ai-trip-chat #ai-menu-top-row { display: none; }
  #ai-menu.ai-trip-chat #ai-menu-header { padding-top: 16px; }

  #mobile-bar { position: relative; }
  #mbar-profile,
  #mbar-login {
    /* The corner has to read as a corner, so the avatar sits the same
       distance from the bottom edge as from the right one.

       The side gap is not chosen anywhere - it falls out of the rail
       centring a --rail-avatar-wide picture in a --mbar-h-wide column, which
       is (58 - 28) / 2 = 15px. The bottom gap was the button's own 6px of
       padding, so the avatar sat 15 in from the side and 6 up from the
       floor and the corner looked lopsided. Taking the vertical padding off
       and insetting the button by that SAME expression makes the two gaps
       one number: change the rail's width or the avatar's size and they
       still match, because neither side is written down twice. */
    --rail-avatar: 26px;
    position: absolute;
    left: 0;
    right: 0;
    bottom: calc((var(--mbar-h) - var(--rail-avatar)) / 2);
    padding-block: 0;
  }

  /* THE CALENDAR IS BUILT THE PORTRAIT iPHONE'S WAY, NOT THE DESKTOP'S.

     This pane is a globe stacked over a calendar in one narrow column. That
     is the PHONE's shape, not the desktop's side-by-side halves - but the
     calendar in here was inheriting the shared `@media (orientation:
     landscape)` recipe, which is the desktop one, and every symptom of
     things not fitting in this column traced back to that recipe's
     assumptions rather than to any single wrong number:

       --sq sizes the day square as the smaller of a height-fit and a
       width-fit, both written for a calendar that owns a FULL-height HALF of
       the screen. Here it owns half the height of half the width, so the
       height-fit always won and the squares came out smaller than the column
       could hold - 55px in a column with room for 61.

       #weekdays and #days are then capped to 7 x --sq + 24px and centred.
       With the square already undersized, the grid sat 43px narrower than
       its own column, so the month floated in the middle of a box it did not
       fill, with dead air down both sides.

       #days is flex: 0 1 auto with a max-height of exactly --cal-rows rows,
       and the block is centred by auto margins. A scroller that refuses to
       take the height it is given cannot use the room it has - whatever the
       pane's real height turns out to be, the grid keeps its own idea of it.

     The phone has none of this. Its recipe is two rules and no arithmetic:
     the seven columns divide the pane's WIDTH, .day is aspect-ratio: 1 so
     the square falls out of that, and #days is flex: 1 with overflow-y:
     auto, so the grid takes every pixel of height left under the title and
     scrolls for the rest of the month. Nothing is measured against the
     viewport, so nothing can be measured against the wrong box.

     So this pane opts out of the landscape recipe and takes the phone's. The
     restated --sq and --cal-rows that used to sit here are GONE rather than
     re-tuned: with the caps below lifted nothing reads them any more, and
     leaving them would have been a third set of numbers to keep in step.

     The type is deliberately not touched - it was matched to the portrait
     iPhone already (21px month, 12.5px day, at the foot of this block). */

  /* The grid fills its column, the way it fills the screen on a phone. */
  #weekdays,
  #days {
    width: 100%;
    max-width: none;
    margin-inline: 0;
  }
  /* The scroller takes the height that is left and scrolls for the rest -
     .day's aspect-ratio: 1 then makes the squares as big as the column's
     width allows, which is the only measurement in the whole recipe. */
  #days {
    flex: 1;
    max-height: none;
    margin-bottom: 0;
  }

  /* THE TITLE SITS ON THE GRID, NOT ABOVE A GAP.

     Both of these belong to the same landscape recipe. #calendar-header's
     margin-top: auto (with #days' margin-bottom: auto, dropped just above)
     centres the block in a full-height pane, which in a half-height one only
     buys empty air at each end. #calendar-title is pulled up 20px by a
     transform to sit near the globe's menu button on the layout it was
     written for; a transform does not move the box, so it LOOKS higher while
     still reserving its old height - a gap you can see but cannot close by
     moving anything. The phone has neither.

     10px below the header, not 0 and not the 52 it was: the month has to sit
     ON the grid it labels without the letters touching it. */
  #calendar-header { margin-top: 0; margin-bottom: 10px; }
  #calendar-title { transform: none; }

  /* The pane's own floor, matched to the phone's 8px. The rail is beside
     this column rather than under it, so there is no bar down there for the
     old 2px to be hugging. */
  #calendar { padding-bottom: 8px; }

  /* THE DAY AND DETAIL PAGES FOLLOW THE MONTH GRID, AS THEY ALWAYS HAVE.

     All three calendar pages are held to one width so the back chevron lines
     up with the content edge - see the shared landscape rule that caps them
     to 7 x --sq + 24px. That cap was the month grid's width, so lifting the
     cap up there without lifting it here would break exactly the alignment
     it exists to keep. Same move, same reason: fill the column. */
  #day-header, #day-body,
  #detail-header, #detail-body {
    max-width: none;
    margin-inline: 0;
  }

  /* FOUR EVENTS ACROSS, NOT THREE - the portrait iPhone's own answer to a
     wider pane, reused rather than reinvented. It is the rule at the
     601-768px portrait band verbatim: a day's cards are squares sized from
     their own column width, so on a pane this much wider than a phone's
     three of them grow big enough that two rows no longer clear the floor of
     a half-height pane. A fourth column spends the extra width on MORE of
     the day being visible instead of on bigger cards.

     Nothing in JavaScript needs telling: sizeDayItems reads the column count
     back off the computed grid, so the squares stay square by themselves. */
  .day-flow { grid-template-columns: repeat(4, minmax(0, 1fr)); }

  /* Type matched to the portrait iPhone's calendar, which is the reference
     for this pane: 21px on the month, 12.5px on the day numbers, against
     the 26 and 18 this layout had inherited from the full-height landscape
     rules. The numbers transfer straight across rather than needing scaling
     because the day cells are the same 55px wide on both - it is the same
     grid at the same size, so it should be the same type on it. The number
     inside a cell inherits, so the cell is the only place it is set. */
  /* A RESERVATION'S SOURCE EMAILS AND A TENTATIVE ONE'S PHOTOS TAKE THE
     MIDDLE COLUMN.

     The same two panels desktop uses (openSourceEmailsPanel and
     openPhotoGalleryPanel — there is one email reader and one gallery in the
     app and these are they); only their box is restated for this layout. On
     desktop they slide across the AI + cost columns; here the middle column
     IS the AI panel, so they slide out from under the globe/calendar column
     and cover exactly that column, up to the rail.

     They share one rule because they are one shape: whichever of the chat or
     the budget is showing in that column, the panel opens on top of it and
     leaves it untouched underneath, so dismissing the panel reveals whatever
     the traveler had there rather than a default. openSourceEmailsPanel's own
     comment makes that same point for desktop; nothing in JavaScript is
     layout-aware, so giving #email-panel a box here is all it needed to work
     on this layout at all - the base rule hides it outright, which is why the
     button did nothing before.

     Animated by easing `left` in from the same line `right` holds, not by a
     translateX — the identical reasoning written up at #email-panel above:
     this box's width is an expression, not a constant, so translateX(-100%)
     would move it by however wide it happens to be rather than however far
     it has to travel. Closed it is a zero-width box on the column's leading
     edge (nothing to paint), open it grows rightward to the rail.

     z-index sits above #ai-menu.open so it covers the column it is over, and
     the rail is left uncovered — the navigation stays reachable while the
     photos are up. */
  #email-panel,
  #photo-panel {
    --rail-col: calc((100% - var(--mbar-h) - env(safe-area-inset-right, 0px)) / 2);
    display: flex !important;
    flex-direction: column;
    position: fixed;
    top: 0;
    bottom: 0;
    left: var(--rail-col);
    right: calc(100% - var(--rail-col));
    width: auto;
    z-index: 12;
    background: var(--bg);
    border-left: 1px solid var(--glass-border);
    transition: left 0.4s var(--ease-out), right 0.4s var(--ease-out);
    pointer-events: none;
    overflow: hidden;
  }
  body.email-panel-open #email-panel,
  body.photo-panel-open #photo-panel {
    right: calc(var(--mbar-h) + env(safe-area-inset-right, 0px));
    pointer-events: auto;
  }
  /* The header carries desktop's 48px of top padding to clear a window's own
     chrome; here the panel starts at the top of the screen, so the safe-area
     inset is the only thing it has to clear. */
  #photo-panel-header,
  #email-panel-header,
  #email-reader-header { padding-top: calc(env(safe-area-inset-top, 0px) + 20px); }
  @media (prefers-reduced-motion: reduce) {
    #email-panel, #photo-panel { transition: none; }
  }

  /* THE AI CHAT AND THE COST VIEW COME DOWN A NOTCH - FROM ONE KNOB.

     --menu-fs is the panel's base text size and the thing the whole surface
     was designed to scale from ("ONE knob - --menu-fs - sets the open menu's
     base text size; every child below sizes in em off it", at the top of this
     file). At 1024 x 768 this layout picks up the desktop step, 15px. Here it
     is 13.8 - the same 0.92 the rest of this layout came down by.

     Everything that kept the bargain moves on its own: the header, the title
     block, the budget total (1.65em), every cost row, the history list, the
     composer's chrome. What follows are the places that DIDN'T - they pinned a
     pixel value instead of sizing off the knob, so the knob could not reach
     them. Each is restated as the ratio it already had to --menu-fs (its old
     px over 15), so the change is purely a scale: every relationship between
     these sizes is exactly what it was, the whole set is just smaller.

     calc off the variable rather than plain em on purpose - em would resolve
     against whatever the parent happens to inherit, which is right today and
     one refactor away from being wrong. The ratio belongs to the knob. */
  :root { --menu-fs: 13.8px; }

  /* Header + empty state */
  .ai-menu-title-route  { font-size: calc(var(--menu-fs) * 1.3333); }  /* was 20 */
  .ai-menu-title-meta   { font-size: calc(var(--menu-fs) * 0.8); }     /* was 12 */
  #ai-empty-hero span   { font-size: calc(var(--menu-fs) * 2.9333); }  /* was 44 */

  /* Chat: the message body and everything drawn inside a message */
  .ai-msg,
  .ai-msg-edit-input        { font-size: calc(var(--menu-fs) * 1); }        /* was 15 */
  .trip-summary-route       { font-size: calc(var(--menu-fs) * 1.2667); }   /* was 19 */
  .trip-summary-arrow       { font-size: calc(var(--menu-fs) * 0.9333); }   /* was 14 */
  .trip-summary-meta        { font-size: calc(var(--menu-fs) * 0.8); }      /* was 12 */
  .trip-summary-foot        { font-size: calc(var(--menu-fs) * 0.8333); }   /* was 12.5 */
  .ai-msg-btn               { font-size: calc(var(--menu-fs) * 0.8667); }   /* was 13 */
  .ai-msg-btn-note          { font-size: calc(var(--menu-fs) * 0.8); }      /* was 12 */
  .ai-msg-link-heading      { font-size: calc(var(--menu-fs) * 0.8667); }   /* was 13 */
  .ai-msg-inline-search-btn { font-size: calc(var(--menu-fs) * 0.8); }      /* was 12 */
  .ai-msg-photo figcaption  { font-size: calc(var(--menu-fs) * 0.8); }      /* was 12 */
  .ai-plan-item-why         { font-size: calc(var(--menu-fs) * 0.8667); }   /* was 13 */

  /* Composer + cost list */
  .ai-plus-menu-item     { font-size: calc(var(--menu-fs) * 0.9333); }  /* was 14 */
  .ai-ph-item            { font-size: calc(var(--menu-fs) * 1.0667); }  /* was 16 */
  .cost-item-amount-wrap { font-size: calc(var(--menu-fs) * 1.0667); }  /* was 16 */

  /* THE TWO INPUTS DO NOT COME DOWN, AND MUST NOT.

     #ai-menu-input and .cost-item input sit at 16px because iPhone Safari
     zooms the whole page in when a focused input's text is smaller than
     that - the original rule says so in its own comment. This layout is a
     phone, so the rule applies with full force: scaling these two with
     everything else would trade slightly tidier type for the page lurching
     every time the traveler taps the composer or edits a cost. They are the
     one deliberate exception to the ratio above. */
  #ai-menu-input,
  #ai-menu-input::placeholder,
  .cost-item input { font-size: 16px; }

  /* The day view's title is the same heading as the month's, one level in,
     so it takes the same 21px rather than the 26 it inherited from the
     full-height landscape rules alongside the month title. */
  /* Type held to the portrait iPhone's, which is the reference for this pane -
     the same three numbers it uses, because this is the same grid at the same
     size in a column of the same width. The month title is the heading, the
     day view's title is one level in and a notch down, and the weekday letters
     are the quietest thing on the grid. Set here, in one place, now that the
     old "iPad landscape" step-up that used to fight them is gone. */
  #calendar-title { font-size: 21px; }
  #day-title { font-size: 18px; }
  #days .day { font-size: 12.5px; }
  #weekdays { font-size: 11px; }
  #days { margin-bottom: 0; }
}
