/* Let's Set Sail — Disney Adventure campaign landing
   Palette + type per DCL-DA Brand Application Trade Toolkit (30 Jul 26):
   p.11 primary palette, p.10 Noto Sans TC for Traditional Chinese,
   default weight 500 / bold 900, tracking 120 (= 0.12em) on display type.

   The page is ONE surface. The key visual sits at the top and dissolves into the
   water gradient that carries the tagline, the reel and the buttons — so nothing
   reads as a band stacked on another band. Everything is sized off --u, a single
   unit that answers to whichever axis is scarcer, which is what keeps the whole
   composition inside one screen instead of clipping the part that matters. */

:root {
  --navy:    #1D386D;
  --sky:     #1268B3;
  --gold:    #C1922E;
  --seafoam: #6CBCC4;
  --deep:    #0B1E4A;
  --white:   #FFFFFF;
  --wake:    #374397;   /* sampled from the KV's water at the cut line — the seam colour.
                           Re-sampled 2026-08-26 at the same point in the KV (the wake streak,
                           x=800 of 1100) after the hero stopped being converted CMYK→sRGB by
                           ImageMagick's naive formula and started going through the ICC
                           profiles. Same pixel, true colour: the old #1539DA was an artefact. */
  --rim: 2px;
  /* The flyer's iridescent card edge, sampled across its top rim at 200dpi.
     Pink → lilac → sky → warm → gold, which is the "pink to yellow" the client
     pointed at; it is the flyer's own colour, not an approximation of it. */
  --iris: linear-gradient(118deg,
    #E07CC4 0%, #C389CB 15%, #8FC4E8 34%, #A9B6C4 52%,
    #DCB187 72%, #E8C77A 87%, #D99C8C 100%);
  /* The Disney Adventure wordmark's own metal, sampled from the KV and stopped
     at the toolkit gold — every stop clears 4.5:1 on the water behind it. */
  --gold-leaf: linear-gradient(180deg, #F8F8B8 0%, #E8D583 28%, #D2BF59 56%, #C1922E 100%);

  /* the one unit — follows the narrower axis, so the layout scales whole */
  --u: min(4.1vw, 1.72svh);
  --pad: clamp(14px, 4.6vw, 26px);
  --sw: 100vw;            /* width of the column the page occupies */
}

* { box-sizing: border-box; margin: 0; padding: 0; }

html { -webkit-text-size-adjust: 100%; }

body {
  font-family: "Noto Sans TC", "PingFang HK", "Microsoft JhengHei", system-ui, sans-serif;
  font-weight: 500;
  color: var(--white);
  background: var(--deep);
  height: 100svh;
  overflow: hidden;
  -webkit-tap-highlight-color: transparent;
}

.stage {
  position: relative;
  isolation: isolate;
  height: 100svh;
  display: flex;
  flex-direction: column;
  padding-bottom: env(safe-area-inset-bottom);
  /* deep water under everything; the KV fades into its top */
  /* The page IS the sea the ship is sailing on: it starts at the exact colour of
     the KV's water where the image ends, then sinks. Nothing to blend, no seam. */
  background:
    radial-gradient(140% 46% at 50% 30%, rgba(64, 132, 232, 0.42) 0%, transparent 70%),
    linear-gradient(180deg,
      var(--wake) 0%, var(--wake) 30%,
      #2A387F 46%, #182860 68%, var(--deep) 100%);
}

/* ── The key visual, as ground ─────────────────────────────────────────────
   The file is the 16:9 campaign KV whole (see build-assets.sh). The band keeps its
   exact ratio so `cover` never crops anything away, and the last few percent fade
   into the water gradient, which is set to the very colour the image ends on.

   ⚠ 97% is now a tight number, not a comfortable one: the KV's own Ships' Registry /
   ©Disney line runs to 96.8% of the file. Lower the fade and it dissolves a legal
   notice — the N-021 failure, in a new place. check.mjs pins it. */
.sea {
  position: relative;
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
  flex: 0 0 auto;
  width: 100%;
  aspect-ratio: 1100 / 620;
  /* The whole composition — wordmark, characters, ship, DCL lockup and the legal
     line — fits in a band the page can afford, on every viewport down to 320x568.
     The cap is a ceiling for tall screens, not the thing that decides the crop. */
  max-height: 40svh;
  overflow: hidden;
}
.sea picture img {
  /* absolute, so the band's aspect-ratio decides the crop — left in flow the
     image's own intrinsic height wins and the composition drifts */
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Bottom-anchored, and that is the whole point: any shortfall is taken off the
     TOP, where there is ~14% of empty sky above the Disney wordmark to give, and
     never off the bottom, where the ship is. The ship's waterline now always
     lands on the band's bottom edge. */
  object-position: center 100%;
  -webkit-user-drag: none;
  pointer-events: none;
  /* Fades below everything the KV carries: the hull, the DCL lockup (ends 93.4%)
     and the Ships' Registry line (ends 96.8%). Three percent of margin, no more. */
  -webkit-mask-image: linear-gradient(180deg, #000 0%, #000 97%, transparent 100%);
          mask-image: linear-gradient(180deg, #000 0%, #000 97%, transparent 100%);
}

.sea__dissolve { display: none; }

/* ── Tagline: white type on the water, no plate ────────────────────────── */
.tagline {
  flex: 0 0 auto;
  user-select: none;
  -webkit-user-select: none;
  /* was -0.9u: the tagline was pulled up OVER the image, which only worked while
     the bottom of the band was faded out. With the ship now solid to the edge it
     sits clear of the water instead. */
  margin-top: calc(var(--u) * 0.25);
  padding: 0 calc(var(--u) * 0.8);
  /* 10 glyphs at 0.12em tracking: the width cap is what actually decides the size,
     so the line can be as large as it fits and can never be clipped. */
  font-size: min(calc(var(--u) * 2.3), calc(var(--sw) * 0.077));
  font-weight: 900;
  letter-spacing: 0.12em;   /* toolkit p.10: tracking 120 */
  text-indent: 0.12em;      /* re-centres against the trailing letter-space */
  line-height: 1.25;
  text-align: center;
  white-space: nowrap;
  color: var(--white);
  text-shadow: 0 calc(var(--u) * 0.14) calc(var(--u) * 0.9) rgba(4, 18, 48, 0.65);
}
/* a single gold rule — the toolkit's own gold, echoing the wordmark above it */
.tagline::after {
  content: "";
  display: block;
  width: calc(var(--u) * 6.4);
  height: 2px;
  margin: calc(var(--u) * 0.5) auto 0;
  border-radius: 2px;
  background: linear-gradient(90deg, transparent, var(--gold) 22%, #E7C36A 50%, var(--gold) 78%, transparent);
}

/* ── Reel: Swiper, with the neighbouring slides showing ───────────────────
   Swiper owns the track's layout and gestures. The peek on either side is the
   affordance — a slide that is visibly cut off at the edge says "there is more"
   in a way a row of dots never does — and the neighbours are dimmed and set back
   so the centre still reads as the subject rather than one of three. */
.reel {
  flex: 1 1 auto;
  /* A floor for the photo. Without it the reel is pure slack and collapses to a
     74px letterbox on a 360x600 in-app browser — the picture stops being a
     picture. The room comes from the three-across buttons above. */
  min-height: calc(var(--sw) * 0.40);
  display: flex;
  flex-direction: column;
  /* The hidden dots used to supply the gap under the reel; this replaces it.
     Where the photo is already at its 3:2 ceiling this costs nothing — the
     space comes out of the slack, not out of the picture. */
  padding: calc(var(--u) * 0.5) 0 calc(var(--u) * 1.35);
  overflow: hidden;
}
.reel__track {
  flex: 1 1 auto;
  min-height: 0;
  width: 100%;
  overflow: hidden;
}
.reel__track .swiper-wrapper { height: 100%; align-items: center; }

/* The slide is a photo with its caption UNDER it — client instruction 2026-08-27
   (Shui). The caption used to ride the bottom of the picture on a dark scrim, and
   that scrim sat exactly on top of the ©Disney line burned into the corner of the
   Duffy shot. Below the photo, the supplied image is never covered by anything. */
.shot {
  position: relative;
  height: 100%;
  display: flex;
  flex-direction: column;
  min-height: 0;
  opacity: 0.42;
  transform: scale(0.9);
  transition: opacity 0.42s cubic-bezier(0.22, 1, 0.36, 1),
              transform 0.42s cubic-bezier(0.22, 1, 0.36, 1);
}
.shot.swiper-slide-active { opacity: 1; transform: none; }

/* the rimmed card is now the picture alone, so the caption is outside the frame
   rather than inside it — the cap that used to sit on the slide sits here */
.shot__frame {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  padding: var(--rim);
  /* A Swiper slide is --sw / 1.22 wide; 3:2 of that is --sw * 0.547, plus the rim.
     0.55 not 0.547: the rim insets the image, so this lands at 1.47:1 — just inside
     the 1.45 floor check.mjs enforces against a landscape press shot drifting
     portrait. It is all the height the photo can legally take; the rest stays as
     air on tall screens. */
  max-height: calc(var(--sw) * 0.55 + var(--rim) * 2);
  border-radius: calc(14px + var(--rim));
  background: var(--iris);
  box-shadow: 0 calc(var(--u) * 0.5) calc(var(--u) * 1.6) rgba(6, 26, 58, 0.45);
}

.shot img {
  width: 100%;
  height: 100%;
  min-height: calc(var(--u) * 3.6);
  /* the frame above carries the ceiling; object-fit crops when space is short */
  object-fit: cover;
  border-radius: 14px;
  -webkit-user-drag: none;
  user-select: none;
}
/* Under the picture, on the water — no plate, no scrim, nothing over the artwork. */
.shot figcaption {
  position: relative;
  flex: 0 0 auto;
  padding: calc(var(--u) * 0.62) calc(var(--u) * 0.6) 0;
  font-size: calc(var(--u) * 1.15);
  font-weight: 900;
  letter-spacing: 0.04em;
  line-height: 1.25;
  text-align: center;
  color: var(--white);
  white-space: nowrap;
  text-shadow: 0 calc(var(--u) * 0.1) calc(var(--u) * 0.6) rgba(4, 18, 48, 0.55);
}
/* The ©Disney notice as live text, in the corner of the picture where the artwork
   itself puts it. The supplied photo carries it burned in, but the slide box is wider
   than the photo's 3:2 on a short viewport and `cover` takes the shortfall off the top
   and bottom — so on an iPhone SE or an in-app browser the burned-in one is simply gone
   (measured 2026-08-27: 1.99:1, 2.39:1, 2.69:1). Biasing the crop downward would save
   it only by cutting the heads off the other two slides, so it is rendered instead.

   This is a credit mark, not the description. The description is the caption, and the
   caption is below the picture — that is the change the client asked for, and a 6-glyph
   mark in a corner does not undo it. No plate, no scrim: a shadow carries it over any
   photo, so nothing but the glyphs sits on the artwork. */
.shot__frame small {
  position: absolute;
  right: calc(var(--u) * 0.7);
  bottom: calc(var(--u) * 0.5);
  font-family: system-ui, -apple-system, "Helvetica Neue", Arial, sans-serif;
  font-size: max(8px, calc(var(--u) * 0.66));
  font-weight: 500;
  line-height: 1;
  letter-spacing: 0.01em;
  color: rgba(255, 255, 255, 0.92);
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.8), 0 0 8px rgba(0, 0, 0, 0.45);
  pointer-events: none;
}

.dots {
  /* Hidden for now at the client's request, to give the reel its height back.
     Kept in the DOM: the buttons are the keyboard and screen-reader route to
     each slide, and re-enabling is this one line. */
  flex: 0 0 auto;
  display: flex;
  justify-content: center;
  align-items: center;
  gap: calc(var(--u) * 0.55);
  padding: calc(var(--u) * 0.55) 0 calc(var(--u) * 0.95);
}
/* …but hidden for now, at the client's request, to give the reel its height
   back. Delete this rule to bring them back. */
.dots { display: none; }
/* three dots for three real slides, whatever the loop is doing behind them */
.dots button {
  border: 0;
  padding: 0;
  cursor: pointer;
  width: calc(var(--u) * 0.55);
  height: calc(var(--u) * 0.55);
  min-width: 5px;
  min-height: 5px;
  margin: 0;
  border-radius: 50%;
  background: #FFFFFF;
  opacity: 0.38;
  transition: opacity 0.25s ease, transform 0.25s cubic-bezier(0.22, 1, 0.36, 1);
}
.dots button[aria-current="true"] {
  opacity: 1;
  transform: scale(1.35);
}

/* ── Booking: the reason the page exists ───────────────────────────────── */
.book {
  flex: 0 0 auto;
  padding: 0 var(--pad);
}
.book__cta {
  /* One line, capped against the column width so it can never wrap or clip, with
     a floor so it stays legible on the smallest screens. Tracking sits between the
     tagline's display 0.12em and the legal line's 0 — a three-step ladder, not
     four arbitrary values. */
  /* The width cap is derived, not guessed: 16 glyphs (8 + 7 + the ！) at 1.1em
     each, plus the caesura and the optical indent, is 18.46em — so 18.8 is the
     largest size that always fits on one line, with a little margin. */
  font-size: max(15px, min(calc(var(--u) * 1.34), calc((var(--sw) - var(--pad) * 2) / 18.8)));
  line-height: 1.28;
  letter-spacing: 0.1em;
  text-align: center;
  white-space: nowrap;
  /* the trailing ！ carries half a glyph of air; pull the line back onto its
     optical centre rather than its metric one */
  text-indent: 0.52em;
  padding-bottom: calc(var(--u) * 1.05);
  /* the solid fallback is the mid of the ramp, so a browser without background-clip
     still gets legible gold rather than transparent text */
  color: #E8D583;
}
@supports (background-clip: text) or (-webkit-background-clip: text) {
  .book__cta {
    background: var(--gold-leaf);
    -webkit-background-clip: text;
            background-clip: text;
    color: transparent;
    -webkit-text-fill-color: transparent;
  }
}
/* Two clauses, one line: who to contact, then what to do. The instruction sits at
   the toolkit's default weight and the action at its bold — so the line has an
   emphasis without a second colour, a second family, or a second line. */
.book__who {
  /* No opacity here: on a parent using background-clip:text, an opaque child
     forms its own compositing context, the clip does not reach it, and the
     clause renders as invisible transparent text. Weight alone carries it. */
  font-weight: 500;
}
.book__do {
  font-weight: 900;
  margin-left: 0.34em;   /* the caesura, held open optically */
}

.agents {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: calc(var(--u) * 0.7);
  /* Optical sizing. Seven logos with aspect ratios from 2.1 to 4.3 cannot share a
     height — matched heights make a wide wordmark (klook, Trip.com) tower over a
     stacked bilingual lockup (Cathay, Wing On). They are matched on WIDTH instead,
     with --k nudging each for the ink it actually carries. */
  --logo: calc(var(--u) * 6.9);
}

.agent {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  /* 44px is the tap-target floor; below it the reel gives up height, not the hand */
  min-height: max(44px, calc(var(--logo) * 0.42 + var(--u) * 0.55));
  padding: calc(var(--u) * 0.22) calc(var(--u) * 1.3);
  /* Enamel under a light source. Two background layers do the work: a soft dome
     of specular light falling from above, over a body that cools as it descends.
     The body has to cool for the highlight to exist at all — a white highlight on
     a white fill is nothing, which is why the first pass read flat. */
  background:
    radial-gradient(132% 88% at 50% -26%, #FFFFFF 0%, rgba(255, 255, 255, 0) 58%),
    linear-gradient(180deg, #FDFEFF 0%, #EFF4FC 44%, #D6E1F2 100%);
  border-radius: 999px;
  text-decoration: none;
  box-shadow:
    /* contact — keeps it sitting on the water rather than floating above it */
    0 1px calc(var(--u) * 0.18) rgba(3, 14, 40, 0.48),
    /* lift */
    0 calc(var(--u) * 0.5) calc(var(--u) * 0.9) rgba(3, 14, 40, 0.42),
    /* ambient, tinted with the sea so the pills read as lit by the water */
    0 calc(var(--u) * 1.35) calc(var(--u) * 2.5) rgba(8, 36, 112, 0.55),
    /* the lit top edge and the gold seat at the base — the two lines that make
       it an object rather than a shape */
    inset 0 2px 0 #FFFFFF,
    inset 0 -2.5px 0 rgba(193, 146, 46, 0.88),
    /* the interior curls away at the bottom, so the face reads domed */
    inset 0 calc(var(--u) * -0.5) calc(var(--u) * 0.72) calc(var(--u) * -0.46) rgba(23, 48, 100, 0.34);
  transition:
    transform 0.22s cubic-bezier(0.22, 1, 0.36, 1),
    box-shadow 0.22s cubic-bezier(0.22, 1, 0.36, 1),
    background 0.22s ease;
}
.agent--last {
  grid-column: 1 / -1;
  justify-self: center;
  width: calc((100% - var(--u) * 0.7) / 2);
}

/* ── Short screens: three across ───────────────────────────────────────────
   Two across is the design; it holds wherever the page has the height for it —
   an iPhone 15 at 852, a Pro Max at 932. It stops holding on a short viewport,
   where four rows of buttons leave the carousel a 74px letterbox (client note,
   2026-08-25). Three across turns four rows into three and hands the difference
   to the photo.

   The switch is on HEIGHT, not width, because height is what actually runs out:
   an iPhone 15 in an in-app browser is the same 393px wide but only ~700 tall,
   and it needs the compact layout its full-screen self does not. 740px puts
   every in-app viewport and every small phone on three, and leaves the
   full-screen flagships on two. */
@media (max-height: 740px) {
  .agents {
    grid-template-columns: repeat(3, 1fr);
    gap: calc(var(--u) * 0.55) calc(var(--u) * 0.5);
    /* a third-width button is narrower; the shared width unit comes down with it
       and the per-logo --k nudges still apply on top */
    --logo: calc(var(--u) * 5.2);
  }
  .agent {
    /* tighter, but the 44px tap floor above is untouched */
    min-height: max(44px, calc(var(--logo) * 0.42 + var(--u) * 0.35));
    padding: calc(var(--u) * 0.16) calc(var(--u) * 0.7);
  }
  .agent--last {
    /* the odd one out sits on the centre column of the three */
    grid-column: 2;
    justify-self: stretch;
    width: auto;
  }
}
.agent img {
  width: auto;
  height: auto;
  max-width: calc(var(--logo) * var(--k, 1));
  max-height: calc(var(--logo) * 0.42);
  object-fit: contain;
}
/* --k: from each logo's trimmed aspect ratio and ink coverage, nudged by eye.
   tools/build-assets.sh prints the measurements it came from. */
.agent[data-agent="cathay_holidays"]  img { --k: 0.86; }  /* 2-line lockup, thin type */
.agent[data-agent="goldjoy"]          img { --k: 0.95; }
.agent[data-agent="kkday"]            img { --k: 0.80; }  /* heavy round strokes read big */
.agent[data-agent="klook"]            img { --k: 0.97; }
.agent[data-agent="travel_resources"] img { --k: 1.02; }
.agent[data-agent="trip_com"]         img { --k: 1.00; }
.agent[data-agent="wing_on"]          img { --k: 0.90; }

.agent:active {
  /* pressed into the water: the light source loses the top of the dome, the body
     inverts, the lift collapses and the gold seat comes up to the rim */
  transform: translateY(2px) scale(0.984);
  background:
    radial-gradient(120% 72% at 50% 122%, rgba(255, 255, 255, 0.92) 0%, rgba(255, 255, 255, 0) 56%),
    linear-gradient(180deg, #CFDBEE 0%, #F2F6FC 72%);
  box-shadow:
    0 1px calc(var(--u) * 0.12) rgba(3, 14, 40, 0.5),
    inset 0 2.5px 0 rgba(193, 146, 46, 0.8),
    inset 0 -2px 0 rgba(255, 255, 255, 0.95),
    inset 0 calc(var(--u) * 0.45) calc(var(--u) * 0.7) calc(var(--u) * -0.4) rgba(23, 48, 100, 0.36);
}
.agent:focus-visible { outline: 3px solid var(--seafoam); outline-offset: 3px; }
@media (hover: hover) {
  .agent:hover {
    transform: translateY(-4px) scale(1.014);
    /* lifting toward the light: the dome brightens and rises, the gold seat goes
       to full strength, and the sea-tinted ambient spreads as it climbs away */
    background:
      radial-gradient(136% 94% at 50% -30%, #FFFFFF 0%, rgba(255, 255, 255, 0) 60%),
      linear-gradient(180deg, #FFFFFF 0%, #F3F7FD 44%, #DCE6F5 100%);
    box-shadow:
      0 1px calc(var(--u) * 0.18) rgba(3, 14, 40, 0.4),
      0 calc(var(--u) * 0.8) calc(var(--u) * 1.3) rgba(3, 14, 40, 0.42),
      0 calc(var(--u) * 2) calc(var(--u) * 3.4) rgba(8, 36, 112, 0.62),
      inset 0 2px 0 #FFFFFF,
      inset 0 -2.5px 0 var(--gold),
      inset 0 calc(var(--u) * -0.5) calc(var(--u) * 0.72) calc(var(--u) * -0.46) rgba(23, 48, 100, 0.3);
  }
}

/* ── Legal ─────────────────────────────────────────────────────────────── */
.legal {
  flex: 0 0 auto;
  display: flex;
  justify-content: space-between;
  gap: 10px;
  padding: calc(var(--u) * 0.6) var(--pad) calc(var(--u) * 0.55);
  font-family: system-ui, -apple-system, "Helvetica Neue", Arial, sans-serif;
  font-size: calc(var(--u) * 0.72);
  letter-spacing: 0;
  color: rgba(255, 255, 255, 0.62);
}

/* Wide screens: mobile-first page, held in a phone-width column. --u is re-based
   on that column's width so the composition matches what a phone gets. */
@media (min-width: 560px) {
  body { background: #071835; display: flex; justify-content: center; }
  .stage {
    width: min(440px, 47svh);
    /* the preview column stays phone-shaped instead of becoming a tall ribbon */
    height: min(100svh, calc(min(440px, 47svh) * 2.16));
    box-shadow: 0 0 80px rgba(0, 0, 0, 0.6);
    --sw: min(440px, 47svh);
    --u: min(calc(min(440px, 47svh) * 0.041), 1.72svh);
  }
}

/* ── First load ────────────────────────────────────────────────────────────
   One orchestrated arrival, not a micro-interaction per element: the KV settles
   while the content rises under it. Every animated element's resting state is
   its normal one, so if the animation never runs (headless render, hidden tab)
   the page is simply there. */
@keyframes settle { from { transform: scale(1.045); } to { transform: scale(1); } }
@keyframes rise   { from { opacity: 0; transform: translateY(calc(var(--u) * 1.1)); }
                    to   { opacity: 1; transform: none; } }
@keyframes fade   { from { opacity: 0; } to { opacity: 1; } }

.sea img   { animation: settle 1100ms cubic-bezier(0.16, 1, 0.3, 1) both; }
.tagline   { animation: rise 620ms cubic-bezier(0.16, 1, 0.3, 1) 120ms both; }
.reel      { animation: rise 620ms cubic-bezier(0.16, 1, 0.3, 1) 230ms both; }
.book__cta { animation: fade 520ms ease-out 340ms both; }
.agent     { animation: rise 520ms cubic-bezier(0.16, 1, 0.3, 1) both; }
.agent:nth-child(1) { animation-delay: 400ms; }
.agent:nth-child(2) { animation-delay: 445ms; }
.agent:nth-child(3) { animation-delay: 490ms; }
.agent:nth-child(4) { animation-delay: 535ms; }
.agent:nth-child(5) { animation-delay: 580ms; }
.agent:nth-child(6) { animation-delay: 625ms; }
.agent:nth-child(7) { animation-delay: 670ms; }
.legal     { animation: fade 500ms ease-out 700ms both; }

@media (prefers-reduced-motion: reduce) {
  .agent, .dots button { transition: none; }
  .shot { transition: none; }
  .reel__track { scroll-behavior: auto; }
  .sea img, .tagline, .reel, .book__cta, .agent, .legal { animation: none; }
}
