html {
  scroll-behavior: smooth;
}

/* Custom Animations */
@keyframes float {
  0% {
    transform: translateY(0px);
  }

  50% {
    transform: translateY(-10px);
  }

  100% {
    transform: translateY(0px);
  }
}

.animate-float {
  animation: float 6s ease-in-out infinite;
}

.glass-panel {
  background: rgba(255, 255, 255, 0.85);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border-bottom: 1px solid rgba(0, 0, 0, 0.05);
}

.map-bg-overlay {
  background: linear-gradient(
    180deg,
    rgba(255, 255, 255, 0.6) 0%,
    rgba(248, 250, 252, 1) 100%
  );
}

/* Modal Transitions */
.modal {
  opacity: 0;
  visibility: hidden;
  transition: all 0.3s ease-in-out;
}

.modal.active {
  opacity: 1;
  visibility: visible;
}

.modal-content {
  transform: scale(0.95);
  transition: all 0.3s ease-in-out;
}

.modal.active .modal-content {
  transform: scale(1);
}

/* [MODAL-VIEWPORT-BOUND] feature-modal / tech-modal had no height cap, so tall injected
   content spilled off-screen and the footer + Close button became unreachable. Bound each
   card to the viewport and let its BODY (the middle section between the fixed header and
   footer) scroll internally — mirrors the search #data-modal pattern already in the markup
   (max-h-[90vh] + flex flex-col + inner overflow-y-auto). Scoped to these two modals so the
   other modals (which already set their own max-h/flex) are untouched. */
#feature-modal .modal-content,
#tech-modal .modal-content {
  max-height: 90vh;
  max-height: 90dvh;            /* dvh keeps it within the mobile viewport incl. the URL bar */
  display: flex;
  flex-direction: column;
}
#feature-modal .modal-content > div:nth-child(2),
#tech-modal .modal-content > div:nth-child(2) {
  overflow-y: auto;
  min-height: 0;                /* let the flex child shrink so it actually scrolls */
}

/* Hide scrollbar for clean look */
::-webkit-scrollbar {
  width: 8px;
}

::-webkit-scrollbar-track {
  background: #f1f5f9;
}

::-webkit-scrollbar-thumb {
  background: #cbd5e1;
  border-radius: 4px;
}

.lang-active {
  @apply bg-brand-primary text-white;
}

/* Quote Fade Effect */
.quote-fade {
  animation: fadeEffect 1s;
}

@keyframes fadeEffect {
  from {
    opacity: 0;
  }

  to {
    opacity: 1;
  }
}

/* Demo Animations for Modals */
@keyframes pulse-ring {
  0% {
    transform: scale(0.5);
    opacity: 0.8;
  }

  100% {
    transform: scale(1.5);
    opacity: 0;
  }
}

.demo-isochrone-circle {
  animation: pulse-ring 2s infinite ease-out;
}

@keyframes draw-path {
  0% {
    stroke-dashoffset: 100;
  }

  100% {
    stroke-dashoffset: 0;
  }
}

.demo-path {
  stroke-dasharray: 100;
  stroke-dashoffset: 100;
  animation: draw-path 2s infinite linear;
}

@keyframes scan-line {
  0% {
    top: 0;
  }

  100% {
    top: 100%;
  }
}

.demo-scan {
  animation: scan-line 2s infinite linear;
}

/* Heatmap Animation */
@keyframes heat-pulse {
  0%,
  100% {
    opacity: 0.5;
    transform: scale(1);
  }

  50% {
    opacity: 0.8;
    transform: scale(1.1);
  }
}

.heat-blob {
  animation: heat-pulse 3s infinite ease-in-out;
  filter: blur(15px);
}

input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
input:-webkit-autofill:active {
  transition: background-color 5000000s ease-in-out 0s;
}

.scene {
  position: relative;
  width: 650px;
  height: 650px;
  margin: auto;
  perspective: 1200px;
}


.carousel {
  width: 100%;
  height: 100%;
  position: absolute;
  /* transform: translateZ(-325px); */
  transform-style: preserve-3d;
  transition: transform 1s;
}

.carousel__cell {
  position: absolute;
  width: 100%;
  height: 500px;
  transition:
    transform 1s,
    opacity 1s;
}

/* Quote carousel scene: the 600px width overflows narrow screens. Cap it to
   the available width. The JS reads `carousel.offsetWidth` to compute the 3D
   cylinder radius, and the cells are width:100%, so the whole carousel scales
   down to fit automatically. Desktop (>=1024px) is unchanged (still 600px). */
#quotes {
  width: min(700px, 100%);
}

@media (max-width: 1023px) {
  /* Each card rotates in 3D and, magnified by the perspective, sweeps wider
     than the scene box — on small screens that spills past the viewport.
     Clip the horizontal sweep; keep overflow-y visible so the decorative
     quote mark that sits above each card is not cut off. */
  #quotes {
    overflow-x: clip;
    overflow-y: visible;
  }

  /* [QUOTE-MARK-SIZE] The decorative double-quote is 80x80px hanging 40px down
     into the card (-top-10 w-20 h-20). Once the card padding shrinks on mobile
     the glyph lands on the first line of text. Halve it and keep the part that
     hangs into the card inside the card's top padding. (lucide swaps the <i>
     for an <svg> carrying the same classes — match both, as the cell's only
     direct icon child.) */
  .carousel__cell > svg,
  .carousel__cell > i[data-lucide="quote"] {
    width: 44px;
    height: 44px;
    top: -26px;
    left: 20px;
  }
}

@media (max-width: 640px) {
  /* On phones the card is much narrower than 600px, so the quote text would
     otherwise spill above and below the fixed-height card. Shrink the type
     and padding so it fits inside; sides tighter than top/bottom to give the
     text every horizontal pixel the small screen has. */
  .carousel__cell {
    padding: 1.25rem 0.75rem;
  }
  .carousel__cell blockquote {
    font-size: 1rem;
    line-height: 1.5rem;
  }
  .carousel__cell .mt-8 {
    margin-top: 1rem;
  }

  /* [QUOTE-COMPACT] The fixed 500px scene + doubled my-8 margins (outer wrapper
     AND #quotes) + the gap-10 before the indicator row add up to ~690px — on a
     phone the card and the six dots could never share one screen. Shrink the
     card, trim the stacked margins and the gap so quote + dots fit together.
     The 3D radius is computed from the carousel WIDTH (js reads offsetWidth),
     so a height change is safe. */
  #quotes {
    height: 400px;
    margin-top: 0.5rem;
    margin-bottom: 0;
    /* Pull the blue frame closer to the screen edges than the section's px-4
       (16px) allows: 8px per side. The parent flex wrapper centres the wider
       scene; the JS reads offsetWidth after CSS applies, so the 3D radius
       follows automatically. */
    width: min(600px, 100vw - 1rem);
  }
  .carousel__cell {
    height: 400px;
  }
  /* When a long quote overflows the shorter card, only the BLOCKQUOTE scrolls;
     the source-link row and author row below it stay pinned and always visible.
     (Scrolling on the cell itself is wrong twice over: its clip edge crops the
     quote mark that hangs above the card top, and it scrolls the footer rows
     away with the text.) The container fills the card so the flexbox can hand
     the leftover height to the blockquote; min-height:0 lets it shrink below
     its content and become scrollable; the footer rows keep their height. */
  .carousel__cell .quote-container {
    height: 100%;
    min-height: 0;
    /* Anchors the absolutely-positioned .quote-scrollbar indicator. */
    position: relative;
    /* Insurance: if content ever still exceeds the card, spill DOWN only.
       Plain `center` overflows both ends, pushing the quote's top out of the
       card where it can never be scrolled back. Ignored (falls back to the
       Tailwind justify-center) on engines without `safe`. */
    justify-content: safe center;
  }
  .carousel__cell blockquote {
    overflow-y: auto;
    /* Never slice a line of text: flex must not shrink the quote to an
       arbitrary height (a flex-derived height is what cut the bottom line in
       half) — instead it is capped at a WHOLE number of lines. The real cap
       is set per cell by [QUOTE-LINE-SNAP] in index.html, which measures the
       leftover height above each quote's own footer (source link + author
       rows wrap to very different heights per quote and language). The
       9-line value below is only the no-JS fallback. */
    flex: 0 0 auto;
    max-height: calc(9 * 1.5rem);
    /* Breathing room between the justified text and the scroll indicator so
       the bar never sits on the letters. */
    padding-right: 0.5rem;
    /* Scrolling the quote must not chain into scrolling the whole page. */
    overscroll-behavior: contain;
    /* A vertical drag on the quote is a SCROLL, never a carousel swipe or a
       browser gesture — say so explicitly for every touch engine. */
    touch-action: pan-y;
    /* Legacy iOS: promote to an async momentum scroller. */
    -webkit-overflow-scrolling: touch;
  }
  /* [QUOTE-SCROLLBAR] The visible scroll indicator is a CUSTOM element built
     by index.html, NOT the native bar: iOS (Safari AND Chrome — both WebKit)
     ignores ::-webkit-scrollbar styling entirely and only flashes an overlay
     bar while scrolling, so no CSS can make a native bar permanently visible
     on iPhone. Hide the native bar on every engine and let .quote-scrollbar
     (positioned/sized by [QUOTE-LINE-SNAP]'s JS) be the one identical
     affordance everywhere. */
  .carousel__cell blockquote {
    scrollbar-width: none;              /* Firefox */
  }
  .carousel__cell blockquote::-webkit-scrollbar {
    display: none;                      /* Chrome / Edge / Android WebView */
  }
  .quote-scrollbar {
    position: absolute;
    width: 4px;
    border-radius: 4px;
    background: #e2e8f0;
    /* Pure indicator — must never steal the touch from the text below it. */
    pointer-events: none;
  }
  .quote-scrollbar__thumb {
    position: absolute;
    left: 0;
    width: 100%;
    border-radius: 4px;
    background: #64748b;
  }
  .carousel__cell .quote-container > div {
    flex-shrink: 0;
  }
  /* The wrapper between the scene and the dots (no id/class hook — match it as
     #quotes' parent). If :has() is unsupported the gap just stays a bit wider. */
  div:has(> #quotes) {
    gap: 1rem;
    margin-top: 1rem;
    margin-bottom: 1rem;
  }
}

/* ============================================================
   Landing-page image & button fixes
   ============================================================ */

/* The hero/concept <img>s use h-full + w-full which, combined with Tailwind
   preflight's `max-width:100%`, force the picture into the box and squash it
   horizontally. object-fit keeps the real aspect ratio. */

/* Wide skyline illustration: fill the hero and let the sides spill past the
   screen edge (crop) instead of being compressed. */
#hero img {
  object-fit: cover;
  object-position: center;
}

/* Near-square concept diagram: never distort it, but keep it fully visible —
   its labels sit around the whole edge, so cropping would cut them off. */
#concept img[data-i18n-media="concept"] {
  width: 100%;
  height: 100%;
  object-fit: contain;
  object-position: center;
}

/* Location quick-links: in a no-wrap row the 4 buttons crammed their labels
   into mismatched shapes. On small screens lay them out as a single column of
   full-width "location chips" — each chip's height fits its own label, with a
   leading map-pin icon and centred text. Desktop keeps the single pill row. */
#locations-container {
  align-items: stretch;
}

@media (max-width: 1023px) {
  #locations-container {
    display: flex;
    flex-direction: column;
    gap: 0.625rem;
    width: min(24rem, 100vw - 2rem);    /* keep the column within the screen */
    margin-inline: auto;                /* centred */
  }
  #locations-container button {
    width: 100%;
    border-radius: 1rem;
    padding: 0.85rem 1rem;
  }
}

/* Professional finish for the location quick-link buttons: a subtle sky
   gradient, a soft layered shadow, refined hover/active feedback and an
   accessible focus ring — instead of the flat solid-blue block. */
#locations-container button {
  background-image: linear-gradient(180deg, #0ea5e9 0%, #0284c7 55%, #0369a1 100%);
  border: 1px solid rgba(3, 105, 161, 0.5);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.18),
    0 1px 2px rgba(2, 132, 199, 0.25),
    0 8px 18px -6px rgba(2, 132, 199, 0.45);
  letter-spacing: 0.005em;
  justify-content: center;   /* centre the icon + label group horizontally */
  text-align: center;        /* centre wrapped label lines */
  transition: transform 0.18s ease, box-shadow 0.18s ease, filter 0.18s ease;
}

/* Map-pin icon that leads each label (mobile chips only) */
#locations-container button svg {
  width: 1.05rem;
  height: 1.05rem;
  flex-shrink: 0;
  opacity: 0.9;
}

/* Desktop keeps the compact, single-line pill row — adding the pin there
   would push the long label onto a second line. The pin is a mobile touch. */
@media (min-width: 1024px) {
  #locations-container button svg {
    display: none;
  }
}

#locations-container button:hover {
  transform: translateY(-2px);
  filter: saturate(1.06) brightness(1.04);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.22),
    0 2px 6px rgba(2, 132, 199, 0.3),
    0 14px 28px -8px rgba(2, 132, 199, 0.55);
}

#locations-container button:active {
  transform: translateY(0);
  filter: brightness(0.97);
  box-shadow:
    inset 0 2px 4px rgba(3, 64, 122, 0.35),
    0 1px 2px rgba(2, 132, 199, 0.2);
}

#locations-container button:focus-visible {
  outline: none;
  box-shadow:
    0 0 0 3px rgba(14, 165, 233, 0.45),
    0 8px 18px -6px rgba(2, 132, 199, 0.45);
}

@media (prefers-reduced-motion: reduce) {
  #locations-container button {
    transition: box-shadow 0.18s ease, filter 0.18s ease;
  }
  #locations-container button:hover,
  #locations-container button:active {
    transform: none;
  }
}

/* [MOBILE-HIDE-LOCATIONS] Hide the "try these locations" quick-link row on mobile. Uses this
   component's own mobile breakpoint (max-width:1023px — the same one that restyles it into a
   column above). An #id selector placed later in source order beats both the earlier column
   rule and the inline Tailwind `flex`, so no !important is needed. */
@media (max-width: 1023px) {
  #locations-container {
    display: none;
  }
}

/* Search modal: the card is sized for desktop (px-32 = 256px padding and a
   text-6xl title), so on phones it overflows the screen — the close button
   falls off the right edge in English and the CJK title is far too large in
   Chinese. Scale the whole card down to fit the viewport. */
@media (max-width: 767px) {
  #search-container > div > div {        /* the white modal card */
    width: calc(100vw - 2rem);
    max-width: 28rem;
    margin-inline: auto;
    padding: 3.5rem 1.25rem 1.75rem;     /* top leaves room for the close button */
  }
  #search-container .absolute {          /* close-button wrapper */
    top: 0.75rem;
    right: 0.75rem;
  }
  #search-container [data-i18n="iframeSearchModalTitle"] {
    font-size: 1.875rem;
    line-height: 2.25rem;
  }
  #search-container [data-i18n="iframeSearchModalText"] {
    font-size: 1rem;
    line-height: 1.4rem;
  }
}

/* Top-left nav logos: the images used `h-full` (height:100%) inside anchors
   that have a width but no defined height — a circular height reference. Some
   iOS/Android browsers resolve it by stretching the logo to the 80px bar
   height while capping its width, squashing the wide logo. Size by width with
   auto height instead, so the aspect ratio is identical on every browser.
   (>=1800px the [NAV-LOGOS-50] block below re-sizes them to the 50px line.) */
#navigation-bar a img {
  width: 100%;
  height: auto;
}

/* [NAV-LOGO-2X] The Walkin'HK nav logo ships as the 244x100 asset but renders
   in the old logo_50 box (122x50) — a 2x-resolution image at 50% size, so the
   mark is crisp on high-DPR screens. Both axes must stay auto with caps, never
   pinned: its anchor is the only flex item on the left cluster without a fixed
   width, so on narrow viewports (iPhone SE, 375px) it gets squeezed below the
   logo's 122px and the preflight max-width:100% clamps the width — a pinned
   height then squashes the aspect ratio. max-height gives the same 122x50 when
   space allows and scales both axes down together when it doesn't. */
#navigation-bar #nav-walkinhk-logo {
  width: auto;
  height: auto;
  max-height: 50px;
  max-width: 100%;
}

/* [NAV-PARTNER-LOGOS] Three partner logos right of the desktop language
   switcher — all-or-nothing. At 50px tall they need ~378px, but the EN menu
   leaves only ~6px spare in the 1280px (max-w-7xl) nav row, so they can
   never share it: on >=1800px windows the nav row (and only the nav row)
   widens to 1780px — measured empirically, 1760px is the narrowest row
   where the EN menu keeps one-line labels next to the 50px partner logos
   AND the left cluster aligned to 50px by [NAV-LOGOS-50] (the explore
   button's scale-80 transform makes rect arithmetic lie), plus
   scrollbar/zoom slack — and the logos show at exactly 50px. Below 1800px
   they hide entirely (they remain in the partners section down-page) and
   the nav row is exactly its pre-logo self; anything in between squeezes
   the menu into wrapped labels. Img sizing = same axis-cap rule as
   [NAV-LOGO-2X]: both axes auto with caps, never a pinned height. Styled
   here rather than with Tailwind utilities so nothing depends on classes
   existing in the prebuilt tailwind-output.css. */
#nav-partner-logos {
  gap: 12px;
  margin-left: 12px;
}
#nav-partner-logos a {
  display: flex;
  align-items: center;
}
/* Both ids so this outranks the generic `#navigation-bar a img` width:100%
   rule above: inside a shrink-to-fit anchor, width:100% resolves to the
   image's intrinsic width and squashes it against the 50px height cap. */
#navigation-bar #nav-partner-logos img {
  width: auto;
  height: auto;
  max-height: 50px;
  max-width: 100%;
}
@media (max-width: 1799.98px) {
  #nav-partner-logos {
    display: none;
  }
}
@media (min-width: 1800px) {
  /* nav row only — #mobile-menu, the other direct child, is xl:hidden and
     its w-full dropdown must not inherit a max-width */
  #navigation-bar > div:first-child {
    max-width: 1780px;
  }
  /* [NAV-LOGOS-50] With the wide row active, the left-cluster logos leave
     their compact width-based sizes (which fit the 1280px row beside the EN
     menu) and align to the same 50px height line as the partner logos.
     Axis caps as usual — except the CSDI svg, which has a viewBox but no
     intrinsic size, so auto axes collapse it to 0x0: pin its height (a
     viewBox letterboxes under a clamp, it cannot distort). The pt-2 optical
     offsets are zeroed so all marks share one centreline. */
  #nav-left-logos a {
    width: auto;
    padding-top: 0;
  }
  #nav-left-logos a > div {
    padding-top: 0;
  }
  #navigation-bar #nav-left-logos img {
    width: auto;
    height: auto;
    max-height: 50px;
    max-width: 100%;
  }
  #navigation-bar #nav-left-logos img[src$=".svg"] {
    height: 50px;
  }
}

/* [ACK-LOGOS] Acknowledgement-card logos: the imgs used `h-full` inside the
   fixed h-20 (80px) box, and the preflight max-width:100% clamps the width of
   the wide marks (polyu.png is 729x142, rils-logo-full 1855x580) while the
   height stayed pinned at 80px — a squashed aspect ratio (same failure mode
   as the nav logos: never pin one axis). Both axes auto with caps: a logo
   fills the 80px line when its shape allows and scales down proportionally
   when the card is too narrow. Styled here, not with Tailwind utilities, so
   nothing depends on classes existing in the prebuilt tailwind-output.css. */
.landsd_logo,
.polyu_logo,
.rils_logo,
.jcdisi_logo {
  width: auto;
  height: auto;
  max-height: 100%;
  max-width: 100%;
}

/* [ACK-WIDE] Wider acknowledgement row (id hook on the max-w-7xl container).
   At the default 1280px the four cards leave only ~226px of content width:
   the longest org name wraps to 3 lines and the aspect-kept wide logos
   render short. 1536px gives each card ~290px of content, so every org name
   fits 2 lines and the logos sit close to the shared 80px line — the white
   cells end up with more or less the same height. Below 1536px the row is
   simply viewport-limited (it fills the screen minus its own px padding);
   the id outranks the .max-w-7xl utility. */
#ack-container {
  max-width: 1536px;
}

/* [NO-PTR] The landing must never trigger browser pull-to-refresh: a swipe
   down at page top (very easy to hit on phones, and while the LiveApp iframe
   overlay is open the landing body is STILL the root scroller behind it)
   would reload the page and drop the app state. overscroll-behavior-y:none
   kills PTR and the rubber-band chain on Chrome/Android and iOS 16+ without
   affecting normal scrolling. Inner scrollers (modals, quote carousel) keep
   their own overscroll rules on top of this. */
html,
body {
  overscroll-behavior-y: none;
}

/* [SCENARIO-MENU] Small picker under the desktop nav "Scenarios" button:
   two story-map choices, labels filled per-language by JS on open. Lives in
   the desktop-only nav wrapper, so no mobile hiding is needed. Styled here
   (not with Tailwind utilities) so it doesn't depend on classes existing in
   the prebuilt tailwind-output.css. */
#scenario-menu-wrap {
  position: relative;
}
#scenario-menu {
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translateX(-50%);
  z-index: 60;
  min-width: 240px;
  max-width: min(340px, 90vw);
  padding: 6px;
  background: #fff;
  border: 1px solid #e2e8f0;
  border-radius: 12px;
  box-shadow: 0 12px 32px rgba(15, 23, 42, 0.16);
}
#scenario-menu .scenario-menu-item {
  display: block;
  width: 100%;
  padding: 10px 14px;
  border-radius: 8px;
  text-align: left;
  font-size: 14px;
  font-weight: 500;
  color: #475569;
  cursor: pointer;
  /* long English titles wrap inside the panel instead of stretching it */
  white-space: normal;
}
#scenario-menu .scenario-menu-item:hover {
  background: #f1f5f9;
  color: #0f172a;
}

/* [HERO-TITLE-SIZE] Shrink two hero lines by exactly one Tailwind text step.
   Done here (main.css loads after tailwind-output.css and these selectors
   out-specify the utility classes) rather than by editing the class names,
   which would need the Tailwind CLI re-run to emit the new utilities.
   Only #heroTitle2Top (the Chinese-layout top line) and the heroTitle1 <h1>
   change — #heroTitle2Bot (English layout) keeps its original size. */
#heroTitle2Top {
  font-size: 1.25rem;      /* was text-2xl (1.5rem) -> text-xl */
  line-height: 1.75rem;    /* text-2xl's fixed 2rem would gape at 20px */
}
h1[data-i18n="heroTitle1"] {
  font-size: 2.25rem;      /* was text-5xl (3rem) -> text-4xl */
  /* line-height stays on the element's leading-tight (1.25, relative) */
}
@media (min-width: 768px) {
  #heroTitle2Top {
    font-size: 1.875rem;   /* was md:text-4xl (2.25rem) -> text-3xl */
    line-height: 2.25rem;
  }
  h1[data-i18n="heroTitle1"] {
    font-size: 3.75rem;    /* was md:text-7xl (4.5rem) -> text-6xl */
  }
}
