/* ============================================================
   VITRINE — base
   ============================================================ */

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  /* The clip has to be on the root, not just body. The page deliberately bleeds
     elements past the viewport edge, and the hero's light cone is wider than a
     phone; on mobile that widens the initial containing block rather than being
     clipped, which is what pushed the fixed top bar off-screen. */
  overflow-x: clip;
}
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

body {
  margin: 0;
  background: var(--void);
  color: var(--dust);
  font-family: var(--sans);
  font-size: var(--t-body);
  font-weight: 400;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow-x: clip;
}

img, video, canvas { display: block; max-width: 100%; }
img { height: auto; }

a { color: inherit; text-decoration: none; }

/* The graffiti ground. Used as ONE cover-sized layer rather than a repeating
   tile, so there is no seam to hide, and held right down in opacity: the print
   on the product has to stay the loudest thing on the page, and a background
   that competes with it would flatten the whole vitrine. */
.ground {
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background: url("../assets/img/graffiti-ground.webp") center / cover no-repeat;
  /* No opacity dial. The stroke value is baked into the asset at #3E3E44 so the
     contrast against text is a known quantity rather than something that drifts
     every time this number is nudged. Legibility of a pattern comes from edge
     contrast against its own ground, not from brightness. */
  opacity: 1;
  /* Ease it off at the very top and bottom so the fixed bar and the footer rule
     do not have to fight it. */
  mask-image: linear-gradient(to bottom,
              transparent 0%, #000 9%, #000 92%, transparent 100%);

  /* ABOVE the shader field, not below it. The field's WebGL context is created
     with alpha:false, so it paints fully opaque — sitting underneath it, this
     layer was invisible no matter what opacity it was given, which is why the
     pattern read as "not legible". Screen blending puts it back on top while
     still letting the field's moving light show through the strokes, which is
     the effect the underneath placement was reaching for in the first place. */
  z-index: 1;
  mix-blend-mode: screen;
}

/* Small and secondary text carries a soft halo of the page ground, so wherever
   a glyph lands on top of a graffiti stroke it is still sitting on near-void.
   Without this the 11px placards drop to 3.1:1 against a stroke, which fails
   AA; with it they stay at the 11:1 they have over the bare background. The
   display type is heavy enough not to need it. */
.placard,
.lede,
.label__dl,
.spec__list li,
.ev figcaption,
.why__q,
.cta__note,
.strip__list span {
  text-shadow: 0 0 10px var(--void), 0 0 4px var(--void), 0 0 2px var(--void);
}

/* ---- the two background canvases: the motion signature ---- */
.bg-layer {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 0;
  pointer-events: none;
}
#motes { z-index: 2; }

main, .topbar, .foot { position: relative; z-index: 3; }

/* ---- the placard voice: 11px mono, uppercase. The whole small end of the
   type system. There is deliberately nothing between this and the display. --- */
.placard {
  font-family: var(--mono);
  font-size: var(--t-placard);
  font-weight: 400;
  letter-spacing: var(--ls-placard);
  text-transform: uppercase;
  color: var(--crete);
  margin: 0;
  line-height: 1.5;
}
.placard .sep { color: var(--rule-lit); padding: 0 .15em; }
.placard.tiny { font-size: 10px; }
.placard.link { color: var(--dust); transition: color var(--dur-fast) var(--ease-out); }
.placard.link:hover, .placard.link:focus-visible { color: var(--hazard); }

/* ---- the display voice: variable weight AND width, both animated ---- */
.kinetic {
  font-family: var(--sans);
  font-size: var(--t-display);
  line-height: .88;
  letter-spacing: var(--ls-display);
  color: var(--paper);
  margin: 0;
  font-weight: 800;
  font-stretch: 112%;
  text-wrap: balance;
}
.kinetic--lg { font-size: var(--t-display-lg); }
/* The brand's own grid sets its headlines in heavy condensed uppercase, so the
   big statements do the same: tighter width, maximum weight, stacked short. */
.kinetic--stack,
.kinetic--lg {
  text-transform: uppercase;
  line-height: .82;
  letter-spacing: -.018em;
}
/* Condensed, because the reveal masks each line with overflow:hidden and a
   wide face would be clipped by its own column. wdth 78 is also closer to the
   compressed grotesk the brand's grid actually uses. */
.kinetic--stack { font-size: clamp(2.4rem, 6.2vw, 5.8rem); }

.kinetic--sm { font-size: clamp(2.5rem, 5vw, 5rem); }
.kinetic .line { display: block; overflow: hidden; }
.kinetic .w {
  display: inline-block;
  font-variation-settings: "wght" var(--w, 800), "wdth" var(--wd, 112);
  transform: translate3d(0, var(--y, 0), 0);
  will-change: transform, font-variation-settings;
}
.kinetic .w.hz { color: var(--hazard); }

/* Pre-reveal state. The entry animates weight and width, never opacity alone —
   that rule is in the contract because an opacity fade is the default nobody
   remembers. */
.kinetic[data-kinetic] .w {
  --w: 150;
  --wd: 62;
  --y: 105%;
}
.kinetic.is-in .w {
  --w: 900;
  --wd: 108;
  --y: 0%;
  transition:
    font-variation-settings 1150ms var(--ease-out) var(--d, 0ms),
    transform 980ms var(--ease-out) var(--d, 0ms);
}
@media (prefers-reduced-motion: reduce) {
  .kinetic[data-kinetic] .w { --w: 900; --wd: 108; --y: 0%; }
  .kinetic.is-in .w { transition: none; }
}

/* ---- staged reveals ---- */
[data-reveal] { opacity: 0; transform: translate3d(0, 26px, 0); }
[data-reveal].is-in {
  opacity: 1;
  transform: none;
  transition: opacity 900ms var(--ease-out), transform 1000ms var(--ease-out);
}
@media (prefers-reduced-motion: reduce) {
  [data-reveal] { opacity: 1; transform: none; }
}

/* ---- the bleed rule: every section pushes exactly one element off an edge --- */
.bleed-right { margin-right: calc(var(--edge) * -1 - 4vw); }
.bleed-left { margin-left: calc(var(--edge) * -1 - 4vw); }

/* ---- chrome ---- */
.skip {
  position: absolute; left: -9999px; top: 0; z-index: 99;
  background: var(--hazard); color: var(--void);
  padding: 10px 16px; font-family: var(--mono); font-size: 12px;
}
.skip:focus { left: 8px; top: 8px; }

.topbar {
  position: fixed; top: 0; left: 0; right: 0; z-index: 20;
  display: flex; align-items: center; justify-content: space-between;
  gap: 16px;
  padding: 18px var(--edge);
  mix-blend-mode: difference;
}
.topbar .placard { white-space: nowrap; }

/* The bar blends in difference mode and carries no ground of its own, which is
   the whole reason it reads as part of the page rather than as chrome bolted
   over it. The cost is that it has nothing to sit on: once the page scrolls,
   it prints straight through live copy — measured on the audit captures it was
   overprinting readable text on six of fifteen frames, worst at the top of the
   hero where it landed inside the lede.

   Giving it a background would fix the collision and kill the effect. Tucking
   it away keeps both: it is present at the top of the page and whenever the
   reader moves back up — the two moments a nav is actually wanted — and gets
   out of the way while they are reading downward. The buy link it carries is
   never more than one upward scroll from reach, and the hero's own BUY NOW and
   the closing CTA are both still in the page. */
.topbar {
  transition: transform var(--dur-mid, 420ms) var(--ease-out),
              opacity var(--dur-mid, 420ms) var(--ease-out);
}
.topbar.is-tucked {
  transform: translateY(-115%);
  opacity: 0;
}
@media (prefers-reduced-motion: reduce) {
  .topbar { transition: none; }
}
/* On a narrow viewport the two ends of the bar collide and the buy link gets
   clipped off the right edge. The city is the expendable half. */
@media (max-width: 560px) {
  /* Hide the whole maker line rather than an inner span: the content layer may
     rebuild that line from JSON and would take any inner hook with it. The
     brand name is in the page a screenful below; the buy link is not. */
  .topbar > .placard:first-child { display: none; }
  .topbar { padding-inline: clamp(14px, 4vw, 20px); justify-content: flex-end; }
}

.foot {
  display: flex; flex-wrap: wrap; gap: 12px 28px;
  align-items: baseline; justify-content: space-between;
  padding: 40px var(--edge) 48px;
  border-top: 1px solid var(--rule);
}

:focus-visible {
  outline: 2px solid var(--hazard);
  outline-offset: 3px;
}

/* The custom cursor hides the native one only where it is actually running. */
body.has-cursor,
body.has-cursor a,
body.has-cursor button { cursor: none; }

/* The stacked statements run condensed. This sits at the end of the file on
   purpose: it ties with `.kinetic.is-in .w` on specificity, so source order is
   what decides, and it has to come last to win. */
.kinetic--stack.is-in .w,
.kinetic--lg.is-in .w { --wd: 76; --w: 900; }
.kinetic--stack .line,
.kinetic--lg .line { padding-right: .06em; }
