/* ============================================================
   Duoradar — scroll-driven fog study
   ============================================================ */

:root{
  /* The palette is absolute: this page is painted --ink on --fog no matter
     what the browser or OS prefers. "only light" opts the page out of the
     browser's light/dark adaptation (Chromium's forced darkening, UA form
     and scrollbar recolouring), so the colours never shift underneath. */
  color-scheme:only light;

  --white:#fff;
  --ink:#0b0e14;
  --ink-2:#141a24;

  /* The whiteout target. The fog does not resolve to pure #fff: it settles on
     this, and the whole site below is painted in the same value, so the hero
     hands off without a visible step and the page reads as one field. */
  --fog:#e4e7ec;
  --fog-deep:#dcE0e6;

  --line:rgba(255,255,255,.58);
  --line-soft:rgba(255,255,255,.26);
  --grey:rgba(255,255,255,.66);

  --line-dark:rgba(11,14,20,.30);
  --grey-dark:rgba(11,14,20,.58);

  --display:"Anton",Impact,"Haettenschweiler","Arial Narrow Bold",sans-serif;
  --script:"Parisienne","Snell Roundhand","Apple Chancery","Segoe Script",cursive;
  --ui:"Inter",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;

  --ease:cubic-bezier(.16,1,.3,1);
  --gut:clamp(14px,2.2vw,34px);
}

*,*::before,*::after{box-sizing:border-box}

html{
  -webkit-text-size-adjust:100%;
  scroll-behavior:auto; /* Lenis owns scrolling */
}

body{
  margin:0;
  background:var(--ink);
  color:var(--white);
  font-family:var(--ui);
  font-weight:400;
  font-size:16px;
  line-height:1.55;
  overflow-x:hidden;
  -webkit-font-smoothing:antialiased;
}

img{max-width:100%;display:block}
a{color:inherit;text-decoration:none}
h1,h2,h3,p{margin:0}

/* Lenis */
html.lenis,html.lenis body{height:auto}
.lenis.lenis-smooth{scroll-behavior:auto !important}
.lenis.lenis-stopped{overflow:hidden}

.u-container{
  width:100%;
  max-width:1500px;
  margin-inline:auto;
  padding-inline:var(--gut);
}

/* ============================================================
   NAV  ·  fixed, transparent rectangles, white/grey outline
   ============================================================ */

.nav{
  position:fixed;
  inset:0 0 auto 0;
  z-index:100;
  display:flex;
  align-items:center;
  justify-content:space-between;
  gap:16px;
  padding:clamp(12px,1.6vw,22px) var(--gut);
  mix-blend-mode:normal;
}

/* the rectangles */
.nav__options{display:flex;align-items:center;gap:clamp(6px,.7vw,12px)}

.nav__opt{
  display:inline-flex;
  align-items:center;
  justify-content:center;
  height:clamp(32px,2.6vw,40px);
  padding-inline:clamp(12px,1.3vw,22px);
  background:transparent;
  border:1px solid var(--line);
  color:var(--white);
  font-family:var(--ui);
  font-size:clamp(10px,.72vw,12px);
  font-weight:500;
  letter-spacing:.16em;
  text-transform:uppercase;
  white-space:nowrap;
  backdrop-filter:blur(3px);
  -webkit-backdrop-filter:blur(3px);
  transition:background-color .45s var(--ease),
             border-color .45s var(--ease),
             color .45s var(--ease);
}

.nav__opt:hover,
.nav__opt:focus-visible{
  background:var(--white);
  border-color:var(--white);
  color:var(--ink);
  outline:none;
}

/* softer grey outline variant */
.nav__opt--solid{border-color:var(--line-soft)}
.nav__opt--solid:hover{border-color:var(--white)}

/* inverts when a light section is behind it */
.nav[data-theme="dark"] .nav__opt{border-color:var(--line-dark);color:var(--ink)}
.nav[data-theme="dark"] .nav__opt:hover,
.nav[data-theme="dark"] .nav__opt:focus-visible{
  background:var(--ink);border-color:var(--ink);color:var(--white);
}
.nav[data-theme="dark"] .nav__opt--solid{border-color:var(--line-dark)}

/* ============================================================
   1. HERO  ·  tall runway + sticky 100svh stage
   ============================================================ */

/* The runway is the scroll budget for the whole fog sequence. Every sheet
   travels a fixed fraction of the STAGE height (100svh, see [data-travel] in
   main.js), so this number alone sets the pace: the fog covers the same
   ground over 330-100 = 230svh of scrolling instead of 220-100 = 120svh,
   roughly half the speed, with no change to the layers themselves. */
.hero{
  position:relative;
  height:330vh;    /* fallback for browsers without svh */
  height:330svh;   /* the scroll runway */
  background:var(--ink);
}

.hero__stage{
  position:sticky;
  top:0;
  height:100vh;
  height:100svh;
  overflow:hidden;
  isolation:isolate;
}

.hero__bg{
  position:absolute;
  inset:0;
  z-index:0;
  overflow:hidden;
  background:#0b0e14;
}

.hero__media,
.hero__video{
  position:absolute;
  inset:0;
  width:100%;
  height:100%;
  object-fit:cover;
  object-position:50% 42%;
  transform:translateZ(0);
}

/* the video only becomes visible once it has actually decoded */
.hero__video{opacity:0;transition:opacity .9s var(--ease)}
.hero__video.is-ready{opacity:1}

.hero__scrim{
  position:absolute;
  inset:0;
  z-index:1;
  background:rgba(9,13,20,.26);
}

/* HUD: the wordmark only, centred. z-index 2 puts it UNDER the fog sheets
   (3/4/5) so the fog rolls in front of the headline rather than behind it. */
.hero__hud{
  position:absolute;
  inset:0;
  z-index:2;
  display:grid;
  grid-template-rows:1fr;
  align-items:center;
  padding:clamp(84px,10vh,130px) var(--gut) clamp(22px,4vh,44px);
  pointer-events:none;
}

.hero__title-wrap{
  grid-row:2;
  display:flex;
  flex-direction:column;
  align-items:center;
  justify-content:center;
  will-change:transform;
}

/* the wordmark */
.hero__title{
  font-family:var(--display);
  font-weight:400;
  font-size:clamp(52px,15.5vw,250px);
  line-height:.9;
  letter-spacing:.005em;
  text-transform:uppercase;
  white-space:nowrap;
  text-align:center;
  color:var(--white);
  will-change:filter,transform;
  /* JS fits the width precisely; this is the pre-fit fallback */
}

/* "guide" — a second, contrasting voice under the wordmark. This is a
   connected script rather than a print handwriting face: it reads as a
   signature against Anton's flat condensed caps instead of competing
   with them letter for letter. */
.hero__script{
  font-family:var(--script);
  font-weight:500;
  font-size:clamp(34px,7vw,116px);
  line-height:.9;
  letter-spacing:0;
  text-align:center;
  color:var(--white);
  margin-top:clamp(-10px,-.7vw,-2px);
  transform:rotate(-5deg);
  transform-origin:50% 100%;
  will-change:filter,transform;
}

/* per-letter reveal, split by JS */
.hero__title .ch{
  display:inline-block;
  will-change:transform,opacity;
}


/* ----------  FOG SHEETS  ----------
   Each sheet starts parked one full viewport below the stage, then scrubs up.
   Height is 260svh and travel stays inside [-160svh, 0], which does two things:
   the sheet covers the viewport continuously for the whole second half of its
   travel, and the band left in view at the end lands in the dense middle of the
   texture rather than in its feathered top/bottom. Shorter sheets dip in
   luminance right as they arrive.

   The whiteout is the same mechanism with a denser source, so the sky going
   white is made of fog texture rather than a flat gradient. It travels the
   furthest (-1.04) and arrives last, after the three thinner sheets have
   already established the parallax.                                          */

.fog{
  position:absolute;
  left:0;
  top:0;
  width:100%;
  height:260vh;
  height:260svh;
  z-index:var(--z,2);
  pointer-events:none;
  transform:translate3d(0,100vh,0);
  transform:translate3d(0,100svh,0);
  will-change:transform;
}

.fog img{
  position:absolute;
  top:0;
  left:0;
  width:100%;
  height:100%;
  object-fit:cover;
  object-position:50% 50%;
  transform:translateZ(0);
}

/* The whiteout needs to read as an unbroken wall of fog by the time it lands,
   so it is scaled slightly past the frame and held fully opaque. Without this
   its feathered top edge still lets the sky through at the end of the runway. */
/* object-position 50% 45% keeps the visible band centred on the sheet's
   fully-opaque middle. At 50% 55% the bottom feather started entering the
   frame early and the top edge arrived as a hard horizontal seam. */
.fog--whiteout img{
  transform:translateZ(0) scale(1.35);
  object-position:50% 45%;
}

/* ============================================================
   2. REST OF SITE
   ============================================================ */

.site{
  position:relative;
  z-index:4;
  background:var(--fog);
  color:var(--ink);
  padding-top:clamp(60px,12vh,140px);
}

.intro{
  padding:clamp(40px,9vh,110px) var(--gut) clamp(70px,14vh,150px);
  max-width:1500px;
  margin-inline:auto;
}

.intro__eyebrow{
  font-size:clamp(10px,.72vw,12px);
  font-weight:500;
  letter-spacing:.26em;
  text-transform:uppercase;
  color:var(--grey-dark);
  margin-bottom:clamp(20px,3.4vh,42px);
}

.intro__lede{
  font-family:var(--ui);
  font-weight:300;
  font-size:clamp(24px,3.6vw,58px);
  line-height:1.1;
  letter-spacing:-.02em;
  max-width:19ch;
  text-wrap:balance;
}

.intro__cols{
  display:grid;
  grid-template-columns:repeat(auto-fit,minmax(260px,1fr));
  gap:clamp(22px,3vw,64px);
  margin-top:clamp(46px,8vh,96px);
  padding-top:clamp(24px,4vh,44px);
  border-top:1px solid var(--line-dark);
  max-width:1100px;
}
.intro__cols p{
  font-size:clamp(14px,1.05vw,17px);
  color:rgba(11,14,20,.66);
  line-height:1.68;
}

/* About page: the overview is its own full-viewport text moment. The lede
   takes the left half and the two notes the right half, vertically centred,
   so the text uses the whole screen instead of the narrow left column that
   used to sit above the galleries. */
.intro--screen{
  min-height:100vh;
  min-height:100svh;
  display:grid;
  grid-template-columns:minmax(0,1.1fr) minmax(0,.9fr);
  grid-template-rows:auto 1fr;
  column-gap:clamp(40px,7vw,140px);
  row-gap:clamp(30px,6vh,72px);
  align-content:center;
  align-items:center;
  padding-block:clamp(100px,18vh,190px) clamp(80px,14vh,150px);
}
.intro--screen .intro__eyebrow{
  grid-column:1 / -1;
  margin-bottom:0;
}
.intro--screen .intro__lede{
  grid-column:1;
  grid-row:2;
  max-width:none;
  font-size:clamp(30px,4.6vw,78px);
}
.intro--screen .intro__cols{
  grid-column:2;
  grid-row:2;
  display:flex;
  flex-direction:column;
  gap:clamp(24px,3.4vh,42px);
  margin-top:0;
  padding-top:0;
  padding-left:clamp(30px,4vw,70px);
  border-top:0;
  border-left:1px solid var(--line-dark);
  max-width:none;
}
.intro--screen .intro__cols p{
  font-size:clamp(15px,1.15vw,18px);
}


/* ---------- the composite compare ----------
   Both composites sit in the SAME box at the same scale: eu1 is the base
   layer, eu2 is stacked exactly on top of it and clipped to the left of the
   divider. Because the two share a projection, dragging the divider uncovers
   the change instead of sliding one picture over another.

   --pos (0...1, written by JS) drives both the clip and the handle, so they
   can never disagree. clip-path percentages resolve against the element's own
   border box, and both images are inset:0 in the same frame, so the reveal is
   a true wipe with no scaling or offset between the layers. */
.gallery{
  /* hairline separators run between the sections (the quote already carried
     them) — the top padding keeps the compare box off the line */
  padding:clamp(40px,8vh,110px) var(--gut) clamp(120px,24vh,260px);
  border-top:1px solid var(--line-dark);
}

.compare{
  --pos:.5;
  position:relative;
  margin:0;
  width:100%;
  max-width:1100px;
  margin-inline:auto;
  aspect-ratio:1138/951;          /* the source ratio, so nothing is cropped */
  overflow:hidden;
  background:#0d1620;             /* the composite's own sea colour */
  border:1px solid var(--line-dark);
  cursor:ew-resize;
  touch-action:none;              /* a horizontal drag must not scroll the page */
  -webkit-user-select:none;
  user-select:none;
}

.compare img{
  position:absolute;
  inset:0;
  width:100%;
  height:100%;
  object-fit:contain;
  object-position:50% 50%;
  pointer-events:none;           /* the figure owns the drag, not the image */
}

/* the revealed layer: everything left of the divider */
.compare__top{
  clip-path:inset(0 calc(100% - var(--pos) * 100%) 0 0);
}

/* the divider line and knob. pointer-events:none so the whole figure stays one
   drag surface, but it keeps tabindex so the control is still keyboard-usable
   via the focus ring below. */
.compare__handle{
  position:absolute;
  top:0;
  bottom:0;
  left:calc(var(--pos) * 100%);
  z-index:3;
  width:2px;
  margin-left:-1px;
  background:var(--fog);
  pointer-events:none;
}

.compare__grip{
  --grip:clamp(40px,4.6vw,58px);
  position:absolute;
  top:50%;
  left:50%;
  width:var(--grip);
  height:var(--grip);
  margin:calc(var(--grip) / -2) 0 0 calc(var(--grip) / -2);
  border-radius:50%;
  background:var(--fog);
  display:grid;
  place-items:center;
  box-shadow:0 2px 18px rgba(11,14,20,.28);
}
.compare__grip svg{
  width:calc(var(--grip) * .40);
  height:calc(var(--grip) * .40);
  fill:none;
  stroke:var(--ink);
  stroke-width:2;
  stroke-linecap:round;
  stroke-linejoin:round;
}
.compare__handle:focus-visible{outline:none}
.compare__handle:focus-visible .compare__grip{
  outline:2px solid var(--ink);
  outline-offset:4px;
}

/* ---------- stat breather ----------
   One line between the composite compare and the per-station gallery: the
   extra space the two galleries breathe in, spent on the numbers the app's
   own About menu also carries. "Products" is styled as one of the nav's
   bordered tabs and leads to the products page. */
.stats{
  min-height:100vh;
  min-height:100svh;
  display:flex;
  flex-direction:column;
  align-items:center;
  justify-content:center;
  padding:clamp(60px,12vh,150px) var(--gut);
  border-top:1px solid var(--line-dark);
  text-align:center;
}

.stats__line{
  font-weight:300;
  font-size:clamp(24px,3.5vw,58px);
  line-height:1.25;
  letter-spacing:-.015em;
  max-width:24ch;
  margin-inline:auto;
  text-wrap:balance;
}

.stats__num{font-weight:500}

/* plain text, not a tab: the link only differs by colour and a hairline
   underline, so the sentence stays one voice */
.stats__link{
  color:var(--grey-dark);
  text-decoration:underline;
  text-decoration-thickness:1px;
  text-underline-offset:.16em;
  transition:color .45s var(--ease);
}
.stats__link:hover,
.stats__link:focus-visible{
  color:var(--ink);
  outline:none;
}

/* ---------- per-station flick gallery ----------
   One row of frames in the white-desert.com "card-flick" pattern: exactly one
   frame is expanded, the rest collapse to dimmed strips, and SCROLL is the
   only thing that moves the selection while the section is pinned (the
   pointer deliberately does nothing here — no hover expansion).

   The geometry is pure CSS — flex-grow 1 vs 4 with a transition — so the
   row keeps working when JS never loads: :focus-visible expands a strip,
   and .is-active (managed by main.js) nominates the frame the scroll has
   chosen. Tap and keyboard focus can jump the selection; scroll re-syncs
   it on the next tick.

   The sweeps are square with a dark sea of their own (rgb 11 14 19 — near
   enough to --ink to be the same colour), so the images are CONTAINED on
   the card field and the card is painted --ink: the expanded frame shows
   the whole circle with no seam and no crop. Every image renders at the
   same fixed size — a collapsed strip is a narrow window onto it, showing
   the sweep's centre slice — so moving the selection never rescales
   anything; the card simply widens around a stationary image. */
.flick{
  min-height:100vh;
  min-height:100svh;
  display:flex;
  flex-direction:column;
  justify-content:center;
  padding:clamp(60px,12vh,140px) var(--gut) clamp(90px,16vh,200px);
  border-top:1px solid var(--line-dark);
}

.flick__eyebrow{
  font-size:clamp(10px,.72vw,12px);
  font-weight:500;
  letter-spacing:.26em;
  text-transform:uppercase;
  color:var(--grey-dark);
  margin-bottom:clamp(20px,3.4vh,42px);
}

.flick__lede{
  font-family:var(--ui);
  font-weight:300;
  font-size:clamp(24px,3.6vw,58px);
  line-height:1.1;
  letter-spacing:-.02em;
  max-width:19ch;
  text-wrap:balance;
}

.flick__row{
  display:flex;
  gap:clamp(4px,.45vw,8px);
  height:clamp(420px,62vh,720px);
  margin-top:clamp(36px,7vh,80px);
}

.flick__item{
  position:relative;
  flex:1 1 0%;
  min-width:0;
  padding:0;
  border:1px solid var(--line-dark);
  background:var(--ink);          /* the sweeps' own background colour */
  overflow:hidden;
  cursor:pointer;
  font:inherit;
  color:inherit;
  -webkit-tap-highlight-color:transparent;
  transition:flex-grow 1.4s var(--ease);
}
.flick__item:focus-visible,
.flick__item.is-active{flex-grow:4}
.flick__item:focus-visible{
  outline:2px solid var(--ink);
  outline-offset:3px;
}

.flick__item img{
  position:absolute;
  top:50%;
  left:50%;
  /* Every frame renders at the SAME size: an explicit square of 92% of the
     row height (both axes spelled out — width:auto would shrink-to-fit the
     narrow strip and turn the slice back into a miniature). A collapsed
     strip is just a narrow window onto that constant-size image — its
     centre slice — so nothing magnifies or jumps when the selection moves;
     expanding a frame only widens the window around it. The translate is
     static centring and is never transitioned. */
  width:clamp(386px,57vh,662px);
  height:clamp(386px,57vh,662px);
  max-width:none;                 /* the global img{max-width:100%} would cap
                                     the square at the strip's width */
  transform:translate(-50%,-50%);
  object-fit:contain;
  filter:blur(.5px) brightness(.78) saturate(.9);
  transition:filter 1.1s var(--ease);
  pointer-events:none;
}
.flick__item:focus-visible img,
.flick__item.is-active img{
  filter:none;
}

/* gradient scrim so the index reads over the bright sweeps */
.flick__scrim{
  position:absolute;
  inset:0;
  background:linear-gradient(180deg,
    rgba(11,14,20,.5),
    rgba(11,14,20,0) 40%);
  opacity:0;
  transition:opacity 1.1s var(--ease);
  pointer-events:none;
}

.flick__caption{
  position:absolute;
  inset:0;
  display:flex;
  flex-direction:column;
  align-items:flex-start;
  justify-content:space-between;
  padding:clamp(14px,1.8vw,28px);
  opacity:0;
  transition:opacity 1.1s var(--ease);
  pointer-events:none;
  text-align:left;
}
.flick__index{
  font-family:var(--display);
  font-size:clamp(24px,2.6vw,44px);
  line-height:1;
  color:var(--white);
}

.flick__item:focus-visible .flick__scrim,
.flick__item.is-active .flick__scrim,
.flick__item:focus-visible .flick__caption,
.flick__item.is-active .flick__caption{opacity:1}

/* ---------- country composites ----------
   The DE frames show the same browser-side merge the intro describes, one
   country at a time: Germany, composited from its own stations' sweeps. All
   three frames are the exact same size and sit stacked in ONE box, aligned
   1:1 on the map, so the crossfade between them matches feature for feature
   — all stations -> raw data -> final product, the data appearing to be
   processed in place. The runway gives every stage its own time; main.js
   only picks the step, the fades themselves are CSS. */
.country{
  padding:clamp(100px,20vh,240px) var(--gut) clamp(100px,18vh,200px);
  border-top:1px solid var(--line-dark);
}

.country__eyebrow{
  font-size:clamp(10px,.72vw,12px);
  font-weight:500;
  letter-spacing:.26em;
  text-transform:uppercase;
  color:var(--grey-dark);
  margin-bottom:clamp(20px,3.4vh,42px);
}

.country__lede{
  font-family:var(--ui);
  font-weight:300;
  font-size:clamp(24px,3.6vw,58px);
  line-height:1.1;
  letter-spacing:-.02em;
  max-width:19ch;
  text-wrap:balance;
}

/* Scroll budget for the three states: first fade at 25%, second at 55%, so
   the finished product keeps the whole tail of the runway to itself. */
.match__runway{
  position:relative;
  height:280vh;
  height:280svh;
  margin-top:clamp(44px,9vh,110px);
}

/* One box, one fixed size, every layer inset 0 — the frames are pixel
   aligned, so what changes under the fade is only the data. The width is
   capped by viewport HEIGHT as well as pixels (so the whole box always fits
   on screen), and the sticky top centres the box vertical at every size,
   holding a minimum clearance for the fixed nav. --mh is the box height,
   derived from the same width expression. */
.match__frame{
  --mw:min(100vw - 2 * var(--gut),1080px,calc(80svh * 1.196));
  position:sticky;
  top:max(70px,calc((100vh - var(--mw) / 1.196) / 2));
  top:max(70px,calc((100svh - var(--mw) / 1.196) / 2));
  width:var(--mw);
  margin-inline:auto;
  aspect-ratio:1138/951;          /* the source ratio, so nothing is cropped */
  overflow:hidden;
  background:#2c353c;             /* the composite's own sea colour */
  border:1px solid var(--line-dark);
  box-shadow:0 30px 70px -30px rgba(11,14,20,.45);
}

.match__layer{
  position:absolute;
  inset:0;
  width:100%;
  height:100%;
  object-fit:contain;
  object-position:50% 50%;
  opacity:0;
  transition:opacity 1.1s var(--ease);
}
/* the earliest state is the base layer; later states fade in over it */
.match__layer:first-child{opacity:1}
.match[data-step="2"] .match__layer:nth-child(2),
.match[data-step="3"] .match__layer:nth-child(2),
.match[data-step="3"] .match__layer:nth-child(3){opacity:1}

/* soft bottom scrim so the stage label reads over the bright sweeps */
.match__scrim{
  position:absolute;
  inset:0;
  background:linear-gradient(0deg,
    rgba(11,14,20,.6),
    rgba(11,14,20,0) 34%);
  pointer-events:none;
}

.match__caption{position:absolute;inset:0;pointer-events:none}
.match__tag{
  position:absolute;
  left:clamp(16px,1.7vw,28px);
  bottom:clamp(14px,1.6vw,26px);
  font-family:var(--display);
  font-size:clamp(15px,1.5vw,26px);
  line-height:1;
  letter-spacing:.03em;
  text-transform:uppercase;
  color:var(--white);
  opacity:0;
  transform:translateY(8px);
  transition:opacity .7s var(--ease), transform .7s var(--ease);
}
.match[data-step="1"] .match__tag:nth-child(1),
.match[data-step="2"] .match__tag:nth-child(2),
.match[data-step="3"] .match__tag:nth-child(3){opacity:1;transform:none}

/* no JS: the runway collapses and the viewer parks on the first state */
.no-gsap .match__runway{height:auto}
.no-gsap .match__frame{position:static}

/* ---------- cloud band ----------
   The panorama that used to sit behind the country frames now has its own
   full-bleed section below them: a visual pause on the way to the contact
   block. */
.sky{
  position:relative;
  height:100vh;
  height:100svh;
  overflow:hidden;
  background:#0b0e14;
}
/* the image is taller than the band so [data-parallax] can drift it
   without ever uncovering the frame */
.sky__img{
  position:absolute;
  left:0;
  top:-9%;
  width:100%;
  height:118%;
  object-fit:cover;
  object-position:50% 50%;
  will-change:transform;
}
/* the band dissolves in and out of the page's fog field: no hard seam at
   either edge — the clouds condense out of the fog and thin back into it */
.sky::before{
  content:"";
  position:absolute;
  inset:0;
  z-index:2;
  pointer-events:none;
  background:
    linear-gradient(180deg,var(--fog) 0%,rgba(228,231,236,0) 32%),
    linear-gradient(0deg,var(--fog) 0%,rgba(228,231,236,0) 20%);
}
.sky::after{                      /* holds the ink mood over the bright sky */
  content:"";
  position:absolute;
  inset:0;
  background:rgba(11,14,20,.25);
  pointer-events:none;
}

/* ---------- reading the map ----------
   Explains the radar page's structure and its freshness signals. The demo
   panel recreates the app's own UI at the app's real values (see the app's
   style.css): chips are dark rounded rectangles, .chip-age-warn is a 3px
   #ff9800 outline (40+ min behind the latest), .chip-age-stale a 3px #f44336
   one (2 h+ or no data), and the bottom strip is the loading bar cycling
   its two states — blue "loading" and the red error blink, 5 s each, at the
   app's own colours, glow and blink rhythms (main.js only flips the class). */
.signals{
  padding:clamp(100px,20vh,240px) var(--gut) clamp(280px,56vh,620px);
  border-top:1px solid var(--line-dark);
}

.signals__eyebrow{
  font-size:clamp(10px,.72vw,12px);
  font-weight:500;
  letter-spacing:.26em;
  text-transform:uppercase;
  color:var(--grey-dark);
  margin-bottom:clamp(20px,3.4vh,42px);
}

.signals__lede{
  font-family:var(--ui);
  font-weight:300;
  font-size:clamp(24px,3.6vw,58px);
  line-height:1.1;
  letter-spacing:-.02em;
  max-width:19ch;
  text-wrap:balance;
}

.signals__cols{
  display:grid;
  grid-template-columns:repeat(auto-fit,minmax(260px,1fr));
  gap:clamp(22px,3vw,64px);
  margin-top:clamp(46px,8vh,96px);
  padding-top:clamp(24px,4vh,44px);
  border-top:1px solid var(--line-dark);
  max-width:1100px;
}
.signals__cols p{
  font-size:clamp(14px,1.05vw,17px);
  color:rgba(11,14,20,.66);
  line-height:1.68;
}

/* the demo panel: a slice of map surface — the Europe composite dimmed to a
   backdrop, three example chips, and the loading bar along the bottom edge */
.signals__demo{
  margin:clamp(44px,9vh,110px) auto 0;
  max-width:1100px;
}
.signals__map{
  position:relative;
  aspect-ratio:21/9;
  overflow:hidden;
  border:1px solid var(--line-dark);
  background:#0f141b;
  display:flex;
  align-items:center;
  justify-content:center;
}
.signals__map::before{            /* the composite, held back to a backdrop */
  content:"";
  position:absolute;
  inset:0;
  background:url(/about/images/eu2.png) 50% 45%/cover;
  opacity:.38;
}
.signals__legend{
  position:relative;
  z-index:1;
  display:flex;
  align-items:flex-start;
  justify-content:center;
  gap:clamp(28px,5vw,88px);
  padding:0 var(--gut);
}
.signals__station{
  display:flex;
  flex-direction:column;
  align-items:center;
  gap:12px;
}

/* chips, at the app's own geometry: dark pill, 1px light border, and the
   staleness variants grow the border to the app's 3px signal outline */
.signals__chip{
  display:inline-flex;
  align-items:center;
  justify-content:center;
  min-width:64px;
  padding:5px 12px;
  border-radius:10px;
  background:rgba(17,22,29,.86);
  color:#eaf1f7;
  border:1px solid rgba(255,255,255,.28);
  font-weight:600;
  font-size:13px;
  letter-spacing:.04em;
  box-shadow:0 1px 4px rgba(0,0,0,.45);
  forced-color-adjust:none;
  print-color-adjust:exact;
}
.signals__chip--warn{border:3px solid #ff9800}
.signals__chip--stale{border:3px solid #f44336}

.signals__note{
  font-size:clamp(10px,.8vw,12px);
  font-weight:500;
  text-align:center;
  color:#b7c1cb;
  max-width:17ch;
  line-height:1.5;
}

/* the loading bar, cycling the app's two states: blue "loading" at the
   app's normal 0.9s rhythm, then the red error blink at its faster 0.35s
   one — main.js flips the class every 5 s, CSS owns the looks */
.signals__bar{
  position:absolute;
  left:0;
  right:0;
  bottom:0;
  height:5px;
  z-index:2;
}
.signals__bar-fill{
  height:100%;
  background:#4fc3f7;             /* the app's --accent */
  box-shadow:0 0 8px rgba(79,195,247,.65);
  animation:signals-blink .9s ease-in-out infinite;
}
.signals__bar.is-error .signals__bar-fill{
  background:#f44336;
  box-shadow:0 0 8px rgba(244,67,54,.7);
  animation-duration:.35s;
}
@keyframes signals-blink{
  0%,100%{opacity:.12}
  50%{opacity:1}
}
/* the global reduce rule would freeze the blink at its dim keyframe — the
   point here is "this is the bar", so it simply stays solid instead */
@media (prefers-reduced-motion:reduce){
  .signals__bar-fill{animation:none;opacity:1}
}

.signals__caption{
  margin-top:clamp(16px,2.4vh,26px);
  text-align:center;
  font-size:clamp(9px,.68vw,11px);
  font-weight:500;
  letter-spacing:.2em;
  text-transform:uppercase;
  color:var(--grey-dark);
}

/* ---------- the standalone products page ----------
   No hero, so the fixed nav sits directly on the light field: invert it from
   the start instead of waiting for a dark section to scroll under it. */
.page .nav__opt{
  border-color:var(--line-dark);
  color:var(--ink);
}
.page .nav__opt:hover,
.page .nav__opt:focus-visible{
  background:var(--ink);
  border-color:var(--ink);
  color:var(--white);
}
.page .nav__opt[aria-current="page"]{border-color:var(--ink)}

.page__main{padding-top:clamp(96px,16vh,180px)}

/* ---------- product catalog (products page) ----------
   One card per parameter: the abbreviated code and its unit up top, the full
   name, then what it actually measures. The grid's background is the hairline
   colour showing through 1px gaps, so the cards read as one ruled table. */
.catalog{
  padding:clamp(60px,11vh,150px) var(--gut);
  border-top:1px solid var(--line-dark);
  max-width:1500px;
  margin-inline:auto;
}

.catalog__eyebrow{
  font-size:clamp(10px,.72vw,12px);
  font-weight:500;
  letter-spacing:.26em;
  text-transform:uppercase;
  color:var(--grey-dark);
  margin-bottom:clamp(20px,3.4vh,42px);
}

.catalog__lede{
  font-family:var(--ui);
  font-weight:300;
  font-size:clamp(24px,3.6vw,58px);
  line-height:1.1;
  letter-spacing:-.02em;
  max-width:19ch;
  text-wrap:balance;
}

.catalog__grid{
  display:grid;
  grid-template-columns:repeat(auto-fit,minmax(min(100%,320px),1fr));
  gap:1px;
  margin-top:clamp(40px,8vh,90px);
  /* Hairlines come from each card's own 1px ring (see .product), not from a
     grid background showing through the gaps: with an odd card count the
     empty last slot would otherwise render as a solid grey block. */
  padding:1px;
}

.product{
  display:flex;
  flex-direction:column;
  gap:clamp(8px,1.2vh,14px);
  padding:clamp(22px,2.4vw,40px);
  background:var(--fog);            /* the page field, so only the gaps show */
  box-shadow:0 0 0 1px var(--line-dark);   /* ruled-table hairline */
  transition:background-color .45s var(--ease);
}
.product:hover{background:var(--fog-deep)}

.product__head{
  display:flex;
  align-items:baseline;
  justify-content:space-between;
  gap:16px;
}
.product__code{
  font-family:var(--display);
  font-weight:400;
  font-size:clamp(26px,2.4vw,42px);
  line-height:1;
  letter-spacing:.02em;
  text-transform:uppercase;
}
.product__unit{
  font-size:clamp(10px,.72vw,12px);
  font-weight:500;
  letter-spacing:.2em;
  text-transform:uppercase;
  color:var(--grey-dark);
  white-space:nowrap;
}
.product__name{
  font-size:clamp(15px,1.15vw,19px);
  font-weight:500;
  letter-spacing:-.01em;
}
.product__text{
  font-size:clamp(13px,.95vw,16px);
  line-height:1.68;
  color:rgba(11,14,20,.66);
}

/* Hard facts under a card's description (map scale, where it is published):
   small label + value rows, pushed to the card's foot so every card in a
   row lines its facts up at the same height. */
.product__facts{
  margin-top:auto;
  padding-top:clamp(10px,1.4vh,16px);
  border-top:1px solid var(--line-dark);
  display:grid;
  grid-template-columns:auto 1fr;
  gap:6px 14px;
  font-size:clamp(12px,.85vw,14px);
  line-height:1.5;
}
.product__facts dt{
  font-size:clamp(9px,.66vw,11px);
  font-weight:500;
  letter-spacing:.18em;
  text-transform:uppercase;
  color:var(--grey-dark);
  padding-top:2px;
}
.product__facts dd{
  margin:0;
  color:rgba(11,14,20,.78);
}

.catalog__note{
  margin-top:clamp(22px,3.4vh,40px);
  font-size:clamp(12px,.9vw,15px);
  line-height:1.7;
  color:var(--grey-dark);
  max-width:66ch;
}

/* ---------- quote ---------- */
/* A near-full-screen beat between the two galleries: the line lands in the
   middle of the viewport, so the reader gets one clean text moment before
   the country deck starts. */
.quote{
  min-height:78vh;
  min-height:78svh;
  display:flex;
  align-items:center;
  justify-content:center;
  padding:clamp(60px,12vh,160px) var(--gut);
  border-top:1px solid var(--line-dark);
  border-bottom:1px solid var(--line-dark);
  text-align:center;
}
.quote blockquote{margin:0;width:100%;max-width:1100px}
.quote p{
  font-family:var(--ui);
  font-weight:300;
  font-size:clamp(22px,3.2vw,50px);
  line-height:1.14;
  letter-spacing:-.018em;
  text-wrap:balance;
}
.quote cite{
  display:block;
  margin-top:clamp(20px,3vh,34px);
  font-style:normal;
  font-size:clamp(10px,.72vw,12px);
  font-weight:500;
  letter-spacing:.24em;
  text-transform:uppercase;
  color:var(--grey-dark);
}

/* ---------- outro ---------- */
.outro{
  padding:clamp(80px,18vh,210px) var(--gut);
  text-align:center;
  max-width:900px;
  margin-inline:auto;
}
.outro__title{
  font-family:var(--display);
  font-weight:400;
  font-size:clamp(38px,8vw,120px);
  line-height:.92;
  letter-spacing:.005em;
  text-transform:uppercase;
}
.outro__text{
  margin:clamp(20px,3vh,34px) auto 0;
  font-size:clamp(14px,1vw,17px);
  color:rgba(11,14,20,.6);
  max-width:46ch;
}
.outro__text code{
  font-family:ui-monospace,SFMono-Regular,Menlo,monospace;
  font-size:.9em;
  padding:.15em .45em;
  border:1px solid var(--line-dark);
}

.outro__cta{
  display:inline-flex;
  align-items:center;
  height:clamp(38px,3vw,48px);
  margin-top:clamp(30px,5vh,54px);
  padding-inline:clamp(20px,2vw,34px);
  background:transparent;
  border:1px solid var(--line-dark);
  font-size:clamp(10px,.72vw,12px);
  font-weight:500;
  letter-spacing:.18em;
  text-transform:uppercase;
  transition:background-color .45s var(--ease),
             border-color .45s var(--ease),
             color .45s var(--ease);
}
.outro__cta:hover{background:var(--ink);border-color:var(--ink);color:var(--fog)}

/* ---------- footer ---------- */
.foot{
  padding:0 var(--gut) clamp(24px,4vh,44px);
  max-width:1500px;
  margin-inline:auto;
}
.foot__row{
  display:flex;
  flex-wrap:wrap;
  gap:10px clamp(20px,4vw,60px);
  justify-content:space-between;
  padding-top:clamp(18px,2.4vh,28px);
  border-top:1px solid var(--line-dark);
  font-size:clamp(9px,.68vw,11px);
  font-weight:500;
  letter-spacing:.2em;
  text-transform:uppercase;
  color:var(--grey-dark);
}
/* the two footer links back to the radar and to mail; `a{color:inherit}`
   already handles the colour, this only adds the hover cue */
.foot__link{transition:color .45s var(--ease)}
.foot__link:hover,.foot__link:focus-visible{color:var(--ink);outline:none}

/* the footer name mirrors the hero wordmark's entrance: JS splits it into
   letters that rise in once as the footer arrives, so the page ends the
   way it began. No GSAP: never split, renders static. */
.foot__brand .ch{
  display:inline-block;
  will-change:transform,opacity;
}

/* ---------- scroll-fade mask ----------
   JS scrubs --mask 0% -> 16% as the quote enters, so it resolves from a
   soft top fade to fully opaque. The resting value is 0%, i.e. fully
   visible: if GSAP never runs, nothing is hidden.

   .intro__lede is deliberately NOT in this list. It is the first thing a
   reader actually reads, and a partially transparent top edge on a block of
   that much text reads as a font-rendering fault rather than as an effect.
   .outro__title is not either: "Say hello" stays plain — the effect budget
   of the closing block goes to the footer name instead. */
.quote p{
  --mask:0%;
  -webkit-mask-image:linear-gradient(180deg,
    rgba(0,0,0,0) 0,
    rgba(0,0,0,1) var(--mask),
    rgba(0,0,0,1) 100%);
  mask-image:linear-gradient(180deg,
    rgba(0,0,0,0) 0,
    rgba(0,0,0,1) var(--mask),
    rgba(0,0,0,1) 100%);
}

/* ============================================================
   ENTRY REVEAL  (mirrors the reference's data-reveal)
   ============================================================ */[data-reveal]{
  animation:1.3s both reveal-opacity,
            1.3s cubic-bezier(.16,1,.3,1) both reveal-transform;
}
@keyframes reveal-opacity{from{opacity:0}to{opacity:1}}
@keyframes reveal-transform{from{transform:translateY(28px)}to{transform:translateY(0)}}

/* word reveal — main.js splits the big statements into .w spans (whole
   elements such as the products link or a stat number count as one word)
   and staggers them up once as the block scrolls in. Spaces stay real text
   nodes, so line wrapping is untouched. Without GSAP or under reduced
   motion the text is never split and renders static. */
.intro__lede .w,
.flick__lede .w,
.country__lede .w,
.signals__lede .w,
.stats__line .w{
  display:inline-block;
  will-change:transform,opacity;
}

/* ============================================================
   RESPONSIVE
   ============================================================ */
@media (max-width:900px){
  /* Three short words fit on a phone, so the rectangles stay visible rather
     than collapsing behind a toggle — the mobile sheet is gone with the brand. */
  .nav__opt{padding-inline:clamp(10px,2.6vw,16px)}
  .hero{height:300vh;height:300svh}
  .intro__lede{max-width:none}

  /* the overview falls back to a single column; the divider moves from the
     left edge to the top of the notes */
  .intro--screen{
    grid-template-columns:1fr;
    row-gap:clamp(28px,5vh,50px);
  }
  .intro--screen .intro__eyebrow,
  .intro--screen .intro__lede,
  .intro--screen .intro__cols{
    grid-column:1;
    grid-row:auto;
  }
  .intro--screen .intro__cols{
    border-left:0;
    border-top:1px solid var(--line-dark);
    padding-left:0;
    padding-top:clamp(22px,3.4vh,36px);
  }

  /* The flick row turns into a vertical accordion: seven expanded squares
     would never fit side by side, and the section is not pinned on phones
     (see main.js), so there is no scroll-advance to size the row against.
     Tap moves .is-active; with no JS at all every frame parks expanded so
     the stack is still a legible gallery. */
  .flick{min-height:0}
  .flick__row{
    flex-direction:column;
    height:auto;
    gap:clamp(6px,1.2vw,10px);
  }
  .flick__item{
    flex:none;
    height:clamp(150px,24vh,240px);
    transition:height 1.2s var(--ease);
  }
  .flick__item:focus-visible,
  .flick__item.is-active{
    flex:none;
    height:clamp(320px,52vh,520px);
  }
  /* The band height animates here, so the image must NOT take its size from
     the band — it would rescale and bring back the zoom this gallery avoids.
     It is pinned to the expanded band's size instead, so a collapsed band is
     a narrow window onto a constant-size image, exactly like desktop. */
  .flick__item img{
    width:clamp(294px,48vh,478px);
    height:clamp(294px,48vh,478px);
  }
  /* no JS: every band parks expanded (tap is what would move .is-active);
     the constant-size images already fit those bands, so nothing to override */
  .no-gsap .flick__item{height:clamp(320px,52vh,520px)}

  /* the signals demo drops its wide-map crop: the panel grows with the
     legend, which stacks so every chip keeps its caption beside it */
  .signals__map{
    aspect-ratio:auto;
    padding:clamp(40px,9vw,64px) var(--gut);
  }
  .signals__legend{
    flex-direction:column;
    align-items:stretch;
    gap:clamp(22px,4vw,32px);
  }
  .signals__station{
    flex-direction:row;
    align-items:center;
    justify-content:flex-start;
    gap:18px;
  }
  .signals__note{
    text-align:left;
    max-width:none;
  }

  /* the match runway is shorter on phones — same three states, less scroll */
  .match__runway{height:210vh;height:210svh}
}

@media (prefers-reduced-motion:reduce){
  *,*::before,*::after{
    animation-duration:.01ms !important;
    animation-iteration-count:1 !important;
    transition-duration:.01ms !important;
  }
  /* no scroll choreography: the viewer parks statically on the last state
     (main.js selects it, the CSS fade is instant here) */
  .match__runway{height:auto}
  .match__frame{position:static}
}
