/* Trip Manager — fresh & friendly UI */
:root {
  --bg: #f0f4f2;
  --bg-grad: linear-gradient(160deg, #eef7f3 0%, #f2f0fa 100%);
  --card: #ffffff;
  /* Input/field surface. Reported 2026-08-26: "recognizing the fields is
     a bit harder, the background color is very similar" — inputs reused
     --bg, so on a --card panel (white here) a field was #f0f4f2 on
     #ffffff, a barely-perceptible step. Fields now have their OWN token
     so their contrast is set independently of the page background, per
     the instruction: light theme goes a bit DARKER than the panel.
     Kept in the same hue family so nothing looks bolted on, and paired
     with --field-edge for a definite boundary rather than relying on
     fill alone (which also helps anyone who can't resolve the fill
     difference at all). */
  --field: #e6ece9;
  --field-edge: #d0dbd5;
  --ink: #22312b;
  /* was #7d8f88 (3.41:1 on --card — fails WCAG AA 4.5:1 for normal text);
     darkened to pass on both --card and --bg while keeping the same hue. */
  --muted: #5f6f68;
  --brand: #0e9f78;
  --brand-deep: #0b7a5c;
  --brand-light: #e0f5ee;
  --accent: #ff9f4a;
  --accent-light: #fff1e3;
  /* --accent is a background/highlight color (2.04:1 as text — fails badly);
     this darker variant is for the rare case accent is used as TEXT. */
  --accent-text: #a85815;
  --lilac: #eae6fb;
  --danger: #e05b4c;
  /* --danger (#e05b4c) is 3.64:1 as BOLD TEXT on --card (fails WCAG AA
     4.5:1 — bold text only counts as "large text" at ≥18.66px, and every
     real usage — .warn, the conflict "outside opening hours" label — is
     13-14px). Same technique as --muted/--accent-text above: same hue,
     darkened until it clears 4.5:1 on --card; --danger itself is left
     untouched everywhere else (borders, rgba() tints, accent-color) since
     those aren't text. */
  --danger-text: #d93826;
  /* --brand/--brand-deep as a WHITE-TEXT background (the gradient behind
     #header's title, .btn-primary, .fab, .sched-time, .d-badge,
     .status-badge) measured 3.02–3.36:1 at best in light mode and as low
     as 1.86:1 in dark mode (dark mode's --brand is a much brighter mint,
     see below) — real AA failures for anything not large-text (most of
     these are 13-15px bold, under the 18.66px-bold large-text floor).
     --cta-1/--cta-2 are a same-hue, darkened-just-enough gradient pair
     used ONLY by those specific white-text surfaces (computed against a
     WCAG-formula script, ~4.5:1+ at the lighter stop); every other
     --brand/--brand-deep usage (borders, chips, focus, dots, the trip
     cover, segmented-active) is untouched. */
  --cta-1: #0c8565;
  --cta-2: #096652;
  --radius: 18px;
  --shadow: 0 2px 10px rgba(30, 70, 55, .08);
  --shadow-lift: 0 6px 20px rgba(30, 70, 55, .14);
}
@media (prefers-color-scheme: dark) {
  :root {
    --bg: #101614;
    --bg-grad: linear-gradient(160deg, #101815 0%, #14121d 100%);
    --card: #1b2420;
    /* Dark theme moves the field the OTHER way — lighter/greyer than the
       panel, per "if it's dark make it a little bit more gray". Going
       darker instead would push it toward the near-black page background
       and make the problem worse, not better: on a dark panel a field
       reads as a raised surface, not a recessed one. */
    --field: #2b3733;
    --field-edge: #3c4b46;
    --ink: #e7efeb;
    --muted: #8ba59b;
    --brand: #2fd6a3;
    --brand-deep: #1fa87e;
    --brand-light: #17352b;
    --accent: #ffb066;
    --accent-light: #3a2a18;
    --accent-text: #ffb066; /* already 8.83:1 on --card — no darkening needed in dark mode */
    --lilac: #262138;
    /* dark theme's --danger (#e05b4c, unchanged) is 4.37:1 as bold text on
       dark --card — under the 4.5:1 floor too, just barely; same hue,
       LIGHTENED slightly (dark bg needs a lighter fg, opposite direction
       from the light-theme fix above) until it clears 4.5:1. */
    --danger-text: #e16253;
    /* dark theme's --brand (#2fd6a3) is a much brighter mint than light
       theme's — 1.86:1 as a white-text background, the worst failure
       found in this pass. Same darkening technique, own values (dark
       mode can't reuse light mode's --cta-1/--cta-2 — same math, verified
       ≥4.5:1 against white via the same script). */
    --cta-1: #1a8564;
    --cta-2: #146650;
    --shadow: 0 2px 10px rgba(0,0,0,.35);
    --shadow-lift: 0 6px 20px rgba(0,0,0,.5);
  }
}
* { box-sizing: border-box; margin: 0; -webkit-tap-highlight-color: transparent; }
html { scroll-behavior: smooth; overflow-x: hidden; }
/* accessibility: visible keyboard-focus ring on every interactive element,
   without adding an outline to mouse/touch clicks (:focus-visible). */
a:focus-visible, button:focus-visible, input:focus-visible, select:focus-visible,
textarea:focus-visible, [tabindex]:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 2px;
}
/* was position:absolute; left:-999px — with no overflow-x guard anywhere,
   that expanded the WHOLE document's horizontal scroll area by 999px
   (position:absolute participates in document scroll bounds; position:
   fixed is viewport-relative and never does). Barely noticeable with a
   mouse wheel, very noticeable as extra side-scroll on a touchscreen —
   exactly the real bug report this fixes. Fixed positioning + the
   overflow-x:hidden guard above (defense in depth for anything similar
   in the future, e.g. the ad-blocker bait element below) both fix it;
   kept both rather than relying on just one. */
.skip-link {
  position: fixed; left: -999px; top: 0; z-index: 100;
  background: var(--card); color: var(--ink); padding: 10px 16px;
  border-radius: 0 0 8px 0; box-shadow: var(--shadow-lift);
}
.skip-link:focus { left: 0; }
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; scroll-behavior: auto !important; }
}
body {
  font-family: "Nunito", -apple-system, "Segoe UI", Roboto, sans-serif;
  /* nicer Hebrew/Arabic faces — Nunito has no glyphs for either script and
     was silently falling back to the OS default. Latin text stays Nunito. */
}
html[lang="he"] body { font-family: "Heebo", "Nunito", -apple-system, "Segoe UI", Roboto, sans-serif; }
html[lang="ar"] body { font-family: "Tajawal", "Nunito", -apple-system, "Segoe UI", Roboto, sans-serif; }
body {
  background: var(--bg-grad); background-attachment: fixed; color: var(--ink);
  min-height: 100vh;
  padding-bottom: env(safe-area-inset-bottom);
  /* overflow-x:hidden REMOVED here (2026-08-25) — real live bug found
     chasing a sticky-positioning report: per the CSS Overflow spec, an
     element can't mix overflow-x:hidden with overflow-y:visible — the
     UA silently forces the visible axis to `auto` too (confirmed: even
     writing overflow-y:visible explicitly got coerced back to auto).
     That silently turned body into a SECOND, inert scroll container —
     it never actually scrolls, html/documentElement does (confirmed via
     document.scrollingElement) — and position:sticky descendants
     resolve against the NEAREST scrolling ancestor, which became body
     instead of the real one, so they just sat static. This was always
     "belt and suspenders" alongside html's OWN overflow-x:hidden below
     it in the cascade (html is the real scrolling element and correctly
     stays a scroll container) — the PRIMARY fix for the original
     horizontal-overflow bug this was guarding (the accessibility
     skip-link) is position:fixed on that element, unrelated to this
     property, so removing the redundant safety net here doesn't
     reintroduce it. */
}

/* ── header ── */
#header {
  /* --cta-1/--cta-2, not --brand/--brand-deep directly — see the :root
     comment: this is white text on the gradient, AA-contrast-checked. */
  background: linear-gradient(135deg, var(--cta-1) 0%, var(--cta-2) 100%);
  color: #fff;
  padding: calc(env(safe-area-inset-top) + 16px) 20px 16px;
  display: flex; justify-content: space-between; align-items: center;
  position: sticky; top: 0; z-index: 5;
  border-radius: 0 0 22px 22px;
  box-shadow: var(--shadow-lift);
  /* Live report: "vertical scroll still very heavy, smooth but really
     slow, unlike horizontal" — the board's horizontal drag scrolls a
     small CONTAINED div (#board-scroll), but its vertical drag scrolls
     the whole PAGE (document.scrollingElement), which is a strictly
     bigger-scope repaint every frame regardless — this sticky header is
     the one thing in that repaint whose position genuinely has to keep
     up. Promoting it to its own compositor layer keeps that recalc off
     the main thread. Real fix candidate, not a full diagnosis — the
     inherent contained-div-vs-whole-page cost gap may still remain; see
     WORKPLAN.md's queue entry for the fuller investigation. */
  will-change: transform;
}
#header h1 { font-size: 20px; font-weight: 800; letter-spacing: .2px; }
/* Offline banner — informational only (see index.html/app.js). Same
   accent tone as the lock-icon "orange glow" design elsewhere planned,
   deliberately not --danger: being offline isn't an error, editing
   still works, nothing is lost. */
#offline-banner {
  background: var(--accent-light); color: var(--accent-text);
  font-size: 13px; font-weight: 700; text-align: center;
  padding: 8px 16px;
}
#tour-mode-badge {
  font-size: 12px; font-weight: 700; background: rgba(0,0,0,.20);
  padding: 4px 12px; border-radius: 12px; backdrop-filter: blur(4px);
}

/* ── layout ── */
.view { padding: 16px; max-width: 560px; margin: 0 auto; animation: fadeUp .25s ease; }
@keyframes fadeUp { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; } }
.hidden { display: none !important; }

/* Desktop two-column layout (view-trip, view-day) — left: attribute
   panels (trip/day settings, tour mode, AI ask); right: the main content
   (itinerary / schedule). Same 1280px breakpoint the ad rectangles
   already switch on, so desktop mode is consistent across the app
   rather than introducing a second threshold.
   Deliberately NOT wrapper divs: .col-left/.col-right sit directly on
   the existing cards, in their ORIGINAL source order. Below 1280px
   they're an unstyled no-op class, so mobile's card order is that exact
   source order — guaranteed, not just "happens to match" — regardless
   of which column each card is assigned to at desktop width. At
   ≥1280px, CSS Grid's implicit auto-placement puts every .col-left item
   into column 1 and every .col-right item into column 2, each stacking
   top-to-bottom in DOM order within its own column (a real bug this
   avoided during development: an earlier wrapper-div version silently
   changed view-trip's MOBILE card order, since wrapping reordered the
   DOM itself — this direct-class approach can't do that by construction).
   view-board is NOT part of this: it already shows its own side-by-side
   day columns and stays that way, full-width, un-split. */
@media (min-width: 1280px) {
  .view-2col {
    /* 340px -> 510px (+50%), direct request: "make right floating panel
       wider by 50%" (the panel renders visually on the right in RTL).
       max-width grown by the same +170px so the schedule/itinerary
       column keeps its previous width instead of being squeezed. */
    max-width: 1150px;
    display: grid; grid-template-columns: 510px 1fr; gap: 20px; align-items: start;
  }
  .view-2col > .topbar,
  .view-2col > input#day-title-input { grid-column: 1 / -1; }
  .view-2col > .col-left { grid-column: 1; }
  .view-2col > .col-right { grid-column: 2; }
  /* Desktop: the short attribute-panels column should float — stay
     visible while the much-taller Schedule column scrolls past it —
     per direct request. Scoped to #view-day specifically (NOT a blanket
     .view-2col rule): view-day's col-left is the single wrapper div from
     the gap fix above, so one sticky element behaves correctly; view-
     trip's col-left is still individual un-wrapped cards (deliberately,
     see the comment on .view-2col itself) — making EACH of those
     independently sticky would pile them on top of one another at the
     same offset instead of floating as one coherent panel. 84px ≈ the
     sticky header's own rendered height + a small gap, so this settles
     right below it rather than under/overlapping it. */
  /* Explicit rows, not left to auto-placement: sticky positioning on a
     grid item is contained within the boundaries of its OWN grid area —
     confirmed live, computed position was correctly sticky/top:84px,
     but scrolling still carried it away with the page, meaning its
     containing row had no extra room to hold it in place while
     scrolling past. Forcing col-left and col-right into the SAME
     explicit row (3, right after the topbar/day-title-input rows above)
     guarantees that row's height matches col-right's own (taller)
     content, so col-left's sticky positioning has real room to float
     within as the page scrolls through row 3's extent. */
  #view-day .col-left, #view-day .col-right { grid-row: 3; }
  #view-day .col-left { position: sticky; top: 84px; align-self: start; }
  /* Explicit rows for the full-span header elements too — without this,
     once view-trip's content rows below are explicitly numbered (see
     next rule), the topbar has no row of its own left to auto-place
     into (both columns already spoken for by rows 1-2) and gets pushed
     to row 3, i.e. the BOTTOM of the page. input#day-title-input simply
     doesn't exist on view-trip, so this rule is a harmless no-op there. */
  .view-2col > .topbar { grid-row: 1; }
  .view-2col > input#day-title-input { grid-row: 2; }
  /* view-trip's DOM order interleaves columns (Itinerary=right, Trip-
     properties=left, Tour Mode=left, Timetable-board=right) — sparse
     auto-placement leaves a gap in that case (confirmed by rendering:
     the browser reserves row 2 col 1 as empty once Itinerary claims row
     2 col 2, and doesn't backfill it), so these four need explicit rows.
     view-day's order is already grouped per column (all col-left items
     before all col-right items), so auto-placement alone works fine for
     ITS content rows — only the header needed the explicit rows above. */
  #itin-card, #tourmode-card { grid-row: 2; }
  #btn-props, #btn-board { grid-row: 3; }
  /* Desktop: float view-trip's col-left items too, per direct request —
     same idea as view-day's sticky panel, but view-trip genuinely can't
     use a single wrapper div for it (its col-left/col-right items
     interleave in DOM order — Itinerary=right, Trip-properties=left,
     Tour-Mode=left, Board=right — deliberately, see the comment above;
     wrapping would force the two col-left items adjacent, moving
     Trip-properties in the DOM and changing MOBILE's card order, the
     exact thing this file's whole col-left/col-right approach exists to
     avoid). Both need to share ONE tall grid row to have real room to
     float in (confirmed live: giving btn-props its own row 3, paired
     with the short btn-board, left it with nowhere to stick — same
     "no room in its own row" failure as the original col-left bug).
     So btn-props moves into row 2 alongside tourmode-card and itin-card
     (whose height is what actually gives this row room), offset below
     tourmode-card's own box via margin-top so they don't overlap before
     either has scrolled into its stuck position. Both measurements
     (row-2's join, 171px, 20px gap) are hardcoded from tourmode-card's
     current real content — same tradeoff as the header-height offset
     elsewhere on this page. */
  #tourmode-card { position: sticky; top: 84px; }
  #btn-props { grid-row: 2; margin-top: 191px; position: sticky; top: 275px; }
  /* view-day: a live report with a screenshot found a real gap — Schedule
     is much taller than col-left's short stacked cards, and per-card
     row-by-row placement (tried first, see git history) always coupled
     SOME row's height to Schedule's full height regardless of exactly
     how the rows were sliced, leaving dead space under whichever short
     card shared that row. Fixed structurally instead (see index.html):
     each column's cards are grouped under ONE wrapper div, so each
     column is a single grid item with its own independent height — no
     row-sharing between columns at all, the actual root cause. */
}

.card {
  background: var(--card); border-radius: var(--radius);
  box-shadow: var(--shadow); padding: 18px; margin-bottom: 14px;
}
.card-title {
  font-weight: 800; font-size: 17px; margin-bottom: 12px;
  color: var(--ink);
}
.card-title.collapsible {
  display: flex; align-items: center; gap: 6px;
  cursor: pointer; margin-bottom: 0;
}
/* pushes the arrow to the far end on its own — was justify-content:
   space-between on just 2 children (title, arrow); switched to this so
   a 3rd child (.count-badge, added right after the title) sits directly
   next to the title instead of drifting to the middle of the row. */
.card-title.collapsible .collapse-btn { margin-bottom: 0; margin-inline-start: auto; }
/* Collapsed-state item count, e.g. "Excluded sites (3)" -- direct
   request: "show counter when collapsed". Shown regardless of
   collapsed/expanded state (simpler, always accurate); empty/hidden via
   JS (textContent='') when the list is empty. */
.count-badge { color: var(--muted); font-weight: 700; font-size: 14px; }
#daysettings-body, #bench-list { margin-top: 12px; }
.topbar {
  display: flex; align-items: center; gap: 10px;
  margin-bottom: 14px; font-weight: 800; font-size: 17px;
  /* Direct request: "current screen title and back-navigation button
     should be merged into one fixed/sticky title bar" — the per-screen
     topbar (back + current screen name) previously scrolled away
     entirely as soon as you scrolled past it, unlike the app's own
     #header above it. 68px = #header's real rendered height (measured
     live), so this settles right below it, never overlapping. Solid
     background + a soft shadow since sticky content now overlays
     whatever scrolls beneath it — without one, page content would
     visibly slide up underneath the bar's own text.
     Verified live on BOTH mobile and desktop (≥1280px 2-column grid) —
     unlike the earlier col-left panel fix elsewhere in this file,
     .topbar spanning grid-column:1/-1 across the full grid (not sharing
     a row with a taller sibling column) doesn't hit that same "sticky
     needs room in its own row" limitation; a direct scroll measurement
     on the real desktop layout confirmed it holds in place, no extra
     row-merging trick needed here. */
  position: sticky; top: 68px; z-index: 4;
  background: var(--bg); padding: 8px 0; box-shadow: 0 4px 10px -6px rgba(0,0,0,.15);
}
.back {
  background: var(--card); border: none; color: var(--brand);
  font-size: 15px; font-weight: 800; padding: 8px 14px; cursor: pointer;
  border-radius: 12px; box-shadow: var(--shadow);
}
.muted { color: var(--muted); font-size: 12px; font-weight: 600; }

/* ── trips list ── */
.trip-row {
  background: var(--card); border-radius: var(--radius); box-shadow: var(--shadow);
  margin-bottom: 14px; cursor: pointer; overflow: hidden;
  transition: transform .15s ease, box-shadow .15s ease;
}
.trip-row:active { transform: scale(.98); }
.trip-cover {
  height: 84px;
  background: linear-gradient(120deg, #0e9f78 0%, #3db8de 55%, #8b7ae8 100%);
  display: flex; align-items: flex-end; padding: 12px 16px;
}
.trip-cover .t-emoji { font-size: 34px; filter: drop-shadow(0 2px 4px rgba(0,0,0,.25)); }
.trip-body { padding: 12px 16px 14px; display: flex; justify-content: space-between; align-items: center; }
.trip-row .t-name { font-weight: 800; font-size: 17px; }
.trip-row .t-sub { color: var(--muted); font-size: 13px; font-weight: 600; margin-top: 2px; }
.trip-row .t-arrow { color: var(--brand); font-size: 22px; font-weight: 800; }

/* ── anchors + days ── */
.anchor-row, .day-row {
  display: flex; align-items: center; gap: 12px;
  padding: 11px 0; border-bottom: 1px solid rgba(128,128,128,.12);
}
.anchor-row:last-of-type, .day-row:last-of-type { border-bottom: none; }
.anchor-icon {
  font-size: 20px; width: 42px; height: 42px; border-radius: 14px;
  background: var(--brand-light); display: flex; align-items: center; justify-content: center;
  filter: grayscale(100%);
}
.anchor-main { flex: 1; }
.anchor-main .a-title { font-weight: 700; font-size: 16px; }
.anchor-main .a-sub { color: var(--muted); font-size: 13px; font-weight: 600; }
.day-row { cursor: pointer; border-radius: 12px; padding-left: 4px; transition: background .15s; }
.day-row:active { background: var(--brand-light); }
.day-row .d-badge {
  background: linear-gradient(135deg, var(--cta-1), var(--cta-2)); color: #fff;
  border-radius: 12px; padding: 7px 13px; font-weight: 800; font-size: 13px;
  box-shadow: 0 2px 6px rgba(14,159,120,.3);
}
.day-row .d-info { flex: 1; font-size: 14px; font-weight: 600; }
.day-row .d-mob { font-size: 20px; filter: grayscale(100%); }

/* ── schedule ── */
/* border-bottom moved here (was on .sched-item) — the expanded tip/
   actions block is now a sibling AFTER .sched-item, not nested inside
   it (see .sched-expanded below), so the row separator needs to sit
   after BOTH, not between them. */
.sched-wrap { transition: opacity .15s; border-bottom: 1px solid rgba(128,128,128,.12); }
.sched-wrap.dragging { opacity: .55; }
.sched-wrap.dragging .sched-item { background: var(--brand-light); border-radius: 12px; }
.sched-item {
  display: flex; gap: 10px; padding: 13px 0; align-items: flex-start;
}
/* Live report with a screenshot: the tip/actions block used to live
   INSIDE .sched-body, so it was capped at the flex row's remaining
   width (squeezed to the left of .sched-time-col) even once the row's
   header content had ended — leaving a big empty column below the time
   pill/star for the whole height of the tip box. Pulled out to a plain
   full-width sibling block instead, so it naturally uses the entire
   row width once expanded ("enlarged to use that slot") rather than
   being permanently narrowed by a column it doesn't actually share
   space with anymore at that point. */
.sched-expanded { padding: 0 0 13px; }
.sched-item.conflict { background: rgba(224,91,76,.07); border-radius: 12px; }
.drag-handle {
  font-size: 20px; color: var(--muted); opacity: .55; cursor: grab;
  padding: 4px 6px; margin: -4px 0; user-select: none; -webkit-user-select: none;
}
/* Live report: the time pill (~62px wide) only ever used the TOP of its
   own column, leaving a tall empty gap below it whenever the row's
   other lines (name/chip/rating) ran longer. An earlier fix moved the
   must-see star into this same column, below the time — since
   superseded (2026-08-25, direct request): the star now sits INLINE
   next to the time instead, and the rating/duration/hours line moved
   into .sched-expanded entirely (see below) so the collapsed row is
   just time + star + name, two fewer rows than before. .sched-time-col
   is kept as a wrapper (now holding only the time pill) rather than
   removed, so the time pill's own sizing/margin rules don't need to
   move. */
.sched-time-col { display: flex; flex-direction: column; align-items: center; gap: 6px; flex-shrink: 0; margin-top: 2px; }
/* Direct report ("why don't I see start-end?"): this used to show only
   the visit's START time — the end time was one tap away in the
   expanded row, not visible on the collapsed row itself. Now shows
   both ("08:50–09:50"), so the pill is a fixed-width badge no longer —
   font-size trimmed and nowrap/padding added so the longer string still
   reads as one tidy pill instead of wrapping or overflowing the old
   62×32 box. */
.sched-time {
  font-weight: 800; font-size: 12px; color: #fff; padding: 0 10px; height: 32px;
  background: linear-gradient(135deg, var(--cta-1), var(--cta-2));
  border-radius: 10px; display: flex; align-items: center; justify-content: center;
  white-space: nowrap; font-variant-numeric: tabular-nums;
  cursor: pointer;
  /* Real bug, found live on a Hebrew (RTL) trip: "16:20-08:50" shown for
     an 08:50-16:20 visit — the DOM string was always correct start-first,
     but two LTR number runs joined by a bare dash inside an RTL-flowing
     page get their VISUAL order flipped by the Unicode bidi algorithm.
     A single time never showed this (nothing to reorder); a start-end
     RANGE does. direction:ltr forces true left-to-right rendering
     regardless of the surrounding page direction; unicode-bidi:isolate
     stops that reordering from leaking into text around it either. */
  direction: ltr; unicode-bidi: isolate;
}
/* Per-site lock mechanism (2026-08-25): a locked start time or duration
   becomes a fixed anchor nothing automatic may move again — the orange
   glow/lock-icon treatment matches the app's existing accent tone (same
   family as the offline banner) so "locked" reads consistently across
   the UI as "this is deliberately fixed," not an error state. */
.sched-time.locked { box-shadow: 0 0 0 2px var(--accent), 0 0 10px 1px var(--accent); }
.locked-dur { cursor: pointer; color: var(--accent-text); font-weight: 800; }
/* Now sits inline next to the time pill, not stacked in .sched-time-col
   below it (see that rule's comment) — small top offset so the 28px
   circle visually centers against the 32px-tall time pill beside it. */
.mustsee-icon {
  width: 28px; height: 28px; border-radius: 50%; padding: 0;
  background: none; border: 1.5px solid var(--muted); color: var(--muted);
  font-size: 14px; opacity: .55; cursor: pointer; font-family: inherit;
  display: flex; align-items: center; justify-content: center; transition: all .15s;
  flex-shrink: 0; margin-top: 4px;
}
.mustsee-icon.on { background: var(--accent); border-color: var(--accent); color: #3a2205; opacity: 1; }
.sched-body { flex: 1; }
.sched-body .s-name { font-weight: 800; font-size: 17px; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
/* Live request: long site names should cut off in the main list, with
   the full name only a tap away (reuses the row's existing expand —
   the same tap that reveals the tip/actions also un-clamps the name).
   Collapsed = 2-line floor with an automatic ellipsis; expanded = no
   clamp at all, full name however long it is. */
.s-name-text.clamped {
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden;
}
/* .sched-expanded's own .s-sub (the date/phone line) is a real live
   report with a screenshot: it's a sibling of .sched-body now (see the
   .sched-expanded comment above), not a descendant, so this rule never
   matched it — it rendered in the page's default text color/size
   instead of the intended small muted line, standing out oddly on its
   own where the tip text below it. Same rule, just also matching the
   sibling location. */
.sched-body .s-sub, .sched-expanded .s-sub { color: var(--muted); font-size: 14px; font-weight: 600; margin-top: 3px; }
.warn { color: var(--danger-text); font-weight: 800; }
.sched-travel {
  font-size: 13px; font-weight: 700; color: var(--muted); padding: 5px 14px 5px 78px;
  display: flex; align-items: center; gap: 6px;
}
.sched-travel::before { content: ''; width: 2px; height: 18px; background: repeating-linear-gradient(var(--brand) 0 3px, transparent 3px 6px); margin-right: 4px; }
/* Real bug fixed, then a real correction on top of that fix (2026-08-30):
   this leg's navigate button was originally styled identically to a
   plain decorative arrow icon next to it, so the tappable "open
   directions" button read as inert decoration. First fix: a small solid
   pill (.leg-nav-pin, now removed). Then a direct correction: two
   separate icon-only pins (Google Maps + a Waze one using a confusing
   compass emoji) collapsed into the ONE button day-view.js's
   legNavHtml() now renders — the app's own existing .chip style (icon +
   "Navigate" text), same as the site's own navigate button. This rule
   just right-aligns that chip within the flex row, the one job
   .leg-nav-group used to do. */
.sched-travel .chip { margin-inline-start: auto; }
.sched-ghost { border-radius: 12px; }
.leg-icons {
  filter: grayscale(100%); font-size: 14px; letter-spacing: 1px;
  display: inline-flex; align-items: center;
}
/* Direct report ("icon is tiny not clear in b[o]th screenshots"), both
   showing the (משוער)/estimated state: .4 opacity was tuned for bold,
   naturally high-contrast colorful emoji — the app's own thin-stroke
   monochrome SVGs (fmt.js's ICON_WALK/CAR/BUS/etc., which replaced them)
   read as a faint, illegible blur at that same .4. Raised so the
   estimated/verified distinction still exists but neither state is hard
   to actually see. */
.leg-icons.estimated { opacity: .7; }
.leg-icons.accurate { opacity: 1; }
#btn-finalize:disabled { opacity: .8; cursor: wait; }
#btn-finalize.all-verified { opacity: .55; cursor: default; background: var(--bg); color: var(--muted); }
#btn-finalize.coming-soon { opacity: .5; cursor: not-allowed; background: var(--bg); color: var(--muted); }
.verify-tag { font-size: 11px; opacity: .65; }
.leg-arrow { opacity: .5; margin: 0 2px; }
[dir="rtl"] .leg-arrow { transform: scaleX(-1); }
.site-tip {
  font-size: 13px; font-style: italic; color: var(--ink); line-height: 1.5;
  background: var(--accent-light); border-radius: 10px; padding: 8px 12px; margin-top: 6px;
}
#hascar-seg { margin-bottom: 6px; }
#hascar-hint { margin-top: 4px; }
/* Real bug fixed: this used to be a thin (14px) full-width, low-opacity
   bar with a centered "+" — genuinely hard to see or hit precisely.
   Now a proper round button, sized like the app's other icon buttons
   (drag-handle is 20px+padding), left-aligned to roughly the same
   column as the drag-handle/timeline above and below it instead of
   floating in the middle of a wide row disconnected from that column. */
.sched-insert {
  display: flex; align-items: center; justify-content: center;
  width: 28px; height: 28px; margin: 3px 0 3px 17px;
  border: none; background: var(--brand-light); color: var(--brand-deep); opacity: .6;
  font-size: 18px; font-weight: 800; line-height: 1; cursor: pointer;
  border-radius: 50%; font-family: inherit; transition: opacity .15s, transform .1s;
}
.sched-insert:hover, .sched-insert:focus-visible, .sched-insert:active { opacity: 1; transform: scale(1.1); }
.sched-leave {
  display: flex; align-items: center; gap: 10px;
  font-size: 14px; font-weight: 700; color: var(--muted);
  padding: 10px 0; border-bottom: 1px solid rgba(128,128,128,.12);
}
.leave-time {
  font-weight: 800; font-size: 15px; min-width: 62px; height: 32px;
  background: var(--bg); color: var(--muted); border: 1.5px dashed var(--muted);
  border-radius: 10px; display: inline-flex; align-items: center; justify-content: center;
  margin-left: 36px;
}
.hint { font-size: 13px; font-weight: 600; color: var(--muted); margin-bottom: 8px; }
.s-actions { display: flex; gap: 6px; margin-top: 9px; flex-wrap: wrap; }
/* This row can hold up to 6 chips (navigate/reviews/website/call/remove/
   exclude) — shrunk specifically here (not a global .chip change, which
   is reused all over the app) so the full set fits with less wrapping,
   per direction: an overflow menu was tried and reverted, "shrinking a
   bit was enough". */
.s-actions .chip { font-size: 12px; padding: 6px 10px; border-radius: 13px; }
.chip {
  font-size: 14px; font-weight: 700; padding: 8px 16px; border-radius: 16px;
  background: var(--brand-light); color: var(--brand-deep);
  border: none; cursor: pointer; text-decoration: none; display: inline-block;
  font-family: inherit;
  transition: transform .1s ease;
}
.chip:active { transform: scale(.94); }
.chip-ghost { background: none; border: 1.5px solid rgba(128,128,128,.3); color: var(--muted); }
.chip-rating { background: var(--accent-light); color: #9a6015; }
/* Calendar+dial date/time entry, approved via a live prototype artifact
   (2026-08-30 direct request thread — see trip-view.js's whenFieldHtml
   for the full history/why, and day-view.js's promptEditStart/
   editSiteHours for the time-only reuse). Direct correction the SAME
   day, after a viewport-gated version shipped ("use it in all... no
   other [picker] should be used on web"): this is NOT a mobile-vs-
   desktop split any more — .when-mobile is now just a hidden holder for
   the real native <input>, on every viewport, and .when-desktop (name
   kept to avoid re-touching every call site) is the one visible widget
   everywhere on web — a real calendar popup for a date field, an
   Android-style 24h dual-ring dial for a time field, both popping open
   the instant their field is clicked, both applying and closing on the
   one pick (no OK/Cancel). */
.when-mobile { display: none; }
.when-desktop { display: block; }
.when-desktop-row { display: flex; gap: 10px; }
.when-date-wrap, .when-time-wrap { position: relative; flex: 1; }
/* Real bug ("overflow, why is clock trimmed?" / "same for date"): #modal
   has its own `overflow-y: auto; max-height: 85vh` (it's a scrollable
   sheet, not the whole page) — these popups used to be position:absolute,
   anchored via .when-date-wrap/.when-time-wrap's position:relative, which
   put them INSIDE that scroll box, so anything past the modal's own
   visible edge got clipped by ITS overflow, not just the viewport's (the
   flip-up logic below only ever checked window.innerHeight, never the
   modal's own tighter boundary). Fixed to position:fixed — a true
   viewport-relative overlay that escapes the modal's clipping entirely —
   with trip-view.js's positionPopup() now computing exact left/top from
   the trigger field's real getBoundingClientRect() instead of relying on
   top:100%/left:0, which only work for position:absolute. Same rounded-
   card/shadow-lift language as .city-suggest either way. */
/* Direct request (2026-08-30): "make both pickers same hight" / "reduce
   the time a bit, enlarge the date a bit" — the calendar and dial popups
   share a min-height so a short (4-week) month or a tall (6-week) one
   both land the same visual size as the dial, and their widths are now
   deliberately unequal (dial narrower, calendar wider) rather than the
   one-size-fits-both 260px they started at. */
.cal-pop, .dial-pop {
  position: fixed; z-index: 25; min-height: 300px;
  background: var(--card); border-radius: 16px; box-shadow: var(--shadow-lift); padding: 14px;
}
.cal-pop { width: 280px; }
.dial-pop { width: 240px; }
.cal-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; font-weight: 800; font-size: 14px; }
.cal-head button { background: none; border: none; font-size: 16px; color: var(--muted); cursor: pointer; padding: 2px 8px; border-radius: 6px; }
.cal-head button:hover { background: var(--brand-light); color: var(--brand-deep); }
.cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 3px; text-align: center; }
.cal-grid .dow { font-size: 11px; font-weight: 800; color: var(--muted); padding: 4px 0; }
.cal-grid .day { font-size: 14px; font-weight: 700; padding: 9px 0; border-radius: 8px; cursor: pointer; font-variant-numeric: tabular-nums; }
.cal-grid .day:hover { background: var(--brand-light); }
.cal-grid .day.selected { background: var(--brand); color: #fff; }
.cal-grid .day.muted { color: var(--muted); opacity: .4; cursor: default; }
/* 24h dual-ring dial: readout (tap either segment to jump straight to
   that ring) + circular face. Outer ring = 1-12, inner ring = 13-23/00,
   see trip-view.js's renderWhenDial for the full layout rationale. Face
   is 200px (was 220px) to match the narrower .dial-pop — trip-view.js's
   dialPolar()/renderWhenDial() center and tick radii were scaled down
   with it (110->100 center, 82/50->75/46 radii) so the ticks still land
   exactly on the smaller circle. */
.dial-pop .readout { display: flex; align-items: baseline; justify-content: center; gap: 6px; margin-bottom: 10px; }
.dial-pop .seg { font-size: 30px; font-weight: 800; font-variant-numeric: tabular-nums; color: var(--muted); cursor: pointer; padding: 2px 6px; border-radius: 8px; }
.dial-pop .seg.active { color: var(--brand-deep); background: var(--brand-light); }
.dial-pop .colon { font-size: 30px; font-weight: 800; color: var(--muted); }
.dial-pop .face { position: relative; width: 200px; height: 200px; border-radius: 50%; background: var(--bg); margin: 0 auto; }
.dial-pop .hand { position: absolute; left: 50%; top: 50%; width: 2px; background: var(--brand); transform-origin: top center; z-index: 1; }
.dial-pop .hub { position: absolute; left: 50%; top: 50%; width: 8px; height: 8px; margin: -4px; border-radius: 50%; background: var(--brand); z-index: 3; }
.dial-pop .tick {
  position: absolute; width: 34px; height: 34px; margin: -17px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font-size: 13px; font-weight: 700; color: var(--ink); cursor: pointer; z-index: 2; font-variant-numeric: tabular-nums;
}
.dial-pop .tick:hover { background: var(--brand-light); }
.dial-pop .tick.selected { background: var(--brand); color: #fff; }
.dial-pop .tick.inner { width: 26px; height: 26px; margin: -13px; font-size: 11px; font-weight: 700; color: var(--muted); }
.dial-pop .tick.inner.selected { color: #fff; }
.bench-row {
  display: flex; align-items: center; gap: 12px;
  padding: 11px 0; border-bottom: 1px solid rgba(128,128,128,.12);
}
.bench-row:last-child { border-bottom: none; }
.bench-main { flex: 1; }
.bench-main .b-name { font-weight: 700; font-size: 16px; }
.bench-row .btn-small { margin-top: 0; flex-shrink: 0; }

/* ── buttons ── */
.btn-primary {
  width: 100%; background: linear-gradient(135deg, var(--cta-1), var(--cta-2));
  color: #fff; border: none;
  border-radius: 14px; padding: 14px; font-size: 15px; font-weight: 800;
  margin-top: 10px; cursor: pointer; font-family: inherit;
  box-shadow: 0 4px 14px rgba(14,159,120,.35);
  transition: transform .1s ease;
}
.btn-primary:active { transform: scale(.97); }
.btn-small {
  background: var(--brand-light); color: var(--brand-deep); border: none;
  border-radius: 11px; padding: 9px 15px; font-size: 13px; font-weight: 700;
  margin-top: 8px; cursor: pointer; font-family: inherit;
}
.row-btns { display: flex; gap: 8px; }
.fab {
  position: fixed; bottom: calc(env(safe-area-inset-bottom) + 22px); right: 22px;
  background: linear-gradient(135deg, var(--cta-1), var(--cta-2));
  color: #fff; border: none; border-radius: 30px;
  padding: 16px 24px; font-size: 15px; font-weight: 800; font-family: inherit;
  box-shadow: 0 6px 18px rgba(14,159,120,.45); cursor: pointer;
  transition: transform .12s ease;
}
.fab:active { transform: scale(.95); }
.segmented { display: flex; background: var(--field); border-radius: 13px; padding: 4px; gap: 3px; border: 2px solid var(--field-edge); }
.segmented button {
  flex: 1; border: none; background: none; padding: 9px 4px;
  font-size: 12px; font-weight: 700; border-radius: 10px; color: var(--muted);
  cursor: pointer; font-family: inherit; transition: all .15s ease;
}
.segmented button.active {
  background: var(--card); color: var(--brand-deep); font-weight: 800;
  box-shadow: var(--shadow);
}
/* 4-way pin-mode toggle (2026-08-30: flight/train/port/address, up from
   2) — same control, just tighter so 4 labels fit without truncating;
   wraps to 2x2 on a narrow phone rather than squeezing each button
   illegibly thin. */
.segmented-4 button { font-size: 11px; padding: 8px 2px; }
@media (max-width: 420px) {
  .segmented-4 { flex-wrap: wrap; }
  .segmented-4 button { flex: 1 1 45%; }
}

textarea, input, select {
  width: 100%; border: 2px solid var(--field-edge); border-radius: 12px;
  padding: 12px; font-size: 14px; font-weight: 600;
  background: var(--field); color: var(--ink);
  font-family: inherit; margin-bottom: 8px; outline: none;
  transition: border-color .15s;
}
textarea:focus, input:focus, select:focus { border-color: var(--brand); }
/* The generic rule above (padding/background box) looks right for text
   inputs but wrong for a range slider — the padded background box reads
   as a stray rounded rectangle around a thin track. Used by the
   duration-lock slider (explicit spec: "duration slider"). */
input[type="range"] {
  padding: 0; background: none; border: none; accent-color: var(--brand);
  height: 32px; margin-bottom: 8px;
}

/* ── tour view ── */
.tip {
  background: linear-gradient(135deg, var(--accent), #ff8a3c); color: #3a2205;
  border-radius: var(--radius);
  padding: 16px; margin-bottom: 14px; font-size: 14px; font-weight: 700;
  box-shadow: 0 4px 14px rgba(255,159,74,.4);
  animation: pop .25s ease;
}
@keyframes pop { from { transform: scale(.92); opacity: 0; } to { transform: scale(1); opacity: 1; } }
.tip .row-btns { margin-top: 12px; }
.tip .btn-small { background: rgba(255,255,255,.85); color: #3a2205; }
#tour-now-card { border: 2px solid var(--brand); }
#tour-current .s-name, #tour-next .s-name { font-size: 17px; font-weight: 800; }
.progress-dot { display: flex; align-items: center; gap: 10px; padding: 6px 0; font-size: 13px; font-weight: 600; }
.progress-dot .dot { width: 12px; height: 12px; border-radius: 6px; background: rgba(128,128,128,.3); flex-shrink: 0; }
.progress-dot.done .dot { background: var(--brand); box-shadow: 0 0 0 3px var(--brand-light); }
.progress-dot.current .dot { background: var(--accent); box-shadow: 0 0 0 3px var(--accent-light); animation: pulse 1.5s infinite; }
.progress-dot.skipped .dot { background: var(--danger); }
@keyframes pulse { 0%,100% { transform: scale(1); } 50% { transform: scale(1.25); } }

/* ── modal ── */
#modal-backdrop {
  position: fixed; inset: 0; background: rgba(10,25,20,.45); z-index: 20;
  display: flex; align-items: flex-end; justify-content: center;
  backdrop-filter: blur(2px);
}
#modal {
  background: var(--card); border-radius: 22px 22px 0 0; padding: 22px;
  width: 100%; max-width: 560px; max-height: 85vh; overflow-y: auto;
  animation: slideUp .22s ease;
}
/* Real bug, found chasing "why have you replaced the date, or only broke
   the proportions?": ANY transform value other than none (this included
   translateY(0), the animation's own resting frame) makes its element
   the CONTAINING BLOCK for every position:fixed descendant, per spec —
   so trip-view.js's cal-pop/dial-pop, "fixed" to escape #modal's own
   overflow clipping, landed relative to #modal instead of the viewport
   for the animation's whole 220ms (a real click can easily land inside
   that window). margin-bottom on a flex-end-aligned backdrop item gives
   the identical slide-up-into-place look with no transform at all, so
   #modal is never a containing block for anything, at any point in the
   animation. */
@keyframes slideUp { from { margin-bottom: -40px; opacity: .5; } to { margin-bottom: 0; opacity: 1; } }
#modal h3 { margin-bottom: 14px; font-size: 17px; font-weight: 800; }

/* Direct report: "on desktop menu screen should be below the menu, not
   centered", then a direct follow-up: "even on mobile set menu to anchor
   on the same corner of the menu button". Originally scoped to a
   @media(min-width:1280px) block (mobile deliberately left as the
   original centered bottom-sheet, the safer half); that breakpoint gate
   is gone now — BOTH viewports anchor the menu under the ☰ button that
   opened it.

   The 'menu-dropdown' class (app.js's anchorMenuDropdown(), called only
   from openMenu()) is the ONLY thing that ever adds this — every other
   modal() caller stays the plain centered overlay at every width, and
   modal() itself unconditionally clears the class + inline styles on
   each call so none of them can inherit a stale menu position.

   #modal's actual top/right offsets are set inline by that same JS from
   #menu-btn's live getBoundingClientRect(), computed fresh on every open
   (never cached), so a rotate/resize self-corrects on the next open.
   position:fixed takes #modal out of the backdrop's flex layout
   entirely, so the backdrop's own centering rules never fight it.

   width is the one thing that still differs by viewport: a fixed 320px
   dropdown suits a desktop but can crowd a narrow phone, so the
   max-width clamp (viewport minus a small margin) does the work on
   small screens. */
#modal-backdrop.menu-dropdown { background: transparent; backdrop-filter: none; }
#modal-backdrop.menu-dropdown #modal {
  position: fixed;
  width: 320px; max-width: min(320px, calc(100vw - 24px));
  max-height: 70vh;
  border-radius: 14px;
  box-shadow: var(--shadow-lift);
  animation: none;
}

/* ── toast ── */
#toast {
  position: fixed; bottom: calc(env(safe-area-inset-bottom) + 88px);
  left: 50%; transform: translateX(-50%);
  background: var(--ink); color: var(--card); padding: 11px 20px;
  border-radius: 22px; font-size: 13px; font-weight: 700; z-index: 30; white-space: nowrap;
  box-shadow: var(--shadow-lift);
  animation: pop .2s ease;
}

/* ── board: days side-by-side timetable ── */
.view-wide { max-width: none; }
.btn-board { margin-bottom: 8px; }
/* Draft-changes bar — hidden until a board edit makes boardDirty true
   (see app.js). Sticky so Save/Discard stay reachable while scrolling a
   tall board, same reasoning as a form's fixed action bar. */
#board-draft-bar {
  position: sticky; top: 0; z-index: 20; display: flex; align-items: center; gap: 10px;
  background: var(--accent); color: #2b1d05; border-radius: var(--radius);
  padding: 10px 14px; margin-bottom: 10px; box-shadow: var(--shadow);
  animation: pop .2s ease;
}
#board-draft-label { font-weight: 800; font-size: 14px; flex: 1; }
#board-draft-bar .btn-small { background: rgba(255,255,255,.55); }
/* .btn-primary defaults to a full-width standalone CTA (width:100%,
   margin-top) — reset both for this inline row usage. */
#board-draft-bar .btn-primary { width: auto; margin-top: 0; padding: 8px 16px; }
/* touch-action:none here too (not just on blocks, see enableBoardDrag's
   comment in app.js) -- live report: native scroll across two nested
   containers with different overflow axes (this element's horizontal
   vs. the page's vertical) locks to a single dominant axis per gesture
   and can't blend into a real diagonal drag. enableBoardScroll (app.js)
   drives ALL scrolling in this element manually instead, exactly the
   same free-diagonal approach blocks already use, so a touch anywhere
   in the board — gap, day-head, empty column space — behaves the same. */
#board-scroll {
  overflow-x: auto; -webkit-overflow-scrolling: touch; touch-action: none;
  scroll-snap-type: x proximity;
  margin: 0 -16px; padding: 0 16px 16px;
  /* Real report, verified via headless-Chrome mobile emulation (row 16):
     "vertical board scroll still heavy/slow unlike horizontal." Root
     cause: horizontal drag already scrolled THIS small contained div
     (cheap — only its own content repaints), but vertical drag scrolled
     document.scrollingElement — the WHOLE PAGE, which means recomputing
     every position:sticky/fixed element (the header) and compositing
     the entire viewport on every single frame of the drag, a strictly
     bigger-scope repaint regardless of how the scroll itself is driven.
     Giving this element its own bounded height + overflow-y makes
     vertical scroll ALSO a small-contained-div scroll — same cheap cost
     class as horizontal, not just a compositor hint on top of the
     expensive path (the earlier will-change:transform on #header was
     exactly that: a mitigation, not a fix for this specific gap — see
     that rule's own comment). 100dvh (not 100vh) accounts for mobile
     browser chrome showing/hiding; 140px approximates the header +
     board-hint + draft-bar chrome above this element — imperfect but
     harmless if off, since the div is still fully scrollable either way,
     never an access problem, only a visual balance one. */
  overflow-y: auto; max-height: calc(100dvh - 140px);
}
#board-cols { display: flex; gap: 10px; min-width: min-content; }
.board-col {
  flex: 0 0 30vw; min-width: 128px; max-width: 220px;
  background: var(--card); border-radius: var(--radius);
  box-shadow: var(--shadow); padding: 10px;
  scroll-snap-align: start;
  transition: box-shadow .15s, outline .15s;
}
/* Mobile: size columns so exactly N sit edge-to-edge for the current
   orientation (2 in portrait, 4 in landscape -- was a flat 30vw with a
   128px floor, which left a sliver of a 3rd column showing regardless
   of rotation and didn't respond to it at all). #board-scroll has 16px
   padding each side (32px) and #board-cols has a 10px gap between
   columns -- the calc()s below subtract those so N columns fill the
   width exactly, with any remaining days reachable by horizontal
   scroll/snap same as before.
   Landscape is gated on max-height (the phone's short edge), NOT
   max-width -- a phone rotated to landscape is often >800px WIDE
   (e.g. 844px), which would otherwise fall through to the desktop
   18vw rule below and never actually respond to the rotation. A
   landscape phone's HEIGHT stays small regardless of width, so that's
   the reliable signal. Desktop's min-height floor keeps a short-but-
   wide phone from also matching the desktop rule. */
@media (max-width: 799px) and (orientation: portrait) {
  .board-col { flex-basis: calc((100vw - 42px) / 2); max-width: none; }
}
@media (max-height: 500px) and (orientation: landscape) {
  .board-col { flex-basis: calc((100vw - 62px) / 4); min-width: 92px; max-width: none; }
}
@media (min-width: 800px) and (min-height: 501px) { .board-col { flex-basis: 18vw; } }
.board-col.drop-target { outline: 2.5px solid var(--brand); box-shadow: var(--shadow-lift); }
.board-day-head {
  font-weight: 800; font-size: 14px; margin-bottom: 10px;
  display: flex; flex-direction: column; cursor: pointer;
}
.board-day-head span { color: var(--muted); font-size: 12px; font-weight: 700; }
/* Shared absolute-time timeline (see renderBoard in app.js): every block
   and gap below is positioned with inline top/height in real minutes ×
   one scale shared across ALL day columns, not flow-stacked — this is
   what makes same-clock-time activities land on the same row across
   columns. .board-timeline is the positioning root; its own height is
   also set inline (the shared axis span), so every column is the same
   total height regardless of how many/few items that day actually has. */
.board-timeline { position: relative; }
.board-block {
  position: absolute; left: 0; right: 0; overflow: hidden;
  background: var(--brand-light); border-radius: 12px;
  padding: 9px 10px; cursor: grab;
  user-select: none; -webkit-user-select: none; -webkit-touch-callout: none;
  transition: transform .1s, opacity .15s;
  display: flex; flex-direction: column; justify-content: center;
}
/* proportional travel gap between board blocks — top/height set inline
   per the shared scale (see boardGapHtml in app.js), dotted line +
   centered pill label mirror the schedule view's .sched-travel leg styling.
   min-height is a pure legibility floor for a very short gap's label —
   CSS min-height always wins over a smaller inline height regardless, so
   this can't ever push a gap ABOVE its true proportional height, only
   guarantee a floor for the rare short one; the shared board scale
   itself no longer factors gaps in at all (see the scale computation in
   renderBoard), so this floor is purely cosmetic and never affects
   alignment or any other block/gap's size. */
.board-gap {
  position: absolute; left: 0; right: 0; min-height: 16px;
  display: flex; align-items: center; justify-content: center;
}
.board-gap::before {
  content: ''; position: absolute; left: 50%; top: 0; bottom: 0; width: 2px;
  transform: translateX(-50%);
  background: repeating-linear-gradient(var(--muted) 0 3px, transparent 3px 6px);
}
.board-gap-label {
  position: relative; z-index: 1; background: var(--card); color: var(--muted);
  font-size: 10px; font-weight: 800; padding: 1px 6px; border-radius: 8px;
  white-space: nowrap;
}
.board-block.press-armed { animation: board-charge .3s linear forwards; } /* keep in sync with BOARD_LONG_PRESS_MS in app.js */
@keyframes board-charge {
  0% { outline: 2px solid transparent; outline-offset: 0; transform: scale(1); }
  100% { outline: 3px solid var(--brand); outline-offset: 2px; transform: scale(1.03); }
}
.board-block.dragging {
  opacity: .95; cursor: grabbing; box-shadow: var(--shadow-lift);
  transition: none; /* follow the pointer 1:1, no lag */
}
.board-block.imp { border-left: 4px solid var(--accent); }
.board-block.confl { background: rgba(224,91,76,.12); }
/* One flowing paragraph, not two stacked lines: time + the ⓘ button sit
   inline at the very start, the name text flows right after and wraps
   underneath — a real "runaround" layout, not a fixed header row. Line
   count is JS-computed per block (see renderBoard) from the block's
   OWN actual pixel height, floor 2 lines — a block with real spare room
   just shows more of the name; -webkit-line-clamp ellipsizes wherever
   it actually gets cut off, so text never forces the block taller. */
.bb-content {
  display: -webkit-box; -webkit-box-orient: vertical; overflow: hidden;
  font-size: 12.5px; line-height: 1.3; color: var(--ink);
}
/* Same RTL bidi fix as .sched-time — a start-end time range reverses its
   visual order in a Hebrew/Arabic page otherwise; see that rule's comment. */
.bb-time-inline { font-weight: 800; color: var(--brand-deep); white-space: nowrap; direction: ltr; unicode-bidi: isolate; }
/* The filled circular background is GONE (2026-08-26). It was there to
   make a bare "i" read as a button, but the button's content was the
   character "ⓘ", which already draws its own ring — so the result was a
   circle inside a circle, and that doubled ring is what actually looked
   wrong on desktop. Two earlier passes at this report only retuned size
   and vertical-align, which could never fix it.
   The content is now ICON_INFO (an SVG, identical on every platform,
   inheriting currentColor like every other icon here). It supplies the
   ring itself, so the button keeps its 20px hit area and drops the
   background. */
.bb-info-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 20px; height: 20px; margin: 0 3px; border: none; border-radius: 50%;
  background: none; color: var(--brand-deep);
  line-height: 1; vertical-align: -4px;
  cursor: pointer; font-family: inherit; padding: 0; flex-shrink: 0;
}
.bb-info-btn:hover { color: var(--brand); }
.bb-name-inline { font-weight: 700; }
.board-drop-indicator {
  position: absolute; left: 0; right: 0; height: 4px; border-radius: 3px;
  background: var(--brand); z-index: 40; animation: pop .12s ease;
}
/* Real desktop (same 1280px threshold the ad rectangles/2-col layout
   already use) — direct request: "on desktop too dense, optimize for 5
   days, more will be scrolled, enlarge font". Below this the 18vw rule
   above just keeps growing with viewport width, but .board-col's own
   220px max-width caps it well before that on any wide monitor — e.g.
   a 2560px screen showed ~11 columns crammed edge-to-edge. Same
   exact-fit formula as the mobile rules further up, N=5: #board-
   scroll's 16px/side padding (32px) + 4 gaps of 10px (40px) = 72px
   subtracted, divided 5 ways — max-width:none lets columns actually
   reach that width instead of being capped back down to 220px. Placed
   AFTER the base .bb-content/.board-day-head rules above (not next to
   the other board-col media queries) so these overrides actually win
   the cascade — same specificity, so source order decides, and an
   earlier attempt placed right after the 18vw rule got silently
   overridden right back by the unconditional base rules that followed
   it. Larger font to match the now-larger blocks. */
@media (min-width: 1280px) {
  .board-col { flex-basis: calc((100vw - 72px) / 5); max-width: none; }
  .bb-content { font-size: 14px; }
  .board-day-head { font-size: 15px; }
  /* Live report: "the i icon (circled) looks crappy on desktop, looks
     great on phone." Root cause: .bb-info-btn's base rule (further up
     this file) is a fixed touch-target size — 20x20px circle, 13px
     glyph, vertical-align tuned to sit on the mobile 12.5px/1.3
     .bb-content line. It has no override here, so at desktop widths it
     stays pinned at that phone-tuned absolute size while its sibling
     text (.bb-content, just above) gets bumped to 14px — the button no
     longer scales with the text it sits inline with, so it reads as
     undersized/pinched and slightly off the text baseline next to the
     now-larger chip text. Scaled up by the same ~1.12x .bb-content grew
     (12.5px → 14px) and vertical-align re-tuned for the taller 14px/1.3
     line; purely cosmetic, same click/hover/focus behavior. */
  .bb-info-btn { width: 22px; height: 22px; font-size: 15px; vertical-align: -5px; }
}

/* ── collapse / party / cost ── */
.collapse-btn {
  border: none; background: none; color: var(--muted); font-size: 22px;
  font-weight: 800; cursor: pointer; padding: 0 6px; align-self: flex-start;
  transition: transform .18s ease; font-family: inherit;
}
.collapse-btn.open { transform: rotate(90deg); }
.s-name, .sched-body .s-sub { cursor: pointer; }
.party-row { display: flex; align-items: center; gap: 12px; padding: 7px 0; }
/* Adults/children count inputs (2026-08-30) — same row as .party-row's
   veteran/elder/disability chips, just a labeled number input instead. */
.party-count { display: flex; align-items: center; gap: 6px; font-size: 13px; font-weight: 700; color: var(--muted); }
.party-count input { width: 52px; padding: 6px 8px; text-align: center; }
/* category-prefs editor (work_log row 63) — a compact label + inline
   3-state .segmented per category, one row each; label needs a fixed-ish
   share so the segmented control stays legible even for the longest
   translated category name across all 13 i18n.js locales. */
.catpref-row { display: flex; align-items: center; gap: 10px; padding: 6px 0; }
.catpref-label { flex: 0 0 38%; font-size: 13px; font-weight: 700; color: var(--ink); }
.catpref-row .segmented { flex: 1; }
/* Religious sub-selection (work_log row 63 v2) — appears under the
   religious row only, whenever it isn't excluded (auto/אוטומטי or
   more/יותר). Multi-select chips, not a single dropdown — user's own
   words: "allow grouping some" (any combination of religions at once).
   Indented to roughly line up under the segmented control above it via
   the same 38% share as .catpref-label. */
.catpref-subs { margin: 0 0 10px; padding-inline-start: calc(38% + 10px); }
.catpref-subs-lbl { font-size: 12px; font-weight: 700; color: var(--muted); margin-bottom: 6px; }
.catpref-subs-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.catpref-sub-chip {
  display: inline-flex; align-items: center; gap: 5px; font-size: 12.5px; font-weight: 700;
  padding: 6px 12px; border-radius: 13px; background: var(--bg); color: var(--muted);
  border: 1.5px solid rgba(128,128,128,.25); cursor: pointer; user-select: none;
}
.catpref-sub-chip input[type="checkbox"] { width: 14px; height: 14px; accent-color: var(--brand); margin: 0; }
.catpref-sub-chip.active { background: var(--brand-light); color: var(--brand-deep); border-color: transparent; }
.party-label { flex: 1; font-size: 15px; font-weight: 700; }
.stepper { display: flex; align-items: center; gap: 12px; background: var(--field); border-radius: 12px; padding: 4px 8px; border: 2px solid var(--field-edge); }
.stepper button {
  border: none; background: var(--card); color: var(--brand-deep);
  width: 34px; height: 34px; border-radius: 10px; font-size: 20px; font-weight: 800;
  cursor: pointer; box-shadow: var(--shadow); font-family: inherit;
}
.stepper b { min-width: 22px; text-align: center; font-size: 17px; }
.cost-line {
  margin-top: 10px; font-size: 14px; font-weight: 600; color: var(--muted);
  background: var(--bg); border-radius: 12px; padding: 10px 14px;
}
.cost-line b { color: var(--ink); }
.daypass {
  display: inline-block; background: var(--accent); color: #3a2205;
  border-radius: 10px; padding: 2px 10px; font-size: 12px; font-weight: 800; margin-inline-start: 6px;
}
.modal-split { display: flex; gap: 10px; }
.city-suggest {
  background: var(--card); border-radius: 12px; box-shadow: var(--shadow-lift);
  margin: -4px 0 8px; overflow: hidden; position: relative; z-index: 25;
}
.city-suggest-item {
  padding: 11px 14px; font-size: 14px; font-weight: 600; cursor: pointer;
  border-bottom: 1px solid rgba(128,128,128,.12);
}
.city-suggest-item:last-child { border-bottom: none; }
.city-suggest-item:hover, .city-suggest-item:active { background: var(--brand-light); }
/* ArrowDown/Up keyboard-highlighted row (attachCityAutocomplete, app.js) —
   distinct class from :active (a pointer-down pseudo-class) since the
   input keeps real DOM focus during keyboard nav, not the item itself. */
.city-suggest-item.active { background: var(--brand-light); }
input.invalid { border-color: var(--danger) !important; }
.modal-split label { flex: 1; font-size: 13px; font-weight: 700; color: var(--muted); }
#ai-status { margin-top: 8px; font-size: 14px; font-weight: 600; color: var(--ink); line-height: 1.6; }
.status-badge {
  display: inline-block; background: var(--cta-2); color: #fff;
  font-weight: 800; padding: 2px 10px; border-radius: 10px; font-size: 13px;
}
.status-note { color: var(--accent-text); font-weight: 800; }
/* Direct request: "when AI is thinking, do a more visible animation
   showing the server is busy." REOPENED with a concrete spec after the
   first version (a subtle opacity pulse) read as static on the user's
   device: "I see static animation, make dots change 1 2 3 1 2 3 etc,
   0.5 sec steps." Now a sequential COUNT cycle — dot 1 alone, then
   dots 1+2, then 1+2+3, repeat — 0.5s per step (1.5s full cycle),
   hard opacity jumps (same-value keyframe holds, no easing blur), so
   the change per step is unmissable. Dot 1 stays lit through the whole
   cycle by design — it's the count that grows, not a chaser.
   NOTE: no prefers-reduced-motion opt-out on this one, deliberately —
   the first version had it, and a reduced-motion OS setting rendering
   the indicator permanently static is the most likely reason the
   animation was reported as static in the first place. Three 6px dots
   stepping twice a second is well under any vestibular-trigger
   threshold; the user explicitly requires visible motion here. */
.ai-thinking { display: inline-flex; align-items: center; gap: 2px; }
.ai-thinking-dots { display: inline-flex; gap: 3px; margin-inline-start: 2px; }
.ai-thinking-dots span {
  width: 6px; height: 6px; border-radius: 50%; background: var(--brand);
}
/* REOPENED again (2026-08-26): "not 1,2,3,1,2,3 but 0,1,2,3,0,1,2,3."
   The previous cycle kept dot 1 permanently lit, so the count never
   returned to empty and the restart was invisible — the sequence read as
   a continuous fill with an abrupt reset rather than a repeating count.
   Now a FOUR-step cycle with a genuine zero state:

     step 0  (0–25%)    no dots
     step 1  (25–50%)   dot 1
     step 2  (50–75%)   dots 1+2
     step 3  (75–100%)  dots 1+2+3

   Still 0.5s per step, so the cycle is 2s rather than 1.5s. All THREE
   dots are animated now — dot 1 included, since it has to go dark for
   step 0; that is exactly what the old rule (no animation on dot 1)
   made impossible.
   Opacity rather than display/visibility so the dots keep their layout
   box and the surrounding text never reflows as the count changes.
   Same-value keyframe holds keep each step a hard jump with no easing
   blur, and the deliberate absence of a prefers-reduced-motion opt-out
   is unchanged — see the note above. */
.ai-thinking-dots span:nth-child(1) { animation: ai-dot-step1 2s infinite; }
.ai-thinking-dots span:nth-child(2) { animation: ai-dot-step2 2s infinite; }
.ai-thinking-dots span:nth-child(3) { animation: ai-dot-step3 2s infinite; }
/* dot 1 appears at step 1 (0.5s = 25% of the 2s cycle) */
@keyframes ai-dot-step1 {
  0%, 24.9% { opacity: 0; }
  25%, 100% { opacity: 1; }
}
/* dot 2 appears at step 2 (1.0s = 50%) */
@keyframes ai-dot-step2 {
  0%, 49.9% { opacity: 0; }
  50%, 100% { opacity: 1; }
}
/* dot 3 appears at step 3 (1.5s = 75%) */
@keyframes ai-dot-step3 {
  0%, 74.9% { opacity: 0; }
  75%, 100% { opacity: 1; }
}
/* The global reduced-motion rule near the top of this file zeroes EVERY
   animation-duration with !important — that is almost certainly why the
   first version of this indicator was reported as fully static ("I see
   static animation"): on a device with reduced motion set, the pulse
   never ran at all. Re-assert this one animation after it in the
   cascade (same !important, more specific selector) — see the design
   note above for why this particular indicator must stay visibly
   animated even under reduced motion. */
/* Updated 2026-08-26 with the 0,1,2,3 cycle: dot 1 is animated now (it
   has to go dark for step 0), so it must be re-asserted here too — left
   out, it would keep the global 2s while dots 2 and 3 were forced to the
   old 1.5s, and the three would drift permanently out of phase under
   reduced motion. Duration updated to 2s to match the four-step cycle
   for the same reason. */
@media (prefers-reduced-motion: reduce) {
  .ai-thinking-dots span:nth-child(1),
  .ai-thinking-dots span:nth-child(2),
  .ai-thinking-dots span:nth-child(3) {
    animation-duration: 2s !important;
    animation-iteration-count: infinite !important;
  }
}
/* Staging-only ad placeholders (see ads.js's PLACEHOLDER block). These
   exist to make the ad LAYOUT reviewable before an approved AdSense
   account supplies real ids — they occupy exactly the dimensions the real
   units will (320x50 mobile, 300x250 margins), so the surrounding page
   reflow can be judged now rather than discovered in production.
   Styled to be unmistakably scaffolding, not a real ad: dashed outline,
   muted, labelled with its own pixel size. Nobody should be able to
   confuse one of these with inventory. */
.ad-placeholder {
  display: flex; align-items: center; justify-content: center;
  border: 2px dashed var(--field-edge); border-radius: 8px;
  background: var(--field); color: var(--muted);
  font-size: 11px; font-weight: 700; letter-spacing: .08em;
  margin: 0 auto 8px; box-sizing: border-box;
  /* flex-shrink defaults to 1, so a "fixed" 970x90 or 320x50 unit sitting
     in a flex slot container silently SHRINKS whenever the container is
     narrower — which is exactly what made the board leaderboard render
     squeezed and left-hugging instead of a centred 970px box. A fixed ad
     size has to be genuinely fixed: an AdSense unit rendered at anything
     other than its declared dimensions does not fill correctly.
     This is also the concrete answer to "make sizes fix, zoom will only
     impact the font size" — the box keeps its declared px size and the
     layout adapts around it, rather than the box being squashed. */
  flex: 0 0 auto;
}
/* Same guarantee for real <ins> units, for the same reason. */
#ad-slot-mobile ins, #ad-slot-board ins,
#ad-slot-left ins, #ad-slot-right ins { flex: 0 0 auto; }
/* Admin-only "view as" switch in the title bar. Sized and coloured to
   read as chrome rather than a primary action -- it is a developer
   affordance sitting next to the menu button, not something a user is
   meant to reach for. Hidden entirely for non-admins (the .hidden class);
   this only styles it for the few who do see it. */
/* #viewas-btn's own type treatment is NOT here — it lives immediately
   after the shared title-bar rule further down, because that rule sets
   font-size and would otherwise override anything declared up here.
   That ordering trap has now bitten this file twice: first the per-button
   boxes overriding the shared size, then this one in the opposite
   direction, which is why the switch rendered in huge letters. */
.conflict-fix { margin-inline-start: 4px; padding: 2px 9px; font-size: 11px; }
.check-row {
  display: flex; align-items: center; gap: 10px; margin-top: 10px;
  font-size: 14px; font-weight: 600; cursor: pointer;
}
.check-row input[type="checkbox"] { width: 18px; height: 18px; accent-color: var(--brand); }
#btn-bulkmode { float: none; padding: 4px 8px; }
#btn-bulkmode.on { background: var(--danger); color: #fff; border-color: var(--danger); }
#bulk-toolbar { display: flex; align-items: center; gap: 12px; margin: 8px 0 4px; flex-wrap: wrap; }
#bulk-toolbar .check-row { margin-top: 0; }
.bulk-check { width: 20px; height: 20px; accent-color: var(--danger); margin-inline-end: 4px; flex: none; }
.bulk-check-spacer { width: 20px; margin-inline-end: 4px; flex: none; }
.modal-split label input[type="time"] { margin-bottom: 0; margin-top: 4px; }

/* ── header nav / menu / share / log preview ── */
#header h1 { cursor: pointer; }
.header-right { display: flex; align-items: center; gap: 10px; }
/* pills were a lightening white overlay (rgba(255,255,255,.18) on the brand
   gradient) — that pushes the pill TOWARD white, which drops white-text
   contrast to 2.70:1 (fails WCAG AA 4.5:1). A darkening black overlay gives
   the same "frosted pill" look but reaches 4.98:1. */
/* Every title-bar control is the SAME SQUARE (direct request: "make both
   square, so make all like the menu"). They previously each sized
   themselves from their own content and padding — the menu button was
   18px/6px 12px, the offline toggle 16px/6px 10px, and the language
   control was a native <select>, which additionally reserves room for a
   dropdown chevron and its widest option. Four different widths sitting
   in a row read as crowded no matter how little each one contains.
   A fixed 40px box with centred content makes them a uniform set, and a
   flex centre means the glyph inside no longer has to be baseline-tuned
   per button. 40px also keeps every one of them at the minimum
   comfortable touch target on a phone, which the tighter ones were
   already close to failing. */
#menu-btn, #offline-toggle, #lang-sel, #viewas-btn {
  width: 40px; height: 40px; padding: 0; flex-shrink: 0;
  display: inline-flex; align-items: center; justify-content: center;
  background: rgba(0,0,0,.20); border: none; color: #fff;
  border-radius: 10px; cursor: pointer; font-family: inherit;
  font-size: 18px; line-height: 1;
}
#menu-btn:hover, #offline-toggle:hover,
#lang-sel:hover, #viewas-btn:hover { background: rgba(0,0,0,.32); }
/* Sizing/background/radius all come from the shared title-bar rule above.
   These rules deliberately carry ONLY what is specific to each button —
   they used to restate the full box, and being defined LATER in the file
   they silently overrode the shared rule, which is why the buttons stayed
   different sizes after that rule was added. */
#lang-sel {
  /* Flag glyphs are drawn from an emoji font and read visually smaller
     than a text glyph at the same font-size, so this is bumped slightly
     to match the menu icon optically rather than numerically. */
  font-size: 19px;
}
/* Content is a short WORD (FREE / PREM / ADMIN), not a glyph, so it needs
   a far smaller and tighter size than the icon buttons to fit the same
   40px square — "ADMIN" is the widest and sets the size. Must stay AFTER
   the shared rule above, which declares font-size: 18px; declared before
   it, this was overridden and the switch rendered in huge letters. */
#viewas-btn { font-size: 9px; font-weight: 800; letter-spacing: .02em; white-space: nowrap; }
/* Code fallback on platforms with no flag emoji (see renderLangBtn): two
   letters need a smaller, bolder treatment than a flag glyph to sit
   comfortably in the same 40px square. */
#lang-sel.lang-code-only { font-size: 13px; font-weight: 800; }
/* The language list inside the picker modal — flag, native name, code. */
.lang-list { display: flex; flex-direction: column; gap: 4px; max-height: 60vh; overflow-y: auto; }
.lang-opt {
  display: flex; align-items: center; gap: 10px; width: 100%;
  padding: 10px 12px; border: 2px solid transparent; border-radius: 12px;
  background: var(--field); color: var(--ink); cursor: pointer;
  font-family: inherit; font-size: 15px; font-weight: 700; text-align: start;
}
.lang-opt:hover { border-color: var(--brand); }
.lang-opt.active { border-color: var(--brand); background: var(--brand-light); color: var(--brand-deep); }
.lang-flag { font-size: 20px; line-height: 1; }
.lang-name { flex: 1; }
.lang-code { font-size: 12px; font-weight: 800; color: var(--muted); letter-spacing: .06em; }
#offline-toggle {
  line-height: 0; /* SVG-only content — kill the inline-text baseline gap */
}
/* Monochrome wifi icon pair (direct feedback on row 11: "monochrome icon
   like wifi connected or disconnected", replacing the 📡 emoji): exactly
   one of the two inline SVGs shows at a time, driven entirely by the
   .forced-offline class the JS already toggles. */
#offline-toggle .wifi-off { display: none; }
#offline-toggle.forced-offline .wifi-on { display: none; }
#offline-toggle.forced-offline .wifi-off { display: inline; }
#offline-toggle.forced-offline {
  background: rgba(255,100,100,.30); color: #fff;
}
#auth-btn {
  background: rgba(0,0,0,.20); border: none; color: #fff;
  font-size: 13px; font-weight: 700; border-radius: 10px; padding: 6px 12px;
  cursor: pointer; font-family: inherit;
}
#auth-btn.auth-circle {
  width: 32px; height: 32px; border-radius: 16px; padding: 0;
  background: #fff; color: var(--brand-deep); font-size: 15px; font-weight: 800;
  overflow: hidden; display: flex; align-items: center; justify-content: center;
}
#auth-btn.auth-circle img { width: 100%; height: 100%; object-fit: cover; }
.drop-indicator { height: 3px; background: var(--brand); border-radius: 2px; margin: 2px 0; }
.btn-danger { background: rgba(224,91,76,.15); color: var(--danger); }
.anchor-row.placeholder .anchor-icon { opacity: .45; }

/* pinned trip-start / trip-end rows */
.pin-row {
  border: 2px dashed var(--brand); border-radius: 14px;
  padding: 12px 14px; margin-bottom: 10px; cursor: pointer;
  background: var(--brand-light);
}
.pin-row.placeholder { border-style: dashed; background: var(--bg); border-color: rgba(128,128,128,.35); }
.pin-label { font-size: 12px; font-weight: 800; text-transform: uppercase; letter-spacing: .5px; color: var(--brand-deep); margin-bottom: 4px; }
.pin-row.placeholder .pin-label { color: var(--muted); }
.pin-when { font-size: 16px; font-weight: 800; }
.pin-detail { font-size: 13px; font-weight: 600; color: var(--muted); margin-top: 2px; }
.pin-placeholder { font-size: 14px; font-weight: 700; }
.pin-edit-title { font-size: 15px; font-weight: 800; color: var(--brand-deep); margin-bottom: 4px; }
.anchor-row .anchor-main { cursor: pointer; }
#day-title-input { margin-bottom: 12px; background: var(--card); box-shadow: var(--shadow); border: none; }
.menu-row { display: flex; justify-content: space-between; align-items: center; padding: 8px 0; font-size: 15px; font-weight: 600; }
.menu-sep { border: none; border-top: 1px solid rgba(128,128,128,.2); margin: 12px 0; }
#modal .btn-primary { margin-top: 8px; }
/* Row 92 — reported with a screenshot: the flight/address tab row and the
   field directly beneath it (the "Airport (TLV…)" input) were flush at
   0px, so the input read as part of the tab control rather than as the
   next field.

   Root cause: the shared `textarea, input, select` rule carries a
   margin-BOTTOM only, and .segmented had no margin-bottom of its own, so
   a control followed by an input had literally nothing between them.
   Every other pair in a modal looked fine purely because both members
   happened to be inputs.

   Scoped to #modal on purpose: .catpref-row .segmented is a flex:1 cell
   inside a row in Trip Properties, where a bottom margin would misalign
   it against its label. */
#modal .segmented { margin-bottom: 14px; }
/* The location group (airport OR address — one is always hidden) sits
   directly under the tab control. Its own inputs keep the shared
   margin-bottom, so this only needs to not collapse against the tabs. */
#modal .field-after-tabs { margin-top: 0; }
.share-code {
  background: var(--bg); border-radius: 8px; padding: 2px 8px;
  font-size: 12px; cursor: pointer; word-break: break-all;
}
.log-preview {
  background: var(--bg); border-radius: 12px; padding: 10px;
  font-size: 11px; line-height: 1.5; max-height: 180px; overflow: auto;
  white-space: pre-wrap; word-break: break-all; font-family: Consolas, monospace;
}
.btn-props { margin-bottom: 8px; background: var(--card); color: var(--brand-deep); box-shadow: var(--shadow); }

/* empty state */
.onboard-rect {
  margin: 40px 16px; padding: 34px 24px; text-align: center;
  border: 2px dashed var(--brand); border-radius: var(--radius);
  color: var(--ink); font-size: 16px; font-weight: 700; cursor: pointer;
  background: var(--card);
}
/* Row 94 / 2026-08-30 direct request ("move all the these instructions in
   a rectangle same as the green saying press +, but different color, and
   vanish it if one having a trip. also add clear link to the full
   guide."): the value-proposition panel under the "add a trip" CTA, shown
   only while the trip list is empty (trip-view.js's showTrips() only
   renders this whole block when there are zero trips, so it vanishes for
   good the moment a first trip exists — no separate flag needed). Same
   rounded-rectangle shape/padding as .onboard-rect above it, but the
   accent color (not brand green) so the two read as two distinct blocks,
   not one repeated card. Was 14px muted gray ("tiny font" complaint) —
   now 15px on the higher-contrast --accent-text (already AA-checked, see
   :root's --accent-text comment). Its top margin is negative against
   .onboard-rect's 40px so the two still read as one grouped unit. */
.onboard-pitch {
  margin: -24px 16px 40px; padding: 22px 24px;
  text-align: center; border-radius: var(--radius);
  border: 2px solid var(--accent-light);
  background: var(--accent-light); color: var(--accent-text);
  font-size: 15px; font-weight: 500; line-height: 1.7;
}
.onboard-pitch p { margin: 0 0 12px; }
/* Clear, persistent (not one-time) link into the full guide — opens the
   in-app Guide view (App.showGuide()). A <button>, not an <a>, since it
   navigates within the app rather than to another page — reset to
   plain-button defaults, styled as an underlined link so it visibly reads
   as "go read more", distinct from the pitch text above it. */
.onboard-guide-link {
  display: inline-block; background: none; border: none; padding: 0;
  font: inherit; font-size: 15px; font-weight: 700; color: var(--accent-text);
  text-decoration: underline; cursor: pointer;
}
/* Guide view (2026-08-28) — direct feedback: no per-feature emoji, one
   unified professional marker instead. A small solid brand-color dot
   before each title, plain CSS, same across every feature — the
   "unified" part is that every card gets the exact same marker, not a
   different pictogram per feature. */
#view-guide .guide-feature { margin-bottom: 12px; }
#view-guide .guide-feature .card-title {
  display: flex; align-items: center; gap: 10px;
}
#view-guide .guide-feature .card-title::before {
  content: ''; flex: 0 0 auto; width: 8px; height: 8px; border-radius: 50%;
  background: var(--brand);
}
#view-guide .guide-howto {
  font-size: 13px; color: var(--muted); margin: 4px 0 0;
}
#view-guide .guide-howto::before { content: "→ "; }
#view-guide #guide-cta { display: block; width: 100%; margin-top: 8px; }
@media (min-width: 1280px) {
  /* 560px -> 728px (+30%), direct request: "on desktop widen the widths
     by 30%" — same desktop breakpoint as .view-2col above; mobile/tablet
     keep the base .view max-width unchanged. */
  #view-guide { max-width: 728px; }
}
/* Public live user/trip counter footer (work_log row 68) — small, muted,
   centered under the trip list. Bottom margin clears the fixed .fab
   button so the two never visually overlap on a short/empty list. */
/* Row 68 design pass, user verbatim: "should be marketing, look bolder.
   there are digital led counters which would be a good animation. no
   icon, larger fonts." Two seven-segment-style displays side by side:
   big glowing monospace digits on a dark pill (LED look works on a dark
   ground in BOTH themes — it's a display device, not themed text),
   small uppercase label beneath each. The count-up animation lives in
   app.js (animateLedCount, rAF ease-out). */
.stats-footer {
  display: flex; justify-content: center; gap: 14px;
  margin: 22px 16px calc(env(safe-area-inset-bottom) + 90px);
}
/* Admin-only click target (app.js's renderStatsFooter) — opens the
   activity report. A visible affordance rather than a silent click zone,
   since nothing about the counter itself hints it's tappable otherwise. */
.stats-footer-clickable { cursor: pointer; }
.stats-footer-clickable .led-stat { transition: box-shadow .15s; }
.stats-footer-clickable:hover .led-stat, .stats-footer-clickable:focus-visible .led-stat {
  box-shadow: var(--shadow), inset 0 0 12px rgba(0,0,0,.6), 0 0 0 2px var(--brand);
}
.led-stat {
  display: flex; flex-direction: column; align-items: center; gap: 4px;
  background: #101614; border-radius: 14px; padding: 12px 22px;
  box-shadow: var(--shadow), inset 0 0 12px rgba(0,0,0,.6);
  /* Direct feedback: "make both counters same size" — min-width alone
     let the pill with the longer label/number grow wider than its
     sibling. flex:1 (equal share of the row) + a shared max cap keeps
     the pair identical regardless of digit/label length. */
  flex: 1 1 0; max-width: 200px; min-width: 120px;
}
.led-num {
  font-family: Consolas, "Courier New", monospace;
  font-size: 32px; font-weight: 800; letter-spacing: 3px;
  color: #2fd6a3;
  text-shadow: 0 0 6px rgba(47,214,163,.9), 0 0 18px rgba(47,214,163,.45);
  font-variant-numeric: tabular-nums; /* digits keep a fixed width while counting — no jitter */
  line-height: 1;
}
.led-lbl {
  font-size: 11px; font-weight: 700; letter-spacing: 1.5px;
  text-transform: uppercase; color: #8ba59b;
}
.t-actions { display: flex; align-items: center; gap: 10px; }
.trash-btn {
  background: none; border: none; font-size: 18px; cursor: pointer;
  opacity: .55; padding: 4px; filter: grayscale(100%);
}
.trash-btn:active { opacity: 1; }

/* ── ads (free-tier only) ──────────────────────────────────────────────
   Fixed sizes only, never "adaptive"/"responsive" ad units — a real
   lesson from the Android app's ad code: adaptive banners have an
   unpredictable per-fill height and visibly jump the layout on load/
   refresh. Fixed height reserved from the start (even before real ads
   are configured) means the space is always there and nothing shifts
   when ads go live.
   Two tiers:
     mobile (<1280px): one sticky-bottom 320x50 banner
     desktop (≥1280px): 300x250 Medium Rectangle in each margin — the
       best-performing standard IAB size (highest advertiser demand/fill/
       eCPM of any fixed size, and landscape-proportioned, unlike a tall
       skyscraper) beats one 160x600 on revenue, not just looks. Stacked
       to 2 per side (4 total) at ≥1600px, where the ~520px of margin
       space per side would otherwise sit dead.
   At most one ad slot TYPE is visible at once (mobile banner XOR margins)
   — the desktop tier only changes how many rectangles stack, it never
   also shows the mobile strip. */
/* Elements start with class="hidden" in the HTML (same convention as the
   rest of the app) — JS reveals only the slot(s) that apply once it knows
   both the breakpoint AND free-tier eligibility. Below 1280px the desktop
   slots are force-hidden (even if JS forgot to hide them) so a resize
   never leaves a stray ad docked over content; same in reverse for the
   mobile strip above 1280px. */
#ad-slot-mobile {
  position: fixed; left: 0; right: 0;
  bottom: 0; height: 50px;
  padding-bottom: env(safe-area-inset-bottom);
  display: flex; align-items: center; justify-content: center;
  background: var(--card); box-shadow: 0 -2px 10px rgba(30,70,55,.08);
  z-index: 20;
}
#ad-slot-mobile ins { width: 320px; height: 50px; }
#ad-slot-left, #ad-slot-right {
  position: fixed; top: 100px; width: 300px;
  display: flex; flex-direction: column; align-items: center; gap: 16px;
}
#ad-slot-left { left: 24px; }
#ad-slot-right { right: 24px; }
#ad-slot-left ins, #ad-slot-right ins { width: 300px; height: 250px; }
/* Side-ad breakpoint raised 1280px -> 1400px, and the body is inset to
   match (2026-08-26).

   Reported with a screenshot at browser zoom: the margin ads sat ON TOP of
   the itinerary and the AI panel. Root cause is arithmetic that was never
   done. The content column is .view-2col at max-width:1150px, centred, so
   the free margin per side is (100vw - 1150) / 2. At 1876px that is 363px
   and a 300px column at left:24px clears it comfortably — which is why
   this looked fine at 100%. Browser zoom shrinks the CSS viewport, so at
   ~1520px CSS the margin is only 185px and the fixed column overlaps the
   content by ~140px. The old 1280px breakpoint tested viewport width alone
   and never asked whether the content itself left room.

   Two changes make the overlap impossible rather than unlikely:
   1. Side ads require 1400px, the point below which there is not enough
      margin for a 300px column plus its 24px offset either side of a
      usable content width.
   2. body.ads-active is PADDED by the full column width on desktop, so
      the centred content can never extend under a fixed column. The
      columns are position:fixed and viewport-relative, so they are
      unaffected by that padding — it frees exactly the space they occupy.

   This also answers "different resolutions handled differently": browser
   zoom changes the CSS viewport width, so it re-evaluates these queries
   live. The ad BOXES stay fixed at 300x250 and 320x50 and never reflow;
   what changes with zoom is how many fit and whether the margins are
   viable at all. */
/* ── "one big mat": zoom scrolls, it never reflows into an overlap ───────
   Chosen deliberately over chasing reflow breakpoints (owner: "zoom to
   scroll, like one big mat — safer, and if only option do that, use it to
   mitigate on too square screens").

   The problem with the previous approach was that it tried to keep the
   layout correct at EVERY width, and a fixed 300px column plus a centred
   1150px content column simply cannot both fit below a certain width.
   Every fix was another breakpoint guess, and the overlap kept coming
   back at some zoom level I had not measured.

   The mat inverts that. Below MAT_MIN the page stops shrinking and
   scrolls horizontally instead, so the geometry that was verified once
   stays verified at every zoom level and on any short/square screen —
   overlap becomes structurally impossible rather than something to keep
   testing for. Browser zoom cannot make CSS pixels overlap a layout that
   refuses to compress.

   THE NUMBER: 1150 content + 2x(300 column + 24 offset) + 2x24 breathing
   = 1846px. Side ads therefore need 1846px of real viewport, NOT the 1400
   guessed earlier — 1400 was the width at which a 560px column still fit,
   but the day view's .view-2col is 1150px, which is exactly the case that
   was reported broken and the one my earlier probe missed because it
   measured the 560px trips column instead.

   Below 1846 the margins are dropped and the bottom strip carries the ad,
   so no width is ever left with no ad at all. */
@media (max-width: 1845px) { #ad-slot-left, #ad-slot-right { display: none !important; } }
@media (min-width: 1846px) {
  /* The mat: never compress below the width the layout was verified at.
     Applied only when ads are active, so a user with no ads (premium,
     admin) keeps the ordinary responsive behaviour and never gets a
     horizontal scrollbar they have no reason to have. */
  /* min-width ALONE is what guarantees clearance — no body inset needed.
     At exactly 1846px the 1150px content centres with (1846-1150)/2 = 348px
     of margin per side, and a 300px column at a 24px offset ends at 324px:
     24px of daylight, which is the breathing room the number was chosen
     for. Measured at 1884px the gap is 43px and it only grows above that.
     An earlier version also set padding-left/right: 348px here. That was
     removed rather than kept "just in case": it measured as 0px at every
     width in a probe — it was being overridden by something I did not
     track down — while min-width from this very same rule applied fine.
     Since the probe proves the inset is not needed, shipping a
     declaration that demonstrably does nothing would just be a trap for
     whoever reads this next. */
  body.ads-active { min-width: 1846px; }
  /* Trip Plan is the exception: no side columns, and no side inset either —
     the board is meant to use the full width. Its ad goes along the bottom
     instead (#ad-slot-board). */
  body.ads-active.board-active { padding-left: 0; padding-right: 0; }
  body.board-active #ad-slot-left,
  body.board-active #ad-slot-right { display: none !important; }
}
/* ── Trip Plan bottom leaderboard ────────────────────────────────────────
   970x90 Large Leaderboard where it fits, 728x90 below that. Chosen over a
   row of 3-4 rectangles: one standard IAB unit has far better demand and
   fill than four small ones competing on the same screen, it is a single
   request rather than four, and at 90px it costs a third of what a row of
   250px rectangles would — on the one view where vertical space IS the
   interaction (dragging sites between day columns).
   Deliberately NOT 970x250 Billboard despite it being the higher-value
   unit: 250px is roughly a third of a day column's visible height here. */
#ad-slot-board {
  position: fixed; left: 0; right: 0; bottom: 0; height: 90px;
  padding-bottom: env(safe-area-inset-bottom);
  display: flex; align-items: center; justify-content: center;
  /* --bg, not --card: the bar spans the full width but the unit inside it
     is only 970px, so on a wide screen most of it is the surround. In
     --card (white) that surround read as a bright slab pasted across the
     bottom of a grey page. Matching the page background lets the ad sit IN
     the page instead of on a panel, which is also the less attention-
     grabbing treatment — the ad should be the thing that stands out, not
     its container. Still fully opaque, since content scrolls underneath. */
  background: var(--bg);
  border-top: 1px solid var(--field-edge);
  z-index: 20;
}
#ad-slot-board ins, #ad-slot-board .ad-placeholder { height: 90px; }
/* BOARD ONLY — this gate was missing.
   The comment below already SAID "only on the board", but nothing actually
   checked body.board-active, so the leaderboard rendered on every desktop
   screen. The trip view ended up showing 6 margin rectangles AND a 970x90
   bar: seven units, reported as "lower banner seems too much in desktop".
   The width queries were never the constraint that mattered here.
   Default hidden and revealed only on the board, so a screen that forgets
   to set the class fails safe (no ad) rather than loudly wrong. */
#ad-slot-board { display: none; }
body.board-active.ads-active #ad-slot-board { display: flex; }
/* Below 1400px even the board falls back to the same 320x50 strip every
   other screen uses. */
@media (max-width: 1399px) { #ad-slot-board { display: none !important; } }
@media (min-width: 1400px) {
  body.board-active.ads-active #ad-slot-mobile { display: none !important; }
  /* The board's own scroll region must END above the banner, or a site
     dragged toward the bottom edge lands behind it — the same defect class
     as the + button being covered, which is worth not repeating. */
  body.board-active.ads-active #board-scroll { padding-bottom: 98px; }
}
/* Reported 2026-08-26 on the staging placeholder layout: "on desktop no
   need both sides and bottom."

   The comment above these rules already CLAIMED this ("same in reverse
   for the mobile strip above 1280px") — but the rule was never written,
   only the max-width one for the side slots. So on desktop the page
   showed two 300x250 margin columns AND the 320x50 bottom strip at the
   same time, which is both ugly and more inventory than the layout was
   designed around. This is the missing half of that pair. */
/* This must be the exact complement of the SIDE-AD threshold, or a band of
   widths ends up with no ad at all. It has moved twice for that reason:
   1280 -> 1400 -> 1846, tracking the side-ad query each time.
   1846 is where the margins genuinely fit (see the mat block above), so
   below it the strip carries the ad and above it the margins do. Exactly
   one treatment at every width, never zero and never both.
   The board's own bottom leaderboard is deliberately NOT tied to this
   number — it is a bottom bar with no overlap risk, so it can appear from
   1400px, and the board block hides this strip itself when it does. */
@media (min-width: 1846px) { #ad-slot-mobile { display: none !important; } }
/* room for the sticky mobile ad strip so it never covers the last card */
body.ads-active { padding-bottom: calc(50px + env(safe-area-inset-bottom)); }
/* Reported 2026-08-26: "on phone in trip manager ad is hiding the +
   button." body.ads-active's padding-bottom reserves room in the page
   FLOW, but .fab is position:fixed and so ignores that padding entirely —
   it kept sitting 22px off the viewport bottom, directly underneath the
   50px ad strip (which is z-index 20). Any fixed, bottom-anchored control
   has the same problem, so it is lifted by the strip's full height rather
   than nudged by a guessed amount.
   Scoped to the breakpoint where the strip actually exists: above 1280px
   the strip is now hidden (rule above), so lifting the button there would
   leave it floating for no reason. */
@media (max-width: 1279px) {
  body.ads-active .fab { bottom: calc(env(safe-area-inset-bottom) + 22px + 50px); }
}

/* ── install-as-app banner (Android real prompt / iOS instructions) ── */
.install-banner {
  position: fixed; left: 12px; right: 12px;
  bottom: calc(env(safe-area-inset-bottom) + 12px);
  background: var(--card); color: var(--ink); border-radius: var(--radius);
  box-shadow: var(--shadow-lift); padding: 14px 16px; z-index: 40;
  display: flex; align-items: center; gap: 12px;
  animation: install-banner-up .25s ease;
  max-width: 480px; margin: 0 auto;
}
@keyframes install-banner-up { from { transform: translateY(24px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
.install-banner-icon { font-size: 28px; line-height: 1; flex: none; }
.install-banner-text { flex: 1; min-width: 0; }
.install-banner-title { font-weight: 800; font-size: 14px; }
.install-banner-body { font-size: 12px; color: var(--muted); margin-top: 2px; }
.install-banner-actions { display: flex; flex-direction: column; gap: 6px; flex: none; }
.install-banner-actions .btn-primary { padding: 7px 14px; font-size: 13px; }
.install-banner-actions .btn-small { padding: 5px 10px; font-size: 12px; }
