/* ==========================================================================
   BON JEU — homepage
   Built to design/comp-home.png. Every position and size below traces to a
   measured value in design/MEASUREMENTS.md, not to an eyeballed estimate.

   The comp is 1536 x 1024. `--u` is one design pixel, so at a 1536-wide
   viewport every calc() below resolves to its measured value exactly, and
   scales proportionally at other widths.
   ========================================================================== */

/* Palette, type stack and reset come from tokens.css. */

:root {
  --u: calc(100vw / 1536);
  --ease: cubic-bezier(0.22, 0.61, 0.36, 1);

  --ink: #ffffff;
  --ink-dim: rgba(255, 255, 255, 0.45);

  /* Cap-height offsets. A text box's top sits above its cap line by an amount
     that depends on the font's metrics, so these are calibrated against the
     comp by scripts/verify_build.py rather than derived. */
  /* Hero plate aspect and the garment hotspots inside it. Mobile overrides
     both: it is served a 9:16 crop centred on the model, so the garments sit
     at different percentages of the frame. */
  --hero-ar: 1.7778;
  --jacket-x: 62.5%; --jacket-y: 13%; --jacket-w: 20%; --jacket-h: 41%;
  --short-x: 64.5%;  --short-y: 53%;  --short-w: 14%;  --short-h: 18%;

  --nav-top: 32;
  --rail-l-top: 185;
  --rail-r-top: 484;
  --wm-top: 371;
  --player-top: 747;
  --scroll-top: 922;
  --sound-top: 921;
}

html { background: #000; }

body {
  color: var(--ink);
  background: #000;
}

:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: calc(4 * var(--u));
}

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

.hero {
  position: relative;
  width: 100%;
  height: 100vh;
  min-height: 640px;
  overflow: hidden;
  isolation: isolate;
}

/* The stage is the plate's coordinate space. It keeps the image's exact 16:9
   ratio while covering the viewport, so a percentage inside it maps to the
   same point on the photograph at any window shape — which is what lets the
   garment hotspots stay locked to the garments. */
.hero__stage {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: max(100%, calc(100vh * var(--hero-ar)));
  height: max(100%, calc(100vw / var(--hero-ar)));
}

.hero__plates { position: absolute; inset: 0; }

.hero__plate {
  position: absolute; inset: 0;
  background-size: cover;
  background-position: center;
  opacity: 0;
  transform: scale(1);
  transition: opacity 900ms ease, filter 620ms var(--ease),
              transform 900ms var(--ease);
}
.hero__plate.is-active { opacity: 1; }

/* ------------------------------------------------------- garment focus pull */

/* The garment grows out of the one he is wearing rather than appearing beside
   him: each layer is centred on that garment's position in the plate and
   scales up from it. Moving it to clear space broke the read — the effect is
   meant to feel like the piece stepping forward off the model. */
.focus {
  position: absolute;
  left: var(--fx);
  top: var(--fy);
  width: var(--fw);
  display: flex;
  flex-direction: column;
  align-items: center;
  opacity: 0;
  pointer-events: none;
  transform: translate(-50%, -50%) scale(0.55);
  transition: opacity 420ms var(--ease), transform 780ms var(--ease);
}

/* Centres measured off design/hero-terrace-grid.png: the jacket occupies
   63-82% x / 14-53% y, the shorts 65-78% x / 53-70% y. */
.focus--jacket { --fx: 71%; --fy: 36%; --fw: 26%; }
.focus--short  { --fx: 71%; --fy: 60%; --fw: 21%; }

.focus__garment { display: block; width: 100%; }

.focus__garment img {
  width: 100%;
  height: auto;
  /* Grounds the cut-out against the blurred photograph so it reads as lifted
     out of the frame rather than pasted onto it. */
  filter: drop-shadow(0 calc(18 * var(--u)) calc(34 * var(--u)) rgba(0, 12, 32, 0.5));
}

.focus__name {
  margin-top: calc(26 * var(--u));
  font-size: calc(13 * var(--u));
  font-weight: 400;
  letter-spacing: calc(3.4 * var(--u));
  text-transform: uppercase;
}

.focus__price {
  margin-top: calc(8 * var(--u));
  font-size: calc(12 * var(--u));
  letter-spacing: calc(2 * var(--u));
  color: rgba(255, 255, 255, 0.72);
}

/* Only meaningful where there is no hover — on a pointer device the second
   tap does not exist. */
.focus__hint {
  display: none;
  margin-top: calc(14 * var(--u));
  font-size: calc(10 * var(--u));
  letter-spacing: calc(2.4 * var(--u));
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.5);
}

/* Hit zones, in stage percentages measured off the plate. */
.hotspot {
  position: absolute;
  display: block;
  cursor: pointer;
  /* Keeps the zone reachable without painting anything. */
  background: transparent;
}
/* Measured off design/hero-terrace-grid.png; overridden for the mobile crop. */
.hotspot--jacket {
  left: var(--jacket-x); top: var(--jacket-y);
  width: var(--jacket-w); height: var(--jacket-h);
}
.hotspot--short {
  left: var(--short-x); top: var(--short-y);
  width: var(--short-w); height: var(--short-h);
}

@media (hover: hover) {
  .hero:has(.hotspot--jacket:hover) .focus--jacket,
  .hero:has(.hotspot--jacket:focus-visible) .focus--jacket,
  .hero:has(.hotspot--short:hover) .focus--short,
  .hero:has(.hotspot--short:focus-visible) .focus--short {
    opacity: 1;
    transform: translate(-50%, -50%) scale(1);
  }

  /* The photograph falls back. The slight scale hides the soft edge blur
     leaves at the element's bounds. */
  .hero:has(.hotspot:hover) .hero__plate,
  .hero:has(.hotspot:focus-visible) .hero__plate {
    filter: blur(calc(6 * var(--u))) brightness(0.68) saturate(0.92);
    transform: scale(1.04);
  }

  /* The hero yields to the product: the wordmark and the garment label want
     the same space, and the label would otherwise sit on the track title. */
  .hero:has(.hotspot:hover) .wordmark,
  .hero:has(.hotspot:focus-visible) .wordmark {
    opacity: 0;
  }

  .hero:has(.hotspot:hover) .player,
  .hero:has(.hotspot:hover) .rail,
  .hero:has(.hotspot:hover) .sound,
  .hero:has(.hotspot:focus-visible) .player,
  .hero:has(.hotspot:focus-visible) .rail,
  .hero:has(.hotspot:focus-visible) .sound {
    opacity: 0.16;
  }
}

@media (hover: none) {
  .focus__hint { display: block; }

  .hero[data-focus='jacket'] .focus--jacket,
  .hero[data-focus='short'] .focus--short {
    opacity: 1;
    transform: translate(-50%, -50%) scale(1);
  }

  .hero[data-focus='jacket'] .hero__plate,
  .hero[data-focus='short'] .hero__plate {
    filter: blur(calc(6 * var(--u))) brightness(0.62) saturate(0.92);
    transform: scale(1.04);
  }

  .hero[data-focus='jacket'] .wordmark,
  .hero[data-focus='short'] .wordmark { opacity: 0; }

  .hero[data-focus='jacket'] .player,
  .hero[data-focus='jacket'] .sound,
  .hero[data-focus='short'] .player,
  .hero[data-focus='short'] .sound { opacity: 0; }
}

/* Legibility scrim. Off by default: the shipped plate is the comp, which is
   already graded. Enable once real photography is in place. */
.hero::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    linear-gradient(to bottom, rgba(0,0,0,.52) 0%, rgba(0,0,0,0) 30%),
    linear-gradient(to top,    rgba(0,0,0,.50) 0%, rgba(0,0,0,0) 34%),
    rgba(0, 0, 0, 0.18);
}

/* Comp mode measures the overlay against a black plate, so the scrim would
   only wash out the readings. */
html[data-comp='1'] .hero::after { display: none; }

/* Comp mode (?comp=1): drop the photograph so scripts/verify_build.py measures
   the overlay's own geometry against a black plate. Test fixture only. */
html[data-comp='1'] .hero__plates { display: none; }
html[data-comp='1'] .hero { background: #000; }

/* ---------------------------------------------------------------------- nav */
/* Cap band y 35-46. MENU at x56, BAG right edge x1482, centre group 503-973. */

.nav {
  position: absolute;
  top: calc(var(--nav-top) * var(--u));
  left: 0; right: 0;
  padding: 0 calc(54 * var(--u));
  display: flex;
  align-items: baseline;
  font-size: calc(15 * var(--u));
  font-weight: 400;
  letter-spacing: calc(2.7 * var(--u));
  text-transform: uppercase;
  line-height: 1;
}

/* Truly centred. The comp put this group 15px left of centre, which was a
   generation artefact I matched while the comp was the authority. It is not
   the authority any more, and the asymmetry was never intentional. */
.nav__links {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  gap: calc(54 * var(--u));
}

/* The comp sets the bag count's brackets at cap height — they sit inside the
   nav's 35-46 band rather than descending, as Archivo's do by default. */
.nav__paren {
  font-size: 0.75em;
  vertical-align: 0.1em;
}
.nav__bag { margin-left: auto; }

.lang {
  display: inline-flex;
  align-items: baseline;
  gap: calc(6 * var(--u));
  margin-left: calc(40 * var(--u));
  font-size: calc(13 * var(--u));
  letter-spacing: calc(1.6 * var(--u));
}
.lang span { opacity: 0.4; }
.lang button { opacity: 0.5; transition: opacity 200ms ease; }
.lang button:hover { opacity: 0.85; }
.lang button[aria-current='true'] { opacity: 1; }

.nav__links a, .nav__bag {
  transition: opacity 200ms ease;
}
.nav__links a:hover, .nav__bag:hover { opacity: 0.62; }


/* Bilingual pairs. The French sits beneath its English in italic — §11's
   "small poetic detail" — quiet enough to read as a caption rather than as
   duplicate copy. */
.alt {
  color: rgba(255, 255, 255, 0.55);
  font-size: 0.82em;
  margin-top: calc(4 * var(--u));
  display: block;
  font-style: italic;
  text-transform: none;
  letter-spacing: 0.04em;
}

/* --------------------------------------------------------------- side rails */
/* Vertical text runs top-to-bottom; letter-band analysis of the comp confirms
   reading order (8 bands + gap + 5 = "SPORTING" "GOODS"). */

.rail { position: absolute; transition: opacity 420ms var(--ease); }

/* Two stacked block spans in vertical-rl become two columns filling
   right-to-left, so line 1 sits outermost — which is how the comp reads.
   Line-height is the column pitch here, not the leading. */
.rail__text {
  writing-mode: vertical-rl;
  font-size: calc(11 * var(--u));
  font-weight: 400;
  text-transform: uppercase;
}

.rail__text span { display: block; }
.rail__text .alt { display: none; }

.rail--left {
  top: calc(var(--rail-l-top) * var(--u));
  left: calc(47.25 * var(--u));
  width: calc(43 * var(--u));
}
.rail--left .rail__text {
  line-height: calc(21.5 * var(--u));
  letter-spacing: calc(3.45 * var(--u));
}
.rail--left .rail__text span:nth-child(1) { letter-spacing: calc(3.30 * var(--u)); }
.rail--left .rail__text span:nth-child(2) { letter-spacing: calc(3.64 * var(--u)); }

.rail--right {
  top: calc(var(--rail-r-top) * var(--u));
  right: calc(45.75 * var(--u));
  width: calc(41 * var(--u));
}
.rail--right .rail__text {
  line-height: calc(20.5 * var(--u));
  letter-spacing: calc(3.13 * var(--u));
}

/* Vertical rule: x65, 1px, y380-463, fading downward. Positioned relative to
   the rail's own top so the two move together. */
.rail__rule {
  position: absolute;
  left: calc(17.75 * var(--u));
  top: calc(195 * var(--u));
  width: 1px;
  height: calc(110 * var(--u));
  background: linear-gradient(to bottom,
    rgba(255,255,255,.95) 0%, rgba(255,255,255,.62) 76%, rgba(255,255,255,0) 100%);
}

/* Slide dots: centre x65, diameter 14, pitch 32.5, first filled. */
.dots[hidden] { display: none; }

.dots {
  position: absolute;
  left: calc(10.75 * var(--u));
  top: calc(310 * var(--u));
  display: flex;
  flex-direction: column;
  gap: calc(18.3 * var(--u));
}

.dot {
  width: calc(14 * var(--u));
  height: calc(14 * var(--u));
  border: 1px solid rgba(255,255,255,.9);
  border-radius: 50%;
  background: transparent;
  padding: 0;
  transition: background 300ms ease;
}
.dot.is-active { background: var(--ink); }

/* ----------------------------------------------------------------- wordmark */
/* The embroidered artwork, cropped to its own ink, so the element box IS the
   mark — no cap-height offsets to calibrate. Block geometry still matches the
   comp: 415px wide, centred on the comp's y481. */

.wordmark {
  position: absolute;
  top: 50%;
  left: calc(547 * var(--u));
  width: calc(415 * var(--u));
  transform: translateY(calc(-50% - 30.5 * var(--u)));
  pointer-events: none;
  transition: opacity 500ms ease;
}

.wordmark img { width: 100%; height: auto; }

/* ------------------------------------------------------------------- player */

.player {
  position: absolute;
  transition: opacity 420ms var(--ease);
  bottom: calc(48 * var(--u));
  left: calc(56 * var(--u));
  width: calc(351 * var(--u));
}

.player__eyebrow {
  font-size: calc(10.5 * var(--u));
  letter-spacing: calc(3.2 * var(--u));
  text-transform: uppercase;
  line-height: 1;
}

.player__title {
  margin-top: calc(21 * var(--u));
  font-size: calc(22 * var(--u));
  font-weight: 400;
  letter-spacing: calc(1.2 * var(--u));
  text-transform: uppercase;
  line-height: 1;
}

.player__sub {
  margin-top: calc(12 * var(--u));
  font-size: calc(12.5 * var(--u));
  font-weight: 300;
  letter-spacing: calc(2.8 * var(--u));
  text-transform: uppercase;
  line-height: 1;
}

/* Scrub bar: track x56-406 (351 wide), centred on y859. */
.player__scrub { margin-top: calc(28 * var(--u)); }


.scrub {
  position: relative;
  height: calc(12 * var(--u));
  cursor: pointer;
  touch-action: none;
}

.scrub__track, .scrub__fill {
  position: absolute;
  top: 50%;
  left: 0;
  height: 1px;
  transform: translateY(-50%);
}
.scrub__track { width: 100%; background: var(--ink-dim); }
.scrub__fill  { width: 0; background: var(--ink); }

.scrub__knob {
  position: absolute;
  top: 50%;
  left: 0;
  width: calc(10 * var(--u));
  height: calc(10 * var(--u));
  margin-left: calc(-5 * var(--u));
  border-radius: 50%;
  background: var(--ink);
  transform: translateY(-50%);
}

.player__times {
  display: flex;
  justify-content: space-between;
  margin-top: calc(12 * var(--u));
  font-size: calc(11 * var(--u));
  letter-spacing: calc(0.8 * var(--u));
  line-height: 1;
}

/* Controls sit on measured centres (73, 146.5, 229.5, 314.5, 389 from the
   player's left edge at x56), which is not an even distribution. */
.player__controls {
  position: relative;
  height: calc(78 * var(--u));
  margin-top: calc(9 * var(--u));
}

.ctrl {
  position: absolute;
  top: 50%;
  display: grid;
  place-items: center;
  transform: translate(-50%, -50%);
  transition: opacity 200ms ease;
}
.ctrl:hover { opacity: 0.65; }

.ctrl svg {
  display: block;
  fill: none;
  stroke: var(--ink);
  stroke-width: 1.4;
  stroke-linecap: round;
  stroke-linejoin: round;
  overflow: visible;
}
.ctrl svg .fill { fill: var(--ink); stroke: none; }

#btn-shuffle { left: calc(17 * var(--u));  width: calc(29 * var(--u)); }
#btn-prev    { left: calc(90.5 * var(--u)); width: calc(18 * var(--u)); }
#btn-play    { left: calc(173.5 * var(--u)); width: calc(78 * var(--u)); height: calc(78 * var(--u)); }
#btn-next    { left: calc(258.5 * var(--u)); width: calc(18 * var(--u)); }
#btn-repeat  { left: calc(333 * var(--u)); width: calc(29 * var(--u)); }

#btn-shuffle svg, #btn-repeat svg { width: calc(29 * var(--u)); }
#btn-prev svg, #btn-next svg { width: calc(18 * var(--u)); }

.ctrl--play .ctrl__ring {
  position: absolute;
  inset: 0;
  border: 1px solid rgba(255,255,255,.9);
  border-radius: 50%;
}
.ctrl--play svg { width: calc(18 * var(--u)); position: relative; }

/* Icon swaps with real playback state — never decorative. */
.ctrl--play .icon-play { display: none; }
.ctrl--play[data-state='paused'] .icon-pause { display: none; }
.ctrl--play[data-state='paused'] .icon-play  { display: block; }

/* Shuffle and repeat read at full white in their default state, as the comp
   draws them. Their "on" state is a dot beneath the icon rather than a
   brightness change, which would otherwise be the only cue and is too weak. */
#btn-shuffle::after, #btn-repeat::after {
  content: '';
  position: absolute;
  bottom: calc(-9 * var(--u));
  left: 50%;
  width: calc(3 * var(--u));
  height: calc(3 * var(--u));
  margin-left: calc(-1.5 * var(--u));
  border-radius: 50%;
  background: var(--ink);
  opacity: 0;
  transition: opacity 200ms ease;
}
#btn-shuffle[aria-pressed='true']::after,
#btn-repeat[aria-pressed='true']::after { opacity: 1; }

/* -------------------------------------------------------------- sound meter */
/* SOUND y923-932; ON y938-952; 9 bars, 8px pitch, x1417-1482, baseline y952. */

/* The three parts sit on measured offsets rather than in flow: the comp puts
   ON's baseline and the bar baseline both on y952, which flex alignment can't
   express (a text box's baseline sits above its box bottom by its descender). */
.sound {
  position: absolute;
  bottom: calc(72 * var(--u));
  right: calc(53 * var(--u));
  transition: opacity 420ms var(--ease);
  width: calc(98 * var(--u));
  height: calc(31 * var(--u));
  text-align: left;
  font-size: calc(12 * var(--u));
  letter-spacing: calc(2.2 * var(--u));
  text-transform: uppercase;
  line-height: 1;
}

.sound__row { display: contents; }

.sound__label { position: absolute; left: 0; top: 0; }

.sound__state {
  position: absolute;
  left: 0;
  bottom: calc(-3 * var(--u));
  font-size: calc(13.8 * var(--u));
  letter-spacing: calc(0.5 * var(--u));
  line-height: 1;
}

.eq {
  position: absolute;
  right: 0;
  bottom: 0;
  display: flex;
  align-items: flex-end;
  gap: calc(7.1 * var(--u));
  height: calc(23 * var(--u));
}

.eq span {
  display: block;
  width: 1px;
  background: var(--ink);
  transform-origin: bottom;
}

/* Bars animate only while audio is genuinely playing and unmuted. */
.sound[data-playing='true'] .eq span {
  animation: eq-pulse 900ms ease-in-out infinite alternate;
}

@keyframes eq-pulse {
  from { transform: scaleY(0.45); }
  to   { transform: scaleY(1); }
}

@media (prefers-reduced-motion: reduce) {
  .sound[data-playing='true'] .eq span { animation: none; }
  .hero__plate { transition: none; }
  .focus { transition: opacity 200ms ease; }
}

/* ------------------------------------------------------------------ mobile */
/* The hero composition is a desktop layout. Below 900px it reflows rather
   than scaling down, which would make the rails illegible. */

@media (max-width: 900px) {
  :root {
    --u: calc(100vw / 480);

    /* A 9:16 crop of the same frame, centred on the model. Shifting the
       landscape plate with CSS cannot work: at exactly 16:9 there is no
       horizontal slack to shift into, so any offset exposes a blank edge.
       Art-directing the crop is the only honest fix. */
    --hero-ar: 0.5625;
    --jacket-x: 18%; --jacket-y: 13%; --jacket-w: 64%; --jacket-h: 41%;
    --short-x: 25%;  --short-y: 53%;  --short-w: 44%;  --short-h: 18%;
  }

  /* The garment sits somewhere else in the 9:16 crop, and has far less room,
     so the focus layer is recentred and enlarged for it. */
  .focus--jacket { --fx: 50%; --fy: 31%; --fw: 74%; }
  .focus--short  { --fx: 47%; --fy: 52%; --fw: 64%; }

  .hero { min-height: 100vh; }
  .hero::before { opacity: 1; }

  .rail { display: none; }

  .nav {
    top: calc(24 * var(--u));
    padding: 0 calc(20 * var(--u));
    font-size: calc(11 * var(--u));
    letter-spacing: calc(1 * var(--u));
  }
  .nav__links { display: none; }

  /* Centred in the viewport. The small upward bias keeps its foot clear of the
     player on short phones, where true centre and the player's top edge meet. */
  .wordmark {
    top: 50%;
    bottom: auto;
    left: calc(20 * var(--u));
    width: calc(440 * var(--u));
    transform: translateY(calc(-50% - 18 * var(--u)));
  }

  .player {
    top: auto;
    bottom: calc(96 * var(--u));
    left: calc(20 * var(--u));
    width: calc(440 * var(--u));
  }


  .sound {
    top: auto;
    bottom: calc(28 * var(--u));
    right: calc(20 * var(--u));
  }
}
