/* Skyline Transport Group site: shared base styles.
   Tokens, fonts and the lockup CSS come from the design system (styles.css).
   Everything here is structural: resets, keyframes, the scroll-reveal system
   and map pin styling. Architecture copied from the reference site.css. */
*, *::before, *::after { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body { font-family: var(--font); background: var(--surface); color: var(--ink); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }
button, a { font-family: inherit; }
a { color: var(--blue); text-decoration: none; }
a:hover { color: var(--blue-hover); }
#app { opacity: 1; }
:focus { outline: none; }
:focus-visible { box-shadow: var(--focus-ring); border-radius: 4px; }
::selection { background: var(--sky2); color: var(--ink); }
body::-webkit-scrollbar { width: 10px; }
body::-webkit-scrollbar-track { background: var(--bg); }
body::-webkit-scrollbar-thumb { background: #CBD5E1; border-radius: 10px; }
body::-webkit-scrollbar-thumb:hover { background: var(--slate); }

@keyframes marquee { from { transform: translateX(0); } to { transform: translateX(-50%); } }
@keyframes navFade { from { opacity: 0; } to { opacity: 1; } }
@keyframes navItem { from { opacity: 0; transform: translateX(14px); } to { opacity: 1; transform: translateX(0); } }
@keyframes navDrop { from { opacity: 0; transform: translate(-50%, -8px); } to { opacity: 1; transform: translate(-50%, 0); } }
@keyframes navDropItem { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } }
@keyframes navDropPanel { from { opacity: 0; transform: translateY(-10px) scale(0.98); } to { opacity: 1; transform: translateY(0) scale(1); } }

/* Route-survey plate: technical linework draws itself in, annotations follow. */
@keyframes zfDraw { to { stroke-dashoffset: 0; } }
@keyframes zfFade { to { opacity: 1; } }

/* Scroll reveal.
   Base state is VISIBLE, per the design system rule in tokens/motion.css:
   "never base-hidden without a failsafe." Elements only become hideable once
   SiteBase has PROVEN it can drive reveals (it probes IntersectionObserver with
   a live sentinel and sets .zf-reveal-ready on <html> only if the callback
   actually fires). If observers or scroll events are unavailable, the class is
   never set and the page simply renders without the entrance animation. */
[data-reveal] { opacity: 1; transform: none; }
.zf-reveal-ready [data-reveal] {
  opacity: 0;
  transition: opacity .85s var(--ease-out), transform .85s var(--ease-out);
}
.zf-reveal-ready [data-reveal] { transform: translateY(16px); }
.zf-reveal-ready [data-reveal="left"] { transform: translateX(-16px); }
.zf-reveal-ready [data-reveal="right"] { transform: translateX(16px); }
.zf-reveal-ready [data-reveal="scale"] { transform: scale(.99); }
[data-reveal].in { opacity: 1 !important; transform: none !important; }
[data-reveal].reveal-instant { transition: none !important; }
.zf-reveal-ready [data-delay="1"] { transition-delay: .05s; }
.zf-reveal-ready [data-delay="2"] { transition-delay: .10s; }
.zf-reveal-ready [data-delay="3"] { transition-delay: .15s; }
.zf-reveal-ready [data-delay="4"] { transition-delay: .20s; }
.zf-reveal-ready [data-delay="5"] { transition-delay: .25s; }
.zf-reveal-ready [data-delay="6"] { transition-delay: .30s; }
@media (prefers-reduced-motion: reduce) {
  [data-reveal], .zf-reveal-ready [data-reveal] {
    opacity: 1 !important; transform: none !important; transition: none !important;
  }
  .zf-tb-track { animation: none !important; }
  .zf-lane-pulse, .zf-origin-pulse { animation: none !important; }
}

/* Lane map: Leaflet surface tuned to the navy palette */
.zf-map { width: 100%; height: 100%; border-radius: var(--r-xl); background: var(--navy3); }
.zf-map .leaflet-control-attribution { background: rgba(9,15,22,.72); color: rgba(255,255,255,.45); font-size: 9px; padding: 2px 6px; border-radius: 4px; }
.zf-map .leaflet-control-attribution a { color: rgba(147,197,253,.75); }
.zf-map .leaflet-tile-pane { filter: brightness(2.1) contrast(1.22) saturate(.75) hue-rotate(-8deg); }
.zf-map-tint { position: absolute; inset: 0; pointer-events: none; z-index: 400;
  background: radial-gradient(ellipse 90% 80% at 50% 50%, transparent 55%, rgba(9,15,22,.55) 100%),
    linear-gradient(180deg, rgba(15,22,31,.34) 0%, rgba(15,22,31,.14) 60%, rgba(15,22,31,.44) 100%); }

/* Panel image slot: while empty, it sits in the upper band so the drop-target
   caption never collides with the copy over the scrim. Once a photo is dropped
   (image-slot sets data-filled) it goes full-bleed. */
.zf-panel-slot { position: absolute; inset: 0; width: 100%; height: 100%; color: rgba(191,219,254,.85); }
/* Unfilled: sit in the panel's flex flow above the copy, so the drop caption can
   never share space with the headline. Filled: back to full-bleed cover. */
.zf-panel-slot:not([data-filled]) { position: relative; inset: auto; width: auto; height: 190px; flex: none; }

.zf-pin { position: relative; pointer-events: none; }
.zf-pin-dot { position: absolute; top: 50%; left: 50%; width: 7px; height: 7px; background: var(--sky);
  border: 1.5px solid #FFFFFF; border-radius: 50%; transform: translate(-50%,-50%); box-shadow: 0 0 8px rgba(147,197,253,.6); }
.zf-pin-dot.origin { width: 12px; height: 12px; background: #FFFFFF; border: 2.5px solid var(--blue); box-shadow: 0 0 18px rgba(46,142,255,.8); }
.zf-origin-pulse { position: absolute; top: 50%; left: 50%; width: 12px; height: 12px; transform: translate(-50%,-50%);
  background: rgba(46,142,255,.5); border-radius: 50%; animation: zfPulse 2.2s ease-out infinite; }
@keyframes zfPulse { 0% { transform: translate(-50%,-50%) scale(1); opacity: .65; } 70%,100% { transform: translate(-50%,-50%) scale(3.4); opacity: 0; } }
.zf-pin-label { position: absolute; left: 13px; top: -10px; white-space: nowrap; font-family: var(--font); }
.zf-pin-name { font-size: 11.5px; font-weight: 700; color: #FFFFFF; letter-spacing: -.01em; text-shadow: 0 0 6px rgba(0,0,0,.95), 0 0 2px rgba(0,0,0,1); }
.zf-pin-sub { font-size: 9.5px; font-weight: 600; letter-spacing: .1em; color: rgba(255,255,255,.6); text-shadow: 0 0 6px rgba(0,0,0,.95); margin-top: 1px; }

/* Processing-step pulse: the live row's dot, so a long fetch does not look stalled. */
@keyframes zfTick { 0%,100% { opacity: 1; transform: scale(1); } 50% { opacity: .35; transform: scale(.72); } }
.zf-tick-pulse { animation: zfTick 1.1s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) { .zf-tick-pulse { animation: none; } }

/* ============================================================
   PRINT. Tool results get forwarded internally, so the printed
   sheet is a deliverable rather than an afterthought: page furniture
   and controls drop out, dark panels invert to paper, collapsed
   sections are forced open, and links show their href.
   ============================================================ */
@media print {
  @page { margin: 14mm 12mm; }
  html, body { background: #fff !important; }
  body { color: #0F161F !important; }

  /* Chrome, navigation and anything interactive has no meaning on paper. */
  nav, footer, .zf-nav, .zf-noprint,
  #contact, [data-print-hide] { display: none !important; }
  button { display: none !important; }
  /* Except the disclosure headers, which label the sections below them. */
  [aria-expanded] { display: flex !important; }
  [aria-expanded] > span:last-child { display: none !important; }

  /* Reveal system must never hide printed content. */
  [data-reveal] { opacity: 1 !important; transform: none !important; }

  /* Dark sections become paper. Navy ink on navy paper prints as a black box. */
  section, div { background-image: none !important; }
  section[style*="9, 15, 22"], section[style*="15, 22, 31"],
  div[style*="rgba(255,255,255,.04)"], div[style*="rgba(255,255,255,.03)"] {
    background: #fff !important; border-color: #DDE3E9 !important;
  }
  section[style*="9, 15, 22"] *, section[style*="15, 22, 31"] * { color: #0F161F !important; }

  /* Keep a finding and its explanation on one sheet. */
  h1, h2, h3 { break-after: avoid; page-break-after: avoid; }
  table, figure, .zf-keep { break-inside: avoid; page-break-inside: avoid; }
  section { break-inside: auto; }

  /* Sources are the point of the methodology section, so print the URL. */
  a[href^="http"]::after { content: " (" attr(href) ")"; font-size: 9pt; color: #4C5B6B; }

  /* Leaflet cannot be trusted to print its tiles; the panel data carries it. */
  .zf-map, .leaflet-container { display: none !important; }
  .zf-map-print-note { display: block !important; }
}
.zf-map-print-note { display: none; }


/* Tool phase swaps replace a short processing screen with a tall results page.
   Scroll anchoring tries to keep previously-visible content in place, which
   drags the viewport toward the footer during that swap. The tools scroll
   themselves to the top after commit, so anchoring has no useful job here. */
#app { overflow-anchor: none; }

/* ============================================================
   OPENING TITLE CARD (homepage, once per visitor)
   Markup and timeline live inline in index.html so the card paints
   on the first frame. Everything below is presentation only.
   ============================================================ */
html.zf-intro-lock, html.zf-intro-lock body { overflow: hidden; }

#zf-intro {
  position: fixed; inset: 0; z-index: 99999;
  background: #05090F;
  opacity: 1;
  transition: opacity 1s cubic-bezier(.16,1,.3,1) .12s;
}
#zf-intro.zf-out { opacity: 0; pointer-events: none; }
/* Exit: the type drifts up and the clip eases forward while the card fades,
   so the hero underneath (same video) reads as a continuous handoff. */
#zf-intro.zf-out #zf-intro-mark { opacity: 0; transform: translateY(-16px); filter: blur(6px); }
#zf-intro-mark { transition: opacity .7s cubic-bezier(.16,1,.3,1), transform .9s cubic-bezier(.16,1,.3,1), filter .7s cubic-bezier(.16,1,.3,1); }
#zf-intro.zf-out #zf-intro-vid { opacity: 0; transform: scale(1.045); transition: opacity 1s cubic-bezier(.16,1,.3,1), transform 1.2s cubic-bezier(.16,1,.3,1); }
#zf-intro.zf-out #zf-intro-skip { opacity: 0; }

#zf-intro-eyebrow {
  font-family: var(--font-mono);
  font-size: 11.5px; letter-spacing: .22em; text-transform: uppercase;
  color: #FFFFFF; text-align: center;
  opacity: 0; transform: translateY(8px);
  transition: opacity 1s cubic-bezier(.16,1,.3,1), transform 1s cubic-bezier(.16,1,.3,1);
}
#zf-intro.zf-in-eyebrow #zf-intro-eyebrow { opacity: 1; transform: none; }

#zf-intro-vid {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; display: block; border: none;
  opacity: 0; transform: scale(1.06);
  transition: opacity 1.4s cubic-bezier(.16,1,.3,1), transform 4s cubic-bezier(.16,1,.3,1);
}
#zf-intro.zf-in-video #zf-intro-vid { opacity: .38; transform: scale(1); }

/* Holds the wordmark legible over whatever frame the clip is on, and carries the
   card on its own when the video does not decode. */
#zf-intro-scrim {
  position: absolute; inset: 0; pointer-events: none;
  background:
    radial-gradient(ellipse 70% 60% at 50% 50%, rgba(5,9,15,.42), rgba(5,9,15,.88) 70%, #05090F 100%),
    linear-gradient(180deg, rgba(5,9,15,.55) 0%, rgba(5,9,15,.30) 45%, rgba(5,9,15,.75) 100%);
}

#zf-intro-mark {
  position: absolute; inset: 0; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 22px; padding: 24px;
}
#zf-intro-logo {
  width: min(420px, 62vw); display: block;
  opacity: 0; transform: translateY(10px);
  transition: opacity 1.1s cubic-bezier(.16,1,.3,1), transform 1.1s cubic-bezier(.16,1,.3,1);
}
#zf-intro.zf-in-logo #zf-intro-logo { opacity: 1; transform: none; }

/* The rule draws out from the centre, which is the one moment of motion. */

#zf-intro-line {
  font-family: var(--font-display);
  font-size: clamp(22px, 3.2vw, 34px); font-weight: 700; letter-spacing: -0.02em;
  color: #FFFFFF; text-align: center;
  opacity: 0; transition: opacity 1s cubic-bezier(.16,1,.3,1) .12s;
}
#zf-intro.zf-in-line #zf-intro-line { opacity: 1; }

#zf-intro-skip {
  position: absolute; right: 20px; bottom: 20px;
  font-family: var(--font); font-size: 11.5px; font-weight: 500;
  letter-spacing: .14em; text-transform: uppercase;
  color: rgba(255,255,255,.55); background: transparent;
  border: 1px solid rgba(255,255,255,.18); border-radius: 999px;
  padding: 8px 16px; cursor: pointer;
  opacity: 0; transition: opacity .8s ease .9s, color .16s ease, border-color .16s ease;
}
#zf-intro.zf-in-logo #zf-intro-skip { opacity: 1; }
#zf-intro-skip:hover { color: #fff; border-color: rgba(255,255,255,.45); }

@media print { #zf-intro { display: none !important; } }

/* iOS Safari zooms into any focused control whose font-size is under 16px.
   On touch devices force controls to 16px; desktop keeps the design sizes.
   !important is required to beat the inline control styles. */
@media (pointer: coarse) {
  input, select, textarea { font-size: 16px !important; }
}

h1,h2,h3,h4{font-family:var(--font-display)}
.zf-glass input::placeholder{color:rgba(255,255,255,.5)}
.zf-marquee{animation:zf-marquee 38s linear infinite}
@keyframes zf-marquee{from{transform:translateX(0)}to{transform:translateX(-50%)}}
@media (prefers-reduced-motion: reduce){.zf-marquee{animation:none}}
.zf-tms-dot{animation:zf-tms-pulse 1.9s ease-in-out infinite}
/* Ask Skyline launcher: fades out while the footer is on screen so it does not
   cover the legal lines or the Back to top pill. The class is toggled by a
   plain interval in zf-assist.js; never hide it while the panel is open
   ([data-open]) or the only close control disappears. */
/* `visibility` is deliberately NOT in the launcher's inline transition list
   (which names only opacity and transform), so it cannot be pinned at its start
   value where the transition clock does not advance. Without it the button
   stayed fully visible over the footer while pointer-events:none still applied,
   which is worse than either end state: a button that looks live but is dead. */
html.zf-at-footer .zf-assist-launcher:not([data-open]){opacity:0;transform:translateY(14px) scale(.96);pointer-events:none;visibility:hidden}
@keyframes zf-tms-pulse{0%,100%{opacity:1}50%{opacity:.25}}
@media (prefers-reduced-motion: reduce){.zf-tms-dot{animation:none}}





/* ============================================================
   PAGE TRANSITIONS
   Opacity-only cross-fade, on arrival and on departure, so clicking
   through the site does not flash white between documents.
   No transform: #app contains the fixed nav and the Ask Skyline
   launcher, and a transform on an ancestor makes it their containing
   block, which tears them off the viewport for the length of the
   animation on any page that loads already scrolled.
   Deliberately fill-mode NONE: before the animation starts (or in a
   context whose animation clock never runs) #app keeps its normal
   opacity, so a page can never be stranded invisible.
   ============================================================ */
#app{animation:zf-page-in .42s cubic-bezier(.16,1,.3,1)}
@keyframes zf-page-in{from{opacity:0}to{opacity:1}}
html.zf-leaving #app{opacity:0;transition:opacity .2s ease}
@media (prefers-reduced-motion:reduce){#app{animation:none}html.zf-leaving #app{opacity:1;transition:none}}

/* Scroll affordance: any element with data-zf-scrollhint gets a sticky cue as
   its last child (.zf-scrollcue). The cue is VISIBLE by default and hidden by
   .zf-at-end, which zf-submit.js adds once there is nothing more below. The
   logic is inverted on purpose: making visibility depend on a transition
   finishing pins the cue at opacity 0 wherever the animation clock does not
   advance, which is exactly how this affordance disappeared before. */
/* No negative horizontal margins: they had to equal the pane's padding, and a
   pane whose padding differs by breakpoint made the cue wider than the
   scroller's content box, forcing a horizontal scrollbar. Width is now the
   content box by definition. */
.zf-scrollcue{position:sticky;bottom:-2px;z-index:5;display:flex;justify-content:center;align-items:flex-end;width:100%;max-width:100%;box-sizing:border-box;height:56px;margin:-56px 0 0;pointer-events:none;background:linear-gradient(180deg,rgba(255,255,255,0) 0%,rgba(255,255,255,.92) 62%,var(--surface) 100%)}
.zf-scrollcue>span{display:inline-flex;align-items:center;gap:6px;margin-bottom:6px;font-family:var(--font-mono);font-size:10px;font-weight:600;letter-spacing:.14em;text-transform:uppercase;color:var(--ink2);background:var(--surface);border:1px solid var(--border);border-radius:999px;padding:5px 11px;box-shadow:var(--sh-sm)}
.zf-scrollcue--dark{background:linear-gradient(180deg,rgba(255,255,255,0) 0%,rgba(255,255,255,.9) 62%,#FFFFFF 100%)}
/* Nothing more below, or nothing to scroll at all: remove it outright rather
   than fading it, so no animation has to run for the panel to look right. */
.zf-at-end>.zf-scrollcue,html:not(.zf-hints-live) .zf-scrollcue{display:none}

/* ============================================================
   MOBILE CENTERING AND RHYTHM
   Headings already centre on narrow viewports (Heading in
   zf-site-base.js). This centres the BODY of a section to match, so
   a phone reads as one centred column instead of centred titles over
   left-ragged cards. Applied by cascade rather than by editing every
   component, then explicitly reverted for the places where centred
   text is wrong: long-form prose, tables, forms and the two embedded
   app panels (TMS demo, chat), which are their own compositions.
   ============================================================ */
@media (max-width: 719px){
  #app section p,#app section h3,#app section h4,#app section li,#app section figcaption,#app section blockquote{text-align:center}
  #app section ul,#app section ol{padding-left:0;list-style-position:inside}

  /* Chip and pill groups sit centred under a centred heading. */
  #app section [style*="flex-wrap: wrap"]{justify-content:center}

  /* --- reverted: composition or reading order depends on left alignment ---
     Every revert carries #app so it outweighs the centring rules above, which
     do too: without it, ID specificity beat source order and the long-form
     articles centred despite being listed here. */
  #app article p,#app article h2,#app article h3,#app article h4,#app article li,
  #app article figcaption,#app article blockquote,#app article td,#app article th,
  #app article ul,#app article ol{text-align:left}
  #app article ul,#app article ol{padding-left:22px;list-style-position:outside}
  #app table,#app table td,#app table th{text-align:left}
  #app form,#app form p,#app form li,#app label,#app input,#app textarea,#app select{text-align:left}
  #app [data-zf-scrollhint],#app [data-zf-scrollhint] p,#app [data-zf-scrollhint] li{text-align:left}
  /* The TMS replica is a picture of another application: its own alignment. */
  #app [data-zf-tms],#app [data-zf-tms] *{text-align:inherit}
  #app [data-zf-tms] [style*="flex-wrap: wrap"],#app article [style*="flex-wrap: wrap"],
  #app form [style*="flex-wrap: wrap"]{justify-content:flex-start}
}
/* Hide the sticky scroll cue while an autocomplete popup is open: the cue sits
   at the foot of the scroller and would show through beside the suggestions. */
body[data-zf-popup] .zf-scrollcue{opacity:0}
/* Horizontal filter strips (FAQ rail on narrow): scrollable by swipe, without
   a scrollbar eating a row of height. scrollbar-width covers Firefox and recent
   Chromium and Safari; this covers older WebKit, where the classic scrollbar
   would otherwise take a row out of the strip's own height. */
.zf-swipe::-webkit-scrollbar{height:0;width:0;display:none}


/* Crawlable copy of the page, written into #app so search engines and AI
   crawlers that do not run JavaScript still read the content. React replaces
   it on mount; until then it is kept off screen so it never flashes. */
.zf-pre{position:absolute!important;left:-10000px;top:0;width:1px;height:1px;overflow:hidden}
