/* Shared layout tokens — single source of truth for navbar height, iOS PWA
   safe-area insets, and z-index layering. Reference these from every view
   instead of hardcoding 56px / 1090 / etc. — that's how things drifted before
   and is what made tweaks accidentally break sticky/fixed anchors. */
:root {
  --nav-height:   56px;
  --safe-top:     env(safe-area-inset-top, 0px);
  --safe-right:   env(safe-area-inset-right, 0px);
  --safe-bottom:  env(safe-area-inset-bottom, 0px);
  --safe-left:    env(safe-area-inset-left, 0px);

  /* How much of the viewport bottom the software keyboard is covering.
     There is no CSS unit for this — lvh/dvh don't shrink for the keyboard
     on iOS — so keyboard-inset.js measures it off visualViewport and
     writes it here. Stays 0px when no keyboard is up, or if the script
     isn't loaded on this page. */
  --kb-inset:     0px;

  /* z-index ladder — pick the closest tier rather than inventing a new
     number. New floating UI in-page → --z-floating; nav-anchored popups
     (avatar/site menus) → --z-nav-menu. Modal stays above --z-floating so
     dialogs claim the screen while open (that's why bd/modal are bumped
     above their Bootstrap defaults of 1050/1055 — see the override below). */
  --z-nav:        1030;  /* Bootstrap fixed-top */
  --z-overlay:    1040;  /* scrims, click-traps above the nav, below content */
  --z-floating:   1090;  /* in-page floating UI: action clusters, section pill, FABs */
  --z-modal-bd:   1094;
  --z-modal:      1095;
  --z-nav-menu:   2000;  /* nav-anchored popups — top of stack */
}

body {
  /* !important is load-bearing here: dozens of views ship inline
     `style="padding-top:56px"` (legacy of pre-token days), which would
     otherwise win the cascade and zero out the iOS PWA safe-area inset
     — leaving the navbar and the page header visible *behind* the
     translucent status bar on Dynamic Island devices. */
  padding-top: calc(var(--nav-height) + var(--safe-top)) !important;
}

.breadcrumb-item+.breadcrumb-item::before {
  color: #fff !important;
}

.navbar-brand {
  margin-right: 0 !important;
}

/* iOS standalone-mode (PWA) intermittently detaches position:fixed
   elements during scroll / overscroll bounce — they briefly track the
   scroll position instead of staying pinned. Promoting them to their
   own compositor layer (translate3d + will-change) makes iOS treat
   them as a separate render target that the scroll handler can't
   move. Applies to .fixed-top (the global nav), every .fab (back-to-
   top buttons etc.), and the album/MorgenGram action clusters.

   Trade-off: a transformed ancestor breaks backdrop-filter on iOS
   Safari descendants — the cosmetic saturate(1.2) inside #ev-actions /
   #mg-actions buttons effectively becomes a no-op. We keep the GPU
   promotion since fixed-element detachment is worse than losing 20%
   saturation. New floating elements should add a class to this list
   (or use .fab) rather than inventing a new id-based selector. */
.fixed-top, .fab,
#ev-actions, #mg-actions, #mg-cal-popup, #mg-view-menu {
  -webkit-transform: translate3d(0, 0, 0);
  transform: translate3d(0, 0, 0);
  will-change: transform;
  -webkit-backface-visibility: hidden;
  backface-visibility: hidden;
}

/* Photo upload triggers carry data-upload-trigger so public/js/upload-
   fullres.js can attach long-press / Shift+click handlers for the
   "Upload at full resolution" toggle. The CSS below suppresses iOS
   Safari's text selection + long-press callout on those elements so
   the gesture is purely ours — the JS-level contextmenu preventDefault
   alone isn't enough, the selection fires first. */
[data-upload-trigger] {
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
}

/* Per-mode upload-trigger paint. Cycled by long-press / Shift+click on
   any [data-upload-trigger] via public/js/upload-fullres.js:
     normal → default blue (no override here)
     hires  → purple
     orig   → gold
   !important is intentional — it has to win against Bootstrap .btn-primary,
   album/morgenGram .*-top-btn rules, and visit-live .btn-plain rules in
   a single declaration. */
body.upload-mode-hires [data-upload-trigger] {
  background-color: #6f42c1 !important;
  border-color: #6f42c1 !important;
  color: #fff !important;
}
body.upload-mode-hires [data-upload-trigger]:hover,
body.upload-mode-hires [data-upload-trigger]:focus {
  background-color: #5a32a3 !important;
  border-color: #5a32a3 !important;
  color: #fff !important;
}
body.upload-mode-orig [data-upload-trigger] {
  background-color: #c9a227 !important;
  border-color: #c9a227 !important;
  color: #fff !important;
}
body.upload-mode-orig [data-upload-trigger]:hover,
body.upload-mode-orig [data-upload-trigger]:focus {
  background-color: #a8841d !important;
  border-color: #a8841d !important;
  color: #fff !important;
}

/* Standard floating action button. Sits at the bottom-right of the
   viewport, respects iOS PWA safe-area on both sides so the home
   indicator doesn't clip the button. Layers at --z-floating, below
   modals but above other in-page floating UI. Pair with .fab-left or
   override `right` for variants. */
.fab {
  position: fixed;
  right:  calc(1rem + var(--safe-right));
  bottom: calc(1rem + var(--safe-bottom));
  z-index: var(--z-floating);
}

/* Full-viewport-minus-nav container for messaging-style pages where the
   composer/input must stay anchored to the bottom. We use lvh (LARGE
   viewport height) instead of dvh because iOS PWAs report dvh
   inconsistently — sometimes excluding safe areas, sometimes
   including them, sometimes shrinking with the soft keyboard — which
   left the composer either off-screen or floating mid-screen
   depending on the device. lvh always equals the full physical
   viewport in standalone PWA mode. We subtract --nav-height (for
   the fixed navbar) and --safe-top (matching body's padding-top)
   so shell-top sits exactly below the navbar and shell-bottom lands
   at the physical screen bottom; composer's own padding-bottom (in
   chat.css) keeps the textarea above the home indicator pill.
   Combine with display:flex; flex-direction:column on the same
   element — see chat.css/.chat-root, group.ejs/.group-shell.

   Because lvh ignores the soft keyboard by design, the keyboard would
   otherwise cover the composer on iPad (and any device where the page
   itself can't scroll — this container is exactly viewport-tall, so the
   browser has no slack to scroll the focused field into view). Subtract
   --kb-inset, which keyboard-inset.js keeps in sync with visualViewport;
   include the script on any page that uses this class. */
.page-full-height {
  height: calc(100vh - var(--nav-height) - var(--safe-top) - var(--kb-inset, 0px));
  height: calc(100lvh - var(--nav-height) - var(--safe-top) - var(--kb-inset, 0px));
}
/* With the keyboard up, the home indicator is behind it — the composer's
   safe-area padding is just dead space between the text field and the
   keyboard, so drop it back to the plain padding. */
html[data-kb-open] .chat-composer {
  padding-bottom: 0.5rem !important;
}
/* Date popup (album) still centers itself (position:fixed; left:50%), so fold
   the GPU promotion into its centering transform to avoid killing it. */
#ev-date-bar-popup {
  -webkit-transform: translate3d(-50%, 0, 0);
  transform: translate3d(-50%, 0, 0);
  will-change: transform;
  -webkit-backface-visibility: hidden;
  backface-visibility: hidden;
}
/* The date pill itself no longer centers via transform — it's a flex child of
   #ev-float-pills (album.ejs), centered by the container. It must NOT carry a
   translateX(-50%) (that double-shifted it half its width to the left on every
   viewport). Keep only the GPU promotion, with NO horizontal offset. */
#ev-date-bar {
  -webkit-transform: translateZ(0);
  transform: translateZ(0);
  will-change: transform;
  -webkit-backface-visibility: hidden;
  backface-visibility: hidden;
}

/* Bootstrap's default modal z-index is 1055 (backdrop 1050), below the
   album's floating section pill (z 1091). That made the pill paint over
   open modals. Raise modals above --z-floating so dialogs own the screen
   while they're up. The nav popups (--z-nav-menu, 2000) sit higher still
   — they only open by themselves, so this doesn't conflict. */
.modal { --bs-modal-zindex: var(--z-modal); z-index: var(--z-modal) !important; }
.modal-backdrop { --bs-backdrop-zindex: var(--z-modal-bd); z-index: var(--z-modal-bd) !important; }

/* Lightbox overlay: when EITHER fullscreen photo lightbox is open, force
   the body background to black so the iOS PWA safe-bottom strip (where
   position:fixed; inset:0 clamps short of the screen bottom on iOS) shows
   continuous black instead of the page background bleeding through. The
   lightbox itself is inset:0; background:#000 — this just extends that
   black underneath into the home-indicator area.

   Uses :has() (Safari 15.4+, Chrome 105+) so we don't have to wire body
   class toggles into every JS open/close path. Falls back gracefully on
   older browsers — the gap stays visible but nothing breaks. */
body:has(#ev-lightbox[style*="display: block"]),
body:has(#mg-lightbox[style*="display: block"]),
body:has(#ev-slideshow[style*="display: block"]),
body:has(#mg-slideshow[style*="display: block"]) {
  background: #000 !important;
}

/* iOS PWA safe-area inset for Bootstrap modals. In standalone mode with
   viewport-fit=cover + apple-mobile-web-app-status-bar-style=black-
   translucent (set in theme-init.ejs), the page renders BEHIND the
   status bar / Dynamic Island. body padding-top already accounts for
   that, but Bootstrap's .modal-dialog uses its own margin (0.5rem on
   small screens, 1.75rem on >=576px) measured from the top of the
   viewport, so a top-aligned modal's title row sits behind the time/
   signal/battery indicators. In a regular browser --safe-top is 0 so
   this rule is a no-op; in PWA it pushes the dialog down by the inset.
   Applies to vertically-centered modals too — they're centered in
   100% minus the margins, so adding to the top margin just shifts the
   centered area down, which is the intended behavior. */
.modal-dialog {
  margin-top: calc(0.5rem + var(--safe-top));
}
@media (min-width: 576px) {
  .modal-dialog {
    margin-top: calc(1.75rem + var(--safe-top));
  }
}

/* Card shadow tokens. The slate-tinted shadow pair below is duplicated
   verbatim across .store-hero / .song-hero / .panel / .music-toolbar /
   .alb-tile and several home-page tiles. Define once here so a tweak to
   the elevation system applies everywhere. */
:root {
  --card-shadow:       0 1px 2px rgba(15, 23, 42, 0.04), 0 4px 12px rgba(15, 23, 42, 0.04);
  --card-shadow-hover: 0 2px 6px rgba(15, 23, 42, 0.08), 0 18px 40px rgba(15, 23, 42, 0.12);
}

/* Empty-state card — dashed border on a muted background, used as the
   "no items yet" placeholder in memories + travel. Consolidated from 4
   byte-identical per-view copies; theme-init.ejs handles the dark-mode
   background swap via the .empty-card entry in its card-class list. */
.empty-card {
  border: 2px dashed var(--app-card-border, rgba(0, 0, 0, 0.15));
  border-radius: 0.6rem;
  padding: 3rem 1rem;
  text-align: center;
  color: var(--app-muted, #6c757d);
}

/* Bootstrap tab override used by memories + travel top-bar nav. The
   default .active state inherits transparent background which looks
   broken against the page bg. Swap to the card surface. */
.top-tabs .nav-link.active {
  background: var(--app-card-bg, #fff);
  color: var(--app-page-fg, #212529);
  border-color: var(--app-card-border, rgba(0, 0, 0, 0.125))
                var(--app-card-border, rgba(0, 0, 0, 0.125))
                transparent;
}

/* Uppercase tracked section label used to introduce groups of tiles or
   list rows. Consolidated from store / music-list / albums-list. */
.section-heading {
  font-size: 0.78rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: #64748b;
  margin: 0 0 14px;
  display: flex;
  align-items: center;
  gap: 8px;
}
.section-heading i {
  font-size: 1rem;
}

/* Share-list card — used by both /memories (user-default share) and the
   memory edit page (per-memory extraShare). Markup in
   views/partials/_memory-share-card.ejs, behavior in
   public/js/memory-share.js. Kept here so a future use site picks up
   the same look automatically. */
.share-card {
  background: var(--app-card-bg, #fff);
  border: 1px solid var(--app-card-border, #e3e6ea);
  border-radius: 0.6rem;
  /* No overflow:hidden — the search-results dropdown is position:
     absolute and needs to escape the card's bounds when matches
     extend past the bottom edge. The summary's rounded corners hold
     up fine without clipping. */
}
.share-card > summary {
  list-style: none;
  cursor: pointer;
  padding: 0.7rem 1rem;
  font-weight: 600;
  display: flex; align-items: center; gap: 0.5rem;
}
.share-card > summary::-webkit-details-marker { display: none; }
.share-card > summary::after {
  content: ''; margin-left: auto;
  width: 0.6rem; height: 0.6rem;
  border-right: 2px solid var(--app-muted, #6c757d);
  border-bottom: 2px solid var(--app-muted, #6c757d);
  transform: rotate(45deg);
  transition: transform 0.15s ease;
}
.share-card[open] > summary::after { transform: rotate(-135deg); }
.share-count { color: var(--app-muted, #6c757d); font-weight: 400; font-size: 0.95rem; }
.share-card .share-body { padding: 0.5rem 1rem 1rem; border-top: 1px solid var(--app-divider, #e9ecef); }
.share-chips { display: flex; flex-wrap: wrap; gap: 0.4rem; margin: 0.6rem 0; min-height: 0.5rem; }
.share-chip {
  display: inline-flex; align-items: center; gap: 0.35rem;
  background: var(--bs-primary, #0d6efd); color: white;
  border-radius: 999px;
  padding: 0.18rem 0.4rem 0.18rem 0.5rem;
  font-size: 0.9rem;
}
/* Visually distinguish group pills (slate) from user pills (blue) so a
   glance tells you which is which without reading the chip's text. */
.share-chip-group { background: #475569; }
.share-chip .share-chip-name { line-height: 1.1; }
.share-chip button {
  background: rgba(255,255,255,0.2); border: 0; color: white;
  border-radius: 999px;
  width: 1.3rem; height: 1.3rem;
  display: inline-flex; align-items: center; justify-content: center;
  cursor: pointer;
  padding: 0;
}
.share-chip button:hover { background: rgba(255,255,255,0.35); }
/* Search dropdown — anchored under the input. position:relative on the
   parent .share-search keeps the absolutely-positioned list aligned. */
.share-search { position: relative; }
.share-search input { max-width: 24rem; }
.share-results {
  position: absolute;
  z-index: 5;
  margin: 0; padding: 0;
  list-style: none;
  background: var(--app-card-bg, #fff);
  border: 1px solid var(--app-card-border, #e3e6ea);
  border-radius: 0.4rem;
  box-shadow: 0 4px 14px rgba(0,0,0,0.12);
  max-width: 24rem; min-width: 16rem;
  max-height: 18rem; overflow-y: auto;
  display: none;
  margin-top: 0.2rem;
}
.share-result {
  display: flex; align-items: center; gap: 0.5rem;
  padding: 0.4rem 0.6rem;
  cursor: pointer;
  border-bottom: 1px solid var(--app-divider, #f1f3f5);
}
.share-result:last-child { border-bottom: 0; }
.share-result:hover { background: var(--app-divider, #f1f3f5); }
.share-result i.bi-collection { color: #475569; font-size: 1.05rem; }
.share-result-tag {
  margin-left: auto;
  font-size: 0.75rem;
  color: var(--app-muted, #6c757d);
  text-transform: uppercase;
  letter-spacing: 0.05em;
}
/* Collections feature: tag chip used on item cards and the tags
   management page. Color is set inline on the element from the tag's
   stored HSL — the type accepts both an anchor (clickable filter
   link) and a span (plain label) without restyling. */
.coll-tag-chip {
  display: inline-flex; align-items: center; gap: .25rem;
  padding: .12rem .55rem; border-radius: 999px;
  color: #fff !important; font-size: .78rem; line-height: 1.4;
  text-shadow: 0 0 1px rgba(0,0,0,.45);
  white-space: nowrap;
}
.coll-tag-chip:hover { filter: brightness(1.15); }
.coll-tag-chip-input {
  background: transparent; border: 0; outline: 0; color: inherit;
  text-shadow: inherit; font: inherit; padding: 0; min-width: 4ch;
}
.coll-tag-filter-banner {
  display: inline-flex; align-items: center; gap: .5rem;
  padding: .35rem .75rem; border-radius: 999px;
  background: var(--bs-body-tertiary-bg);
  border: 1px solid var(--bs-border-color);
  font-size: .9rem;
}

/* Collections tag picker search dropdown. The rows use Bootstrap
   list-group classes; we just need to size + scroll them and give
   the keyboard-highlighted row a visible accent. */
.coll-tag-picker-search .coll-tag-picker-results {
  max-height: 16rem;
  overflow-y: auto;
}
.coll-tag-picker-search .list-group-item.active {
  background: var(--bs-body-tertiary-bg);
  color: var(--bs-body-color);
  border-color: var(--bs-border-color);
}

/* Inactive filter chip — faded background so the active filter stands
   out without recoloring it. */
.coll-tag-chip-dim { opacity: .55; }
.coll-tag-chip-dim:hover { opacity: 1; }

/* ---------------------------------------------------------------------------
   Themeparks crowd-level / reliability heat bands (see public/js/themeparks-scores.js)
   Band 1 = best / coolest, band 5 = worst / hottest. Backgrounds are translucent
   so they read on both light and dark themes; the text colour is overridden for
   dark mode where the light-mode ink would be too dark on a dark tint.

   .tp-bandc-N is the same ink WITHOUT the tint, for surfaces that carry the band
   as a coloured word on a plain background (the board's headline card) rather
   than as a filled box. Each colour is declared once and shared by both.
--------------------------------------------------------------------------- */
.tp-band-0 { background: transparent; }
.tp-band-0, .tp-bandc-0 { color: var(--bs-secondary-color); }
.tp-band-1 { background: rgba(25, 135, 84, .15);  }
.tp-band-2 { background: rgba(102, 187, 106, .16); }
.tp-band-3 { background: rgba(255, 193, 7, .20);   }
.tp-band-4 { background: rgba(253, 126, 20, .18);  }
.tp-band-5 { background: rgba(220, 53, 69, .16);   }
.tp-band-1, .tp-bandc-1 { color: #0f5132; }
.tp-band-2, .tp-bandc-2 { color: #2e7d32; }
.tp-band-3, .tp-bandc-3 { color: #997404; }
.tp-band-4, .tp-bandc-4 { color: #ab4a04; }
.tp-band-5, .tp-bandc-5 { color: #b02a37; }
[data-bs-theme="dark"] .tp-band-1, [data-bs-theme="dark"] .tp-bandc-1 { color: #75b798; }
[data-bs-theme="dark"] .tp-band-2, [data-bs-theme="dark"] .tp-bandc-2 { color: #a3cfbb; }
[data-bs-theme="dark"] .tp-band-3, [data-bs-theme="dark"] .tp-bandc-3 { color: #ffda6a; }
[data-bs-theme="dark"] .tp-band-4, [data-bs-theme="dark"] .tp-bandc-4 { color: #feb272; }
[data-bs-theme="dark"] .tp-band-5, [data-bs-theme="dark"] .tp-bandc-5 { color: #ea868f; }

/* ---------------------------------------------------------------------------
   Mouse-head watermark for the themeparks banners (the resort headline on the
   main page, the crowd page's band card, the dashboard's hero).

   Three circles in a mask rather than an image file: it stays crisp on a 4K wall
   panel at no download cost, and — being a mask over currentColor — it takes
   whatever ink the surface is already using. Every one of these banners colours
   its text from the crowd band, so the watermark shifts with the mood (green when
   it's quiet, red when it's packed) and needs no per-band rules of its own. It
   also means light and dark themes are handled for free.

   The host clips it, so a banner with rounded corners trims the shape like a
   watermark instead of a pasted-on sticker. Tunable per surface:
     --tp-mickey-op    opacity (default .07)
     --tp-mickey-size  mask-size — height percentages track the banner
     --tp-mickey-pos   mask-position
--------------------------------------------------------------------------- */
.tp-mickey { position: relative; overflow: hidden; }
/* The content needs a stacking order of its own or it paints under the mark. */
.tp-mickey > * { position: relative; z-index: 1; }
.tp-mickey::before {
    content: ''; position: absolute; inset: 0; z-index: 0; pointer-events: none;
    background-color: currentColor; opacity: var(--tp-mickey-op, .07);
    /* Both spellings: unprefixed mask is Safari 15.4+. */
    -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 300 269'%3E%3Ccircle cx='64' cy='64' r='59'/%3E%3Ccircle cx='236' cy='64' r='59'/%3E%3Ccircle cx='150' cy='164' r='100'/%3E%3C/svg%3E") var(--tp-mickey-pos, center) / var(--tp-mickey-size, auto 88%) no-repeat;
    mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 300 269'%3E%3Ccircle cx='64' cy='64' r='59'/%3E%3Ccircle cx='236' cy='64' r='59'/%3E%3Ccircle cx='150' cy='164' r='100'/%3E%3C/svg%3E") var(--tp-mickey-pos, center) / var(--tp-mickey-size, auto 88%) no-repeat;
}
