/* ==========================================================================
   EV Clinic — the front page.

   The layer stack, and why it is this order:

     -2  body::before   the cell mosaic, fixed          (base.css)
     -1  body::after    the colour washes, fixed        (base.css)
      0  #backdrop      workshop footage as a mosaic    front page only
      1  #stage         the WebGL canvas
      2  main.front     everything that is read
      3  .front-rest    the sections below the story

   .front-rest is NOT opaque, and that is the load-bearing decision on this
   page. The previous version painted it solid so it would cover the fixed
   WebGL canvas once the story had played out. That works, but it also covers
   the mosaic and the washes — and those are the only thing the glass panels
   below have to refract. An opaque band turns every frosted panel on two
   thirds of the page into a grey rectangle.

   So the stage gets itself out of the way instead: evc-scene.js fades the
   canvas out by scroll position as the chapters end (the `past` calculation
   in mountScene). .front-rest only carries a gradient to soften the seam.
   ========================================================================== */

/* ------------------------------------------------------ the fixed layers -- */

#backdrop,
#stage{
  position:fixed;
  inset:0;
  pointer-events:none;
}
#backdrop{z-index:0}
#stage{z-index:1}

/*
   The mosaic.

   On the dark sibling this layer glows: the cells are brighter than the
   ground and it composites normally at 0.72. Here it has to go the other way.
   `multiply` makes each cell darken the paper under it, which is the only way
   a light page can carry this texture without turning into a pale wash — and
   it means the same footage reads as ink rather than as light.

   contrast() is above 1 here and below 1 on the dark page, for the same
   reason in reverse: there the blacks needed lifting off a black ground, here
   the mid-tones need separating from white.
*/
#bd-canvas{
  position:absolute;
  top:50%;
  left:50%;
  transform:translate(-50%,-50%);
  image-rendering:pixelated;
  opacity:var(--bd-opacity,.28);
  mix-blend-mode:multiply;
  filter:brightness(1.02) saturate(1.25) contrast(1.06);
  -webkit-mask-image:radial-gradient(circle at center,#000 41%,transparent 46%);
          mask-image:radial-gradient(circle at center,#000 41%,transparent 46%);
  -webkit-mask-size:var(--bd-cell,19px) var(--bd-cell,19px);
          mask-size:var(--bd-cell,19px) var(--bd-cell,19px);
  -webkit-mask-repeat:repeat;
          mask-repeat:repeat;
}

/* Keeps the decoder alive. display:none, visibility:hidden and detaching all
   suspend it, and then drawImage copies an empty frame forever. Two
   almost-transparent pixels are enough. */
.bd-src{
  position:absolute;
  width:2px;height:2px;
  opacity:.01;
  pointer-events:none;
}

.bd-tint{
  position:absolute;inset:0;
  background:linear-gradient(180deg,
    rgba(var(--white-rgb),.30) 0%,
    rgba(var(--white-rgb),.62) 100%);
}

/* The stage's own ground carries alpha, so the mosaic shows through it. */
/*
   The stage carries no ground of its own any more.

   It used to paint its own washes, which made sense when the page beneath it
   was flat. Now body::after does that for the whole document, and a second
   set here would double the tint over the hero and then stop abruptly where
   the fixed canvas ends.
*/
#stage{background:transparent}
#stage canvas{display:block;width:100%;height:100%}

/*
   Push the object out of the reading column.

   The scene renders centred in the viewport, which on the dark page is right:
   the copy sits on a translucent panel and the object behind it reads as
   depth. On paper an object crossing the text takes the text with it. Shifting
   the whole stage moves the object into the empty right-hand half without
   touching the camera, so the framing the scene computed from real dimensions
   is preserved exactly — a camera offset would have changed the perspective
   and pushed the far beats out of frame.

   The canvas moves, not #stage. #stage carries the base gradient, and
   translating that leaves a hard vertical edge down the left of the viewport
   where the wash simply stops.

   Only above 1100px, where there is a right-hand half to move into.
*/
@media (min-width:1100px){
  #stage canvas{transform:translateX(19%)}
}

/* Everything the visitor reads sits above both. */
.site-header{z-index:50}
.front{position:relative;z-index:2}

/*
   The opaque run.

   A hard edge where the transparent section ends would draw a line across the
   page. The gradient band above it covers the stage over about 140px instead,
   so the 3D fades out under the content rather than being cut off by it.
*/
.front-rest{
  position:relative;
  z-index:3;
  /* Transparent on purpose. See the note at the top of this file: painting
     this solid is what kills the glass everywhere below the story. */
  background:transparent;
}

/* A soft wash where the transparent run ends, so the 3D dissolves into the
   content rather than being cut off by an edge. Weak enough that the mosaic
   still reads through it. */
.front-rest::before{
  content:"";
  position:absolute;
  inset:-160px 0 auto;
  height:160px;
  background:linear-gradient(180deg,
    transparent,
    rgba(var(--white-rgb),.55) 55%,
    rgba(var(--white-rgb),.82));
  pointer-events:none;
}

/* ---------------------------------------------------------------- hero -- */

.hero{
  position:relative;
  min-height:92svh;
  display:flex;
  align-items:center;
  padding-block:clamp(72px,12vh,140px) clamp(48px,8vh,96px);
}
.hero-inner{width:100%}
.hero-col{max-width:min(620px,92%)}

.hero h1{margin-bottom:.42em;max-width:15ch}
.hero .big{
  font-size:clamp(17px,1.7vw,21px);
  line-height:1.55;
  color:var(--mute);
  max-width:46ch;
}

/*
   Copy over 3D, on paper.

   The dark sibling does not need this: white text on a near-black ground
   stays readable whatever passes behind it. Here the ground is paper and the
   objects are mid-grey metal, so grey body copy crossing a drive unit loses
   its edges entirely.

   Two things fix it together. The stage is pushed out of the text column on
   wide screens, and the text carries a soft paper halo for the cases where
   they still meet — a tall object, a short viewport, a long headline. The
   halo is not a box: at this radius it reads as the type being lit rather
   than as a panel behind it, which keeps the no-frames rule intact.
*/
.hero-col,
.chapter-col{
  /* A halo, not a panel. The glass 3D behind the copy is bright and busy, and
     grey body text crossing a refracting edge loses its shape. At this radius
     it reads as the type being lit rather than as a box behind it, which
     keeps the no-frames law intact. */
  text-shadow:
    0 0 14px rgba(var(--white-rgb),.96),
    0 0 30px rgba(var(--white-rgb),.86);
}
.hero-col .btn,
.chapter-col .btn{text-shadow:none}

/* -------------------------------------------------------------- readout --

   Fixed telemetry, bottom left, naming the beat the 3D is currently on.
   Hidden below 760px, where the stage is dialled back anyway and the screen
   has no room for a second layer of chrome. */

.hud{
  position:fixed;
  left:var(--gut);
  bottom:22px;
  z-index:40;
  display:flex;
  align-items:center;
  gap:11px;
  padding:8px 13px;
  background:linear-gradient(180deg, var(--glass-top), var(--glass-bottom));
  backdrop-filter:blur(var(--glass-blur)) saturate(var(--glass-saturate));
  -webkit-backdrop-filter:blur(var(--glass-blur)) saturate(var(--glass-saturate));
  border:1px solid var(--glass-edge);
  border-radius:var(--radius);
  box-shadow:inset 0 1px 0 var(--glass-sheen), var(--glass-shadow);
  font-family:var(--mono);
  font-size:10.5px;
  letter-spacing:.14em;
  text-transform:uppercase;
  color:var(--mute);
  opacity:0;
  transition:opacity .4s var(--ease-out);
  pointer-events:none;
}
.hud.is-on{opacity:1}
.hud b{color:var(--mint-ink);font-weight:500}
.hud .bar{position:relative;width:74px;height:1px;background:var(--line);overflow:hidden}
.hud .bar span{position:absolute;inset:0 auto 0 0;width:0;background:var(--mint-ink)}

@media (max-width:760px){ .hud{display:none} }

/* ------------------------------------------------------------- chapters -- */

.chapters{position:relative}

.chapter{
  min-height:100svh;
  display:flex;
  align-items:center;
  padding-block:clamp(64px,10vh,110px);
}
.chapter-col{max-width:min(520px,92%)}

/*
   The copy stays on the left through every chapter.

   The dark page alternates sides, which works there because the object is
   centred and the copy can sit over it. Here the stage is pushed into the
   right-hand half so the type stays clean, and a chapter that swapped to the
   right would land the copy straight back on the object. The markup still
   carries .right on alternate chapters — it costs nothing and it is the hook
   if the composition is ever revisited.
*/

.chapter h2{max-width:17ch;margin-bottom:.4em}
.chapter p{color:var(--mute);max-width:48ch}

.readout{
  margin:26px 0 0;
  padding:0;
  border-top:1px solid var(--line);
  font-family:var(--mono);
  font-size:12.5px;
}
.readout > div{
  display:flex;
  justify-content:space-between;
  gap:20px;
  padding:9px 0;
  border-bottom:1px solid var(--line-soft);
}
.readout dt{color:var(--mute);letter-spacing:.08em}
.readout dd{margin:0;color:var(--ink);font-weight:500}

@media (max-width:900px){
  .chapter{min-height:auto;padding-block:64px}
  .chapter-col,.chapter-col.right{max-width:100%;margin-left:0}
}

/* -------------------------------------------------------------- pillars -- */

.pillar-grid{counter-reset:pillar}
.pillar{border-top:1px solid var(--line);padding-top:16px}
.pillar-n{
  display:block;
  font-family:var(--mono);
  font-size:10px;
  letter-spacing:.2em;
  color:var(--amber-ink);
  margin-bottom:10px;
}
.pillar h3{margin-bottom:.4em}
.pillar p{font-size:14.5px;color:var(--mute);margin:0}

/* ----------------------------------------------------- generated grounds --

   No image files. Three radial washes, a hairline grid that drifts very
   slowly to give the eye a parallax cue, and a turbulence filter for grain.

   The grain is far weaker than on the dark page. Noise that reads as texture
   over near-black reads as a dirty screen over paper, and the blend mode has
   to be multiply rather than overlay for the same reason. */

.network{position:relative;overflow:hidden}

/*
   The network band's own grounds.

   The washes and the lattice that used to live here are now drawn once for
   the whole document in base.css, so these two are much weaker than on the
   dark page: they exist to make the map section a shade warmer than what
   surrounds it, not to supply texture that is already there.

   .bg-grid keeps its drift because it is the only moving thing in this band,
   and a lattice that creeps very slowly behind a static map is what makes
   the map feel like a live readout rather than an image.
*/
.bg-mesh{
  position:absolute;inset:0;z-index:0;pointer-events:none;
  background:
    radial-gradient(58% 44% at 14% 18%, rgba(var(--cold-rgb),.05), transparent 64%),
    radial-gradient(46% 38% at 86% 12%, rgba(var(--mint-rgb),.05), transparent 66%);
}

.bg-grid{
  position:absolute;inset:-2px;z-index:0;pointer-events:none;opacity:.55;
  background-image:
    linear-gradient(to right, var(--cell-ink) 1px, transparent 1px),
    linear-gradient(to bottom, var(--cell-ink) 1px, transparent 1px);
  /* A multiple of --cell, so this lattice and the page's land on the same
     lines instead of producing a moire. */
  background-size:calc(var(--cell) * 4) calc(var(--cell) * 4);
  animation:evc-drift 62s linear infinite;
}
@keyframes evc-drift{
  to{ background-position:calc(var(--cell) * 4) calc(var(--cell) * 4) }
}

.bg-grain{
  position:absolute;inset:0;z-index:0;width:100%;height:100%;
  pointer-events:none;
  opacity:.03;
  mix-blend-mode:multiply;
}

.network > .wrap{position:relative;z-index:1}

@media (prefers-reduced-motion:reduce){
  .bg-grid{animation:none}
}

/* --------------------------------------------------------- network head -- */

.network-head{
  display:flex;
  flex-wrap:wrap;
  gap:clamp(20px,4vw,56px);
  align-items:flex-end;
  justify-content:space-between;
  margin-bottom:clamp(24px,3vw,40px);
}
.head-copy{max-width:56ch}
.head-copy .section-head{margin-bottom:0}

.net-counts{display:flex;gap:clamp(20px,3vw,40px);margin:0}
.net-counts > div{display:flex;flex-direction:column;gap:4px}
.net-counts dt{
  font-family:var(--mono);font-size:9.5px;letter-spacing:.18em;
  text-transform:uppercase;color:var(--faint);
}
.net-counts dd{
  margin:0;
  font-family:var(--display);font-weight:700;
  font-size:clamp(26px,3vw,38px);line-height:1;
  letter-spacing:-.02em;
}

.feed-state{
  margin-top:16px;
  font-size:10px;letter-spacing:.16em;text-transform:uppercase;
  color:var(--faint);
}
/* A snapshot is not live data, and the page should not imply that it is. */
.feed-state[data-source="fallback"]{color:var(--amber-ink)}

/* ------------------------------------------------------- map fallback --

   Removed from the DOM by evc-front.js once the map has drawn, so this is
   what a crawler, a no-JS browser and a failed module get. */

.map-fallback{margin-top:clamp(20px,3vw,32px)}
.mini-head{
  font-family:var(--mono);font-size:10px;letter-spacing:.2em;text-transform:uppercase;
  color:var(--faint);font-weight:500;margin:0 0 14px;
}
.centre-list{list-style:none;margin:0;padding:0;
  display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:2px 24px}
.centre{
  display:flex;align-items:baseline;gap:8px;
  padding:9px 0;border-bottom:1px solid var(--line-soft);
  font-size:14px;
}
.centre a{color:var(--ink);text-decoration:none}
.centre a:hover{color:var(--cold);text-decoration:underline}
.centre-status{
  margin-left:auto;
  font-family:var(--mono);font-size:10px;letter-spacing:.12em;text-transform:uppercase;
  color:var(--faint);
}
.centre[data-status="open"] .centre-status{color:var(--st-open)}
.centre[data-status="soon"] .centre-status{color:var(--st-soon)}

/* ------------------------------------------------------------- marquee -- */

.marquee{
  position:relative;
  overflow:hidden;
  border-block:1px solid var(--line);
  background:linear-gradient(180deg, var(--glass-top), var(--glass-bottom));
  backdrop-filter:blur(var(--glass-blur)) saturate(var(--glass-saturate));
  -webkit-backdrop-filter:blur(var(--glass-blur)) saturate(var(--glass-saturate));
  box-shadow:inset 0 1px 0 var(--glass-sheen);
  padding-block:11px;
  /* Skews with scroll velocity: the custom property is published by
     publishScrollVelocity() in evc-motion.js. */
  transform:skewX(calc(var(--scroll-v,0) * -0.6deg));
}
.marquee-track{
  display:flex;
  gap:34px;
  width:max-content;
  animation:evc-marq 48s linear infinite;
}
.marquee:hover .marquee-track{animation-play-state:paused}
@keyframes evc-marq{ to{ transform:translateX(-50%) } }

.marq-item{
  display:inline-flex;align-items:center;gap:8px;
  font-family:var(--mono);font-size:11px;letter-spacing:.1em;
  white-space:nowrap;
  color:var(--mute);
}
.marq-item b{font-weight:500;color:var(--ink)}
.marq-item em{font-style:normal;font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--faint)}
.marq-item .dot{width:5px;height:5px;border-radius:50%;background:var(--st-soon);flex:none}
.marq-item[data-status="open"] .dot{background:var(--st-open)}
.marq-item[data-status="boarding"] .dot{background:var(--st-boarding)}
.marq-item[data-status="signed"] .dot{background:var(--st-signed)}
.marq-item[data-status="available"] .dot{background:transparent;border:1px solid var(--amber)}

@media (prefers-reduced-motion:reduce){
  .marquee{transform:none}
  .marquee-track{animation:none}
}

/* -------------------------------------------------------------- rotator -- */

.journal-head{
  display:flex;flex-wrap:wrap;gap:20px 32px;
  align-items:flex-end;justify-content:space-between;
  margin-bottom:clamp(22px,3vw,34px);
}
.journal-head .section-head{margin-bottom:0;flex:1 1 320px}

.rot-tabs{display:flex;flex-wrap:wrap;gap:8px}
.rot-tab{cursor:pointer;font-family:var(--mono)}
.rot-tab.is-on{border-color:var(--ink);color:var(--ink)}

.rot-panel[hidden]{display:none}

/*
   A real scroll container, not a transform carousel.

   It works before the JS runs: a touch device swipes it, a trackpad scrolls
   it, and a keyboard arrows through it once focused. The buttons are an
   addition rather than the mechanism.
*/
.rot-track{
  display:grid;
  grid-auto-flow:column;
  grid-auto-columns:minmax(clamp(240px,26vw,320px),1fr);
  gap:clamp(14px,1.8vw,22px);
  overflow-x:auto;
  scroll-snap-type:x mandatory;
  scroll-padding-inline:2px;
  padding-bottom:6px;
  /* The scrollbar is part of the design here: hiding it removes the only cue
     that there is more to the right on a device with no touch affordance. */
  scrollbar-width:thin;
  scrollbar-color:var(--line) transparent;
}
.rot-track > *{scroll-snap-align:start}
.rot-track::-webkit-scrollbar{height:6px}
.rot-track::-webkit-scrollbar-thumb{background:var(--line);border-radius:3px}
.rot-track::-webkit-scrollbar-track{background:transparent}

.rot-foot{
  display:flex;align-items:center;justify-content:space-between;
  gap:16px;flex-wrap:wrap;
  margin-top:18px;
}
.rot-nav{display:flex;gap:6px}
.rot-nav button{
  width:36px;height:36px;
  display:grid;place-items:center;
  background:var(--panel);
  border:1px solid var(--line);border-radius:var(--radius);
  color:var(--ink);cursor:pointer;
  font-size:14px;line-height:1;
  transition:border-color .2s var(--ease-out),color .2s var(--ease-out),opacity .2s;
}
.rot-nav button:hover:not(:disabled){border-color:var(--cold);color:var(--cold)}
.rot-nav button:disabled{opacity:.32;cursor:default}

/* ------------------------------------------------------------ shop, kb -- */

.shop-cats,.academy-cats{max-width:60ch}
.price{
  font-family:var(--mono);font-size:14px;font-weight:500;
  color:var(--ink);margin:0;
}
.price del{color:var(--faint);font-weight:400;margin-right:.4em}
.price ins{text-decoration:none;color:var(--amber-ink)}

.academy-grid{
  display:grid;
  grid-template-columns:1.25fr 1fr;
  gap:clamp(28px,4vw,64px);
  align-items:start;
}
@media (max-width:900px){ .academy-grid{grid-template-columns:1fr} }

.academy-count{font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:var(--faint);margin:14px 0 18px}
.kb-list{display:grid;gap:2px}
.kb-card{border-top:1px solid var(--line-soft)}
.kb-card .card-body{padding:14px 0}

/* ------------------------------------------------------------- services -- */

.service-list{list-style:none;margin:0;padding:0;border-top:1px solid var(--line)}
.service{border-bottom:1px solid var(--line-soft)}
.service-link{
  display:flex;align-items:baseline;gap:20px;
  padding:18px 0;
  text-decoration:none;color:var(--ink);
  transition:padding-left .3s var(--ease-out);
}
.service-link:hover,.service-link:focus-visible{padding-left:10px;color:var(--cold)}
.service-name{font-family:var(--display);font-weight:600;font-size:clamp(17px,1.7vw,21px)}
.service-price{
  margin-left:auto;
  font-family:var(--mono);font-size:14px;letter-spacing:.04em;
  color:var(--amber-ink);white-space:nowrap;
}
.service-price.muted{color:var(--faint)}

/* --------------------------------------------------------------- doors -- */

.door{
  display:flex;flex-direction:column;gap:9px;
  padding-top:16px;
  border-top:1px solid var(--line);
  text-decoration:none;color:inherit;
  transition:border-color .3s var(--ease-out);
}
.door:hover,.door:focus-visible{border-top-color:var(--amber)}
.door h3{margin:0}
.door p{font-size:14px;color:var(--mute);margin:0;flex:1}
.door-cta{
  font-family:var(--mono);font-size:11px;letter-spacing:.1em;
  color:var(--cold);margin-top:4px;
}

/* ----------------------------------------------------------------- r&d --

   Deliberately not a card. Boxing it would give it the same weight as the
   four doors above, and it is a different kind of ask. */

.rnd{text-align:left}
.rnd h2{max-width:20ch}
.pulse{
  width:7px;height:7px;border-radius:50%;
  background:var(--mint);
  box-shadow:0 0 0 0 rgba(var(--mint-rgb),.6);
  animation:evc-pulse 2.4s var(--ease-out) infinite;
  flex:none;
}
@keyframes evc-pulse{
  70%{box-shadow:0 0 0 9px rgba(var(--mint-rgb),0)}
  100%{box-shadow:0 0 0 0 rgba(var(--mint-rgb),0)}
}
@media (prefers-reduced-motion:reduce){ .pulse{animation:none} }

/* ------------------------------------------------ small screens, no 3D -- */

@media (max-width:760px){
  /* The stage is dialled back rather than removed: at this size the geometry
     is too small to read, but the wash it casts is still part of the page. */
  #stage{opacity:.45}
  .hero{min-height:auto;padding-top:96px}
}

/* ------------------------------------------------------------- proof grid --

   CSS columns, not grid.

   The source set is thirteen portrait photographs to eight landscape, and a
   grid of fixed spans always leaves holes when the heights vary that much.
   Columns take mixed heights without gaps. The trade is that reading order
   runs down each column rather than across, which is the right trade for a
   set of photographs and the wrong one for anything with an order. */

.proof-grid{
  column-count:3;
  column-gap:clamp(12px,1.6vw,20px);
}
@media (max-width:900px){ .proof-grid{column-count:2} }
@media (max-width:560px){ .proof-grid{column-count:1} }

.tile{
  position:relative;
  margin:0 0 clamp(12px,1.6vw,20px);
  /* A tile must not be split across a column boundary. */
  break-inside:avoid;
  page-break-inside:avoid;
  border-radius:var(--radius);
  overflow:hidden;
  background:var(--raise);
}

.tile img,
.tile video{
  display:block;
  width:100%;
  height:auto;
  /*
     Duotone.

     `color` takes the hue and saturation of the blend layer and the
     luminosity of what is underneath — so a greyscaled photograph supplies
     the tonal range and the gradient supplies the colour. That is the
     definition of a duotone, and it is why the image is desaturated first:
     the filter clears the hue so the gradient has something to fill.

     `luminosity` was tried here and is exactly backwards. It takes the
     gradient's brightness and the photograph's colour, and since the
     photograph has just been stripped of colour the result is a flat grey
     wash with none of the ramp in it.
  */
  filter:grayscale(1) contrast(1.06) brightness(1.02);
  transform:scale(1.02);
  transition:filter .45s var(--ease-out),transform .45s var(--ease-out);
}

.tile::before{
  content:"";
  position:absolute;inset:0;z-index:1;
  /* The palette's three accents, in order. Restrained: at full strength a
     duotone over a light page reads as a colour cast rather than as a print
     treatment, and the hover has to have somewhere to travel to. */
  background:linear-gradient(150deg,#2B4BD8 0%,#0F7A57 56%,#8A5200 100%);
  mix-blend-mode:color;
  opacity:.55;
  pointer-events:none;
  transition:opacity .45s var(--ease-out);
}

/* Resolving to full colour on hover is the point: the duotone says "this is
   a set", the hover says "this is a real photograph". */
.tile:hover img,.tile:hover video,
.tile:focus-within img,.tile:focus-within video{
  filter:none;
  transform:scale(1.04);
}
.tile:hover::before,.tile:focus-within::before{opacity:0}

/* A tile carrying a clip gets its <video> created on scroll by
   initLazyMedia(); until then it is a photograph and costs a photograph. */
.tile.has-clip::after{
  content:"";
  position:absolute;right:10px;top:10px;z-index:2;
  width:7px;height:7px;border-radius:50%;
  background:var(--mint);
  box-shadow:0 0 0 3px rgba(255,255,255,.85);
}
.tile video{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}

@media (prefers-reduced-motion:reduce){
  .tile img,.tile video{transform:none;transition:none}
}
