/* ==========================================================================
   King Palm Cannabis
   The pouch is the design system. Near-black field, one neon accent at a time,
   condensed caps, thin-outline spec boxes. Every colour below was sampled off
   the artwork — see .claude/context/design.md.
   ========================================================================== */

@font-face {
  font-family: 'Anton';
  src: url('assets/fonts/anton.woff2') format('woff2');
  font-weight: 400;
  font-display: swap;
}
@font-face {
  font-family: 'Barlow Condensed';
  src: url('assets/fonts/barlow-condensed-500.woff2') format('woff2');
  font-weight: 500;
  font-display: swap;
}
@font-face {
  font-family: 'Barlow Condensed';
  src: url('assets/fonts/barlow-condensed-600.woff2') format('woff2');
  font-weight: 600;
  font-display: swap;
}
@font-face {
  font-family: 'Inter';
  src: url('assets/fonts/inter.woff2') format('woff2');
  font-weight: 400 700;
  font-display: swap;
}

:root {
  --ink: #07080a;
  --ink-2: #0d0f12;
  --smoke: #16191d;
  --line: rgba(255, 255, 255, 0.13);
  --text: #f2f4f0;
  --text-dim: #8b918c;

  --kp-green: #84c03c;
  --kp-lime: #aac81e;
  --kp-violet: #a080c0;
  --kp-violet-deep: #5840a0;
  --kp-pink: #f03c78;
  --kp-amber: #a05800;
  /* The pouch amber is too dark to read as small text on black (3.9:1).
     This is the same hue lifted into AA range for UI use. */
  --kp-amber-lift: #e2a03c;
  --kp-leaf: #848430;

  /* The one variable the whole page hangs off. The scroll observer rewrites
     it as each strain takes over; everything accent-coloured follows. */
  --accent: var(--kp-green);
  --accent-soft: rgba(132, 192, 60, 0.16);

  --display: 'Anton', 'Arial Narrow', sans-serif;
  --label: 'Barlow Condensed', 'Arial Narrow', sans-serif;
  --body: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;

  --gutter: clamp(1.25rem, 4vw, 4.5rem);
  --maxw: 1360px;
  --ease: cubic-bezier(0.22, 1, 0.36, 1);
}

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

html { scroll-behavior: smooth; -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--ink);
  color: var(--text);
  font-family: var(--body);
  font-size: 16px;
  line-height: 1.6;
  overflow-x: hidden;
  /* The accent bleeds into the page background so a strain change is felt,
     not just seen on the type. */
  transition: background-color 0.9s var(--ease);
}

body.gated { overflow: hidden; }

img { max-width: 100%; display: block; }
a { color: inherit; }

::selection { background: var(--accent); color: var(--ink); }

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

/* --------------------------------------------------------------- primitives */

.eyebrow {
  font-family: var(--label);
  font-weight: 600;
  font-size: 0.75rem;
  letter-spacing: 0.34em;
  text-transform: uppercase;
  color: var(--accent);
  margin: 0;
}

.eyebrow--mute { color: var(--text-dim); }

.display {
  font-family: var(--display);
  font-weight: 400;
  text-transform: uppercase;
  line-height: 0.86;
  letter-spacing: -0.015em;
  margin: 0;
  /* Anton is drawn tight; a hair of optical padding stops descenders clipping
     when the size gets very large. */
  padding-bottom: 0.04em;
}

.lede {
  font-size: clamp(1rem, 1.6vw, 1.2rem);
  line-height: 1.65;
  color: var(--text-dim);
  max-width: 46ch;
}

/* Lifted straight off the pouch: the boxed "1 GRAM BLUNT" / "TOBACCO-FREE"
   labels. Same proportions, same hairline. */
.pill {
  display: inline-flex;
  align-items: center;
  gap: 0.5em;
  font-family: var(--label);
  font-weight: 600;
  font-size: 0.78rem;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  padding: 0.5em 1.05em 0.42em;
  border: 1px solid var(--line);
  border-radius: 2px;
  color: var(--text);
  white-space: nowrap;
}

.pill--accent {
  border-color: var(--accent);
  color: var(--accent);
}

.pill-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
}

/* Two deliberate lines rather than one wrapping run: the strain type reads on
   its own, then the format underneath it. Tighter than the body gap so the
   pair still reads as one block. */
.pill-stack {
  display: grid;
  gap: 0.6rem;
  justify-items: start;
}

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.6em;
  font-family: var(--label);
  font-weight: 600;
  font-size: 0.9rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  padding: 1.05em 2.2em 0.95em;
  border-radius: 2px;
  border: 1px solid var(--accent);
  background: var(--accent);
  color: var(--ink);
  text-decoration: none;
  cursor: pointer;
  transition: background-color 0.3s var(--ease), color 0.3s var(--ease),
    transform 0.3s var(--ease);
}

.btn:hover { transform: translateY(-2px); }

.btn--ghost {
  background: transparent;
  color: var(--text);
  border-color: var(--line);
}

.btn--ghost:hover {
  border-color: var(--accent);
  color: var(--accent);
}

.rule {
  height: 1px;
  background: var(--line);
  border: 0;
  margin: 0;
}

.section {
  padding: clamp(5rem, 12vh, 9rem) var(--gutter);
  position: relative;
}

/* The header is fixed, so an anchor jump would otherwise park the section's
   first line underneath it. */
.section,
.strain,
[id] { scroll-margin-top: 5.5rem; }

.wrap {
  max-width: var(--maxw);
  margin-inline: auto;
  width: 100%;
}

/* ---------------------------------------------------------------- age gate */

.gate {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: grid;
  place-items: center;
  padding: var(--gutter);
  background: var(--ink);
  text-align: center;
}

/* The gate lives in the markup and removes itself, rather than being injected
   by JS — with scripts blocked the content stays behind the gate, which is the
   safe failure direction. */
.gate[hidden] { display: none; }

.gate::after {
  content: '';
  position: absolute;
  inset: 0;
  background: radial-gradient(
    ellipse 60% 50% at 50% 55%,
    rgba(132, 192, 60, 0.13),
    transparent 70%
  );
  pointer-events: none;
}

.gate__inner {
  position: relative;
  z-index: 1;
  max-width: 30rem;
  display: grid;
  gap: 1.6rem;
  justify-items: center;
}

.gate__logo { width: clamp(11rem, 26vw, 15rem); }

.gate__q {
  font-size: clamp(1.6rem, 4.4vw, 2.3rem);
}

.gate__actions {
  display: flex;
  gap: 0.8rem;
  flex-wrap: wrap;
  justify-content: center;
}

.gate__fine {
  font-size: 0.78rem;
  color: var(--text-dim);
  line-height: 1.6;
  max-width: 34ch;
}

.gate__deny {
  color: var(--text-dim);
  font-size: 0.85rem;
}

/* ------------------------------------------------------------------- header */

.hdr {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 1.15rem var(--gutter);
  transition: background-color 0.4s var(--ease), backdrop-filter 0.4s var(--ease),
    border-color 0.4s var(--ease);
  border-bottom: 1px solid transparent;
}

.hdr.is-stuck {
  background: rgba(7, 8, 10, 0.82);
  backdrop-filter: blur(14px) saturate(140%);
  border-bottom-color: var(--line);
}

.hdr__logo { width: clamp(6.5rem, 11vw, 8.5rem); }

.hdr__nav {
  display: flex;
  gap: 1.9rem;
  font-family: var(--label);
  font-weight: 600;
  font-size: 0.82rem;
  letter-spacing: 0.19em;
  text-transform: uppercase;
}

.hdr__nav a {
  text-decoration: none;
  color: var(--text-dim);
  transition: color 0.25s var(--ease);
}

.hdr__nav a:hover { color: var(--accent); }

.hdr__age {
  font-family: var(--label);
  font-weight: 600;
  font-size: 0.75rem;
  letter-spacing: 0.2em;
  color: var(--text-dim);
}

/* Below the nav breakpoint the bar holds one thing, so left-aligning it just
   left a wide empty strip and a tall band over the artwork. Centre the mark and
   take the bar down to the height it actually needs. */
@media (max-width: 860px) {
  .hdr__nav,
  .hdr__age { display: none; }

  .hdr {
    justify-content: center;
    padding: 0.6rem var(--gutter);
  }

  .hdr__logo { width: 5.4rem; }
}

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

/* The hero is a scroll track; .hero__pin is the screen you actually look at.
   `--x` is the expansion progress, 0 at the top of the track and 1 when the
   roll has opened all the way across. Everything below reads that one value. */
.hero {
  position: relative;
  min-height: 100svh;
}

/* One extra screen of scroll opens it. 250svh left a long dead tail once the
   roll had finished moving. */
.js .hero { min-height: 205svh; }

.hero__pin {
  position: relative;
  min-height: 100svh;
  display: grid;
  place-items: center;
  padding: 7rem var(--gutter) 3rem;
  overflow: hidden;
  text-align: center;
}

.js .hero__pin {
  position: sticky;
  top: 0;
  height: 100svh;
}

/* The bloom behind the roll. Same job as the lightning field on the pouch:
   give the product something to stand in front of. */
.hero__glow {
  position: absolute;
  left: 50%;
  top: 52%;
  width: min(115vw, 70rem);
  aspect-ratio: 1;
  translate: -50% -50%;
  background: radial-gradient(
    circle at 50% 50%,
    var(--accent-soft),
    transparent 62%
  );
  pointer-events: none;
}

.hero__grid {
  position: absolute;
  inset: 0;
  background-image: linear-gradient(var(--line) 1px, transparent 1px),
    linear-gradient(90deg, var(--line) 1px, transparent 1px);
  background-size: 8rem 8rem;
  opacity: 0.25;
  mask-image: radial-gradient(ellipse 70% 60% at 50% 45%, #000, transparent 75%);
  pointer-events: none;
}

.hero__inner {
  position: relative;
  z-index: 2;
  display: grid;
  justify-items: center;
  gap: 1.6rem;
  width: 100%;
  max-width: var(--maxw);
}

/* The roll is very tall and very narrow (570 × 2761), which is the whole
   reason this composition works: stand it in the middle at full height and the
   headline still reads on both sides of it. Type behind, product in front,
   one image rather than a picture with a caption underneath. */
.hero__stage {
  position: relative;
  display: grid;
  place-items: center;
  width: 100%;
  /* Bounded so the composition can never push the spec pills off-screen —
     the failure the first cut had. One variable, used by the box AND the roll:
     `height: 100%` on the image cannot work here, because a centred grid item
     sits in an auto-sized row and the percentage resolves to auto, which is the
     image's intrinsic 2761px. */
  --stage-h: min(62svh, 36rem);
  height: var(--stage-h);
}

/* The two lines are left-aligned inside a centred block, not centred
   individually. That pulls the short word ("REAL") away from the middle so the
   roll passes to the right of it instead of deleting a letter out of it. */
.hero__title {
  position: absolute;
  inset: 0;
  display: grid;
  place-content: center;
  justify-items: start;
  text-align: left;
  font-size: clamp(2.6rem, 12.5vw, 10.5rem);
  z-index: 1;
  pointer-events: none;
  /* Sits behind the product, so it needs to read as behind. */
  text-shadow: 0 0 2.5rem rgba(7, 8, 10, 0.9);
}

.hero__title em {
  font-style: normal;
  color: var(--accent);
  display: block;
}

.hero__roll {
  position: relative;
  z-index: 2;
  height: var(--stage-h);
  width: auto;
  /* Nudged off dead-centre so it lands in the gap between GREEN and LEAF.
     The offset is in vw because the headline is sized in vw — the two scale
     together, so the roll stays in the word gap at any desktop width. */
  translate: 3.4vw 0;
  filter: drop-shadow(0 1.5rem 3rem rgba(0, 0, 0, 0.9))
    drop-shadow(0 0 5rem var(--accent-soft));
}

/* Layering the roll over the headline needs horizontal room the phone doesn't
   have — at 393px the roll is 109px wide and "REAL" is about 130px, so the
   product very nearly erases the word. Stack instead of overlap. */
@media (max-width: 900px) {
  .hero__stage {
    height: auto;
    grid-auto-flow: row;
    gap: 0.8rem;
  }
  .hero__title {
    position: static;
    justify-items: center;
    text-align: center;
    text-shadow: none;
  }
  .hero__roll {
    translate: 0 0;
    height: min(40svh, 21rem);
    /* --grow is NOT set here. It lives with the transform further down, next
       to its own breakpoint override — declaring it in this block put it
       before the default, same specificity, so the default silently won. */
  }

  /* Less travel, because the words are wider relative to the screen and would
     otherwise cross each other before they cleared. */
  .hero__w1 { transform: translateX(calc(var(--x, 0) * -20vw)); }
  .hero__w2 { transform: translateX(calc(var(--x, 0) * 20vw)); }

  .js .hero { min-height: 190svh; }
}

.hero__meta {
  position: absolute;
  bottom: 1.6rem;
  left: var(--gutter);
  right: var(--gutter);
  z-index: 4;
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 1rem;
  font-family: var(--label);
  font-weight: 600;
  font-size: 0.72rem;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--text-dim);
}

.hero__scroll {
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
  text-decoration: none;
  color: var(--text-dim);
  transition: color 0.25s var(--ease);
}

.hero__scroll:hover { color: var(--accent); }

.hero__scroll span[aria-hidden] {
  display: inline-block;
  animation: nudge 2.4s var(--ease) infinite;
}

@keyframes nudge {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(4px); }
}

@media (max-width: 700px) {
  .hero__meta {
    flex-direction: column;
    align-items: center;
    text-align: center;
    gap: 0.7rem;
  }
  .hero__meta > :last-child { order: -1; }
}

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

.band {
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
  overflow: hidden;
  padding: 1.05rem 0;
  background: var(--ink-2);
}

.band__track {
  display: flex;
  gap: 3.5rem;
  width: max-content;
  /* Base drift. JS layers scroll velocity on top via --band-speed, and flips
     --band-dir when you scroll up, so the strip reacts to the page. */
  animation: slide calc(38s / var(--band-speed, 1)) linear infinite;
  animation-direction: var(--band-dir, normal);
  font-family: var(--label);
  font-weight: 600;
  font-size: 0.85rem;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: var(--text-dim);
}

.band__track span { display: flex; align-items: center; gap: 3.5rem; }
.band__track span::after {
  content: '';
  width: 5px;
  height: 5px;
  background: var(--accent);
  border-radius: 50%;
  flex: none;
}

@keyframes slide {
  to { transform: translateX(-50%); }
}

/* ------------------------------------------------------------------ the leaf */

.split {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: clamp(2.5rem, 7vw, 6rem);
  align-items: center;
}

.split__media {
  position: relative;
  display: grid;
  place-items: center;
  min-height: 24rem;
}

.split__media::before {
  content: '';
  position: absolute;
  width: 78%;
  aspect-ratio: 1;
  border-radius: 50%;
  background: radial-gradient(circle, var(--accent-soft), transparent 65%);
}

.split__media img {
  position: relative;
  max-height: clamp(20rem, 52vh, 34rem);
  width: auto;
  filter: drop-shadow(0 1.5rem 3rem rgba(0, 0, 0, 0.7));
}

.split__body { display: grid; gap: 1.5rem; align-content: start; }

/* 0.86 is right for the hero, where the lines are plain caps. Section titles
   carry apostrophes and full stops that collide with the line below at that
   leading, so they get a little more room. */
.split__title {
  font-size: clamp(2.4rem, 5.2vw, 4.2rem);
  line-height: 0.98;
}

.strain__name,
.find__title { line-height: 0.94; }

@media (max-width: 900px) {
  .split { grid-template-columns: 1fr; }
  .split__media { min-height: 18rem; order: -1; }
}

/* Spec strip — the numbers, set like instrument readouts. */
/* Fixed column counts, not auto-fit. With auto-fit the four cells reflowed to
   three-plus-one and the leftover track showed the 1px gap colour as a solid
   grey block — an empty cell that looked like a broken tile. */
.specs {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 1px;
  background: var(--line);
  border: 1px solid var(--line);
  border-radius: 3px;
  overflow: hidden;
}

@media (max-width: 620px) {
  .specs { grid-template-columns: repeat(2, 1fr); }
}

.spec {
  background: var(--ink);
  padding: 1.4rem 1.3rem;
  display: grid;
  gap: 0.35rem;
}

.spec__k {
  font-family: var(--display);
  font-size: clamp(1.6rem, 3vw, 2.3rem);
  color: var(--accent);
  line-height: 1;
}

.spec__v {
  font-family: var(--label);
  font-weight: 600;
  font-size: 0.74rem;
  letter-spacing: 0.19em;
  text-transform: uppercase;
  color: var(--text-dim);
}

/* ------------------------------------------------------------- how it's made */

.steps {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: 1.5rem;
  counter-reset: step;
}

.step {
  border: 1px solid var(--line);
  border-radius: 3px;
  padding: 1.8rem 1.6rem;
  display: grid;
  gap: 0.7rem;
  align-content: start;
  background: linear-gradient(180deg, var(--ink-2), var(--ink));
  transition: border-color 0.35s var(--ease), transform 0.35s var(--ease);
}

.step:hover {
  border-color: var(--accent);
  transform: translateY(-3px);
}

.step::before {
  counter-increment: step;
  content: '0' counter(step);
  font-family: var(--display);
  font-size: 1.6rem;
  color: var(--accent);
  line-height: 1;
}

.step h3 {
  font-family: var(--label);
  font-weight: 600;
  font-size: 1.05rem;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  margin: 0;
}

.step p {
  margin: 0;
  color: var(--text-dim);
  font-size: 0.94rem;
  line-height: 1.62;
}

/* Solventless vs solvent — the comparison that carries the whole claim. */
.versus {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1.5rem;
  margin-top: 2.5rem;
}

.versus__card {
  border: 1px solid var(--line);
  border-radius: 3px;
  padding: 1.9rem 1.7rem;
  display: grid;
  gap: 1rem;
  align-content: start;
}

.versus__card--ours { border-color: var(--accent); background: var(--accent-soft); }

.versus__card h3 {
  font-family: var(--label);
  font-weight: 600;
  font-size: 0.85rem;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  margin: 0;
  color: var(--text-dim);
}

.versus__card--ours h3 { color: var(--accent); }

.versus__lead {
  font-family: var(--display);
  font-size: clamp(1.5rem, 3vw, 2.1rem);
  text-transform: uppercase;
  line-height: 1;
  margin: 0;
}

.versus__card ul {
  margin: 0;
  padding: 0;
  list-style: none;
  display: grid;
  gap: 0.6rem;
  font-size: 0.94rem;
  color: var(--text-dim);
}

.versus__card li {
  display: grid;
  grid-template-columns: 1.1rem 1fr;
  gap: 0.6rem;
  align-items: start;
}

.versus__card li::before {
  content: '—';
  color: var(--text-dim);
}

.versus__card--ours li::before { content: '+'; color: var(--accent); }

@media (max-width: 760px) {
  .versus { grid-template-columns: 1fr; }
}

/* ---------------------------------------------------------------- strains */

/* One screen per strain. This used to be a 210svh track with a pinned stage,
   scrubbing the pack through four renders as you pushed through it — which
   cost roughly six screens of scrolling across the three strains and read as
   the page refusing to move. The pack angles are now on the thumbnails.
   See the note in app.js. */
.strain {
  position: relative;
  min-height: 100svh;
}

/* The divider. Three strains in a row, each repainting the page in its own
   colour, ran together — you could not tell where one ended. This is a hard
   stop in the strain's own accent, brightest in the middle and falling off to
   nothing at the edges so it reads as a seam rather than a table border. */
.strain::before {
  content: '';
  position: absolute;
  inset-inline: 0;
  top: 0;
  height: 1px;
  background: linear-gradient(
    90deg,
    transparent,
    var(--line) 12%,
    var(--accent) 38%,
    var(--accent) 62%,
    var(--line) 88%,
    transparent
  );
  opacity: 0.9;
}

/* A short bloom under the line, so the seam registers at a glance on a page
   this dark without the line itself getting heavy enough to read as a border. */
.strain::after {
  content: '';
  position: absolute;
  inset-inline: 0;
  top: 0;
  height: 8rem;
  background: linear-gradient(
    180deg,
    var(--accent-soft),
    transparent
  );
  opacity: 0.5;
  pointer-events: none;
}

.strain__glow {
  position: absolute;
  inset: 0;
  background: radial-gradient(
    ellipse 55% 60% at 28% 50%,
    var(--accent-soft),
    transparent 68%
  );
  pointer-events: none;
}

.strain__wrap {
  position: relative;
  z-index: 1;
  max-width: var(--maxw);
  margin-inline: auto;
  width: 100%;
  display: grid;
  grid-template-columns: 1.05fr 0.95fr;
  gap: clamp(2rem, 6vw, 5rem);
  align-items: center;
  align-content: center;
  padding: clamp(5rem, 12vh, 8rem) var(--gutter);
  min-height: 100svh;
}

.strain__body { display: grid; gap: 1.4rem; align-content: center; }

.strain__index {
  font-family: var(--label);
  font-weight: 600;
  font-size: 0.75rem;
  letter-spacing: 0.28em;
  color: var(--text-dim);
}

.strain__name {
  font-size: clamp(2.4rem, 6vw, 5.2rem);
}

.strain__name em {
  font-style: normal;
  display: block;
  color: var(--accent);
}

.strain__x {
  display: block;
  font-family: var(--label);
  font-weight: 500;
  font-size: 0.5em;
  letter-spacing: 0.3em;
  color: var(--text-dim);
  padding: 0.18em 0;
}

.strain__stage {
  position: relative;
  display: grid;
  place-items: center;
  min-height: clamp(20rem, 58vh, 38rem);
}

.strain__pack {
  max-height: clamp(20rem, 58vh, 38rem);
  width: auto;
  filter: drop-shadow(0 2rem 4rem rgba(0, 0, 0, 0.8));
  transition: opacity 0.35s var(--ease);
}

.strain__pack.is-swapping { opacity: 0; }

.strain__angles {
  display: flex;
  gap: 0.6rem;
  flex-wrap: wrap;
  margin-top: 1.4rem;
  justify-content: center;
}

.angle {
  width: 3.6rem;
  height: 4.4rem;
  padding: 0.25rem;
  border: 1px solid var(--line);
  border-radius: 3px;
  background: var(--ink-2);
  cursor: pointer;
  display: grid;
  place-items: center;
  transition: border-color 0.25s var(--ease);
}

/* Same reason as .format__media img — explicit, not percentage. */
.angle img { height: 3.9rem; width: 100%; object-fit: contain; }
.angle:hover { border-color: var(--accent); }
.angle[aria-current='true'] { border-color: var(--accent); }

@media (max-width: 940px) {
  .strain__wrap { grid-template-columns: 1fr; }
  .strain__stage { order: -1; min-height: 18rem; }
  .strain__pack { max-height: 20rem; }
  .strain__body { text-align: center; justify-items: center; }
  .strain__body .pill-row { justify-content: center; }
  .strain__body .pill-stack { justify-items: center; }
}

/* ---------------------------------------------------------------- formats */

.formats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr));
  gap: 1.5rem;
}

.format {
  border: 1px solid var(--line);
  border-radius: 3px;
  padding: 2rem 1.7rem;
  display: grid;
  gap: 1.1rem;
  align-content: start;
  background: linear-gradient(180deg, var(--ink-2), var(--ink));
  transition: border-color 0.35s var(--ease);
}

.format:hover { border-color: var(--accent); }

.format__media {
  /* Explicit height on BOTH the box and the image. `max-height: 100%` on a
     centred grid item resolves to none — the row is auto-sized, so the
     percentage has nothing definite to resolve against and the image falls
     back to its intrinsic height. Here that overflowed a 240px box to 714px. */
  --media-h: 15rem;
  height: var(--media-h);
  display: grid;
  place-items: center;
  position: relative;
}

.format__media::before {
  content: '';
  position: absolute;
  width: 80%;
  aspect-ratio: 1;
  border-radius: 50%;
  background: radial-gradient(circle, var(--accent-soft), transparent 65%);
}

.format__media img {
  position: relative;
  height: var(--media-h);
  width: 100%;
  object-fit: contain;
}

.format h3 {
  font-family: var(--display);
  font-size: 1.7rem;
  text-transform: uppercase;
  margin: 0;
  line-height: 1;
}

.format p {
  margin: 0;
  color: var(--text-dim);
  font-size: 0.94rem;
}

/* ------------------------------------------------------------------ locator */

.find {
  position: relative;
  text-align: center;
  border-top: 1px solid var(--line);
  overflow: hidden;
}

.find__glow {
  position: absolute;
  inset: 0;
  background: radial-gradient(ellipse 60% 70% at 50% 100%, var(--accent-soft), transparent 65%);
  pointer-events: none;
}

.find__inner {
  position: relative;
  z-index: 1;
  display: grid;
  gap: 1.5rem;
  justify-items: center;
  max-width: 46rem;
  margin-inline: auto;
}

.find__title { font-size: clamp(2.4rem, 6.5vw, 5rem); }

.finder {
  display: flex;
  gap: 0.6rem;
  width: 100%;
  max-width: 30rem;
  flex-wrap: wrap;
}

.finder input {
  flex: 1 1 12rem;
  background: var(--ink-2);
  border: 1px solid var(--line);
  border-radius: 2px;
  color: var(--text);
  font-family: var(--label);
  font-weight: 600;
  font-size: 0.95rem;
  letter-spacing: 0.12em;
  padding: 1.05em 1.1em 0.95em;
}

.finder input::placeholder { color: var(--text-dim); letter-spacing: 0.16em; }
.finder input:focus { border-color: var(--accent); outline: none; }
.finder .btn { flex: 0 0 auto; }

.finder__note {
  font-size: 0.82rem;
  color: var(--text-dim);
  min-height: 1.4em;
  margin: 0;
}

/* ------------------------------------------------------------------- footer */

.ftr {
  border-top: 1px solid var(--line);
  padding: clamp(3.5rem, 8vh, 5rem) var(--gutter) 2.5rem;
  background: var(--ink-2);
}

.ftr__top {
  display: grid;
  grid-template-columns: 1.2fr 1fr;
  gap: clamp(2rem, 6vw, 5rem);
  align-items: start;
  max-width: var(--maxw);
  margin-inline: auto;
}

.ftr__logo { width: 9rem; margin-bottom: 1.2rem; }

.ftr__links {
  display: flex;
  gap: 1.6rem;
  flex-wrap: wrap;
  font-family: var(--label);
  font-weight: 600;
  font-size: 0.8rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
}

.ftr__links a { color: var(--text-dim); text-decoration: none; }
.ftr__links a:hover { color: var(--accent); }

/* The legally required block. Deliberately not styled down to invisibility —
   small, but the same contrast as any other body copy. */
.warn {
  max-width: var(--maxw);
  margin: 2.6rem auto 0;
  padding-top: 2rem;
  border-top: 1px solid var(--line);
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 1.4rem;
  align-items: start;
}

.warn__sym {
  width: 3.6rem;
  flex: none;
}

.warn p {
  margin: 0 0 0.7rem;
  font-size: 0.79rem;
  line-height: 1.65;
  color: var(--text-dim);
}

.warn strong { color: var(--text); font-weight: 600; }

.warn__todo {
  color: var(--kp-amber-lift);
  border: 1px dashed currentColor;
  padding: 0.15em 0.5em;
  border-radius: 2px;
  font-size: 0.95em;
}

.ftr__base {
  max-width: var(--maxw);
  margin: 2rem auto 0;
  padding-top: 1.6rem;
  border-top: 1px solid var(--line);
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
  font-family: var(--label);
  font-weight: 500;
  font-size: 0.75rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--text-dim);
}

@media (max-width: 800px) {
  .ftr__top { grid-template-columns: 1fr; }
  .warn { grid-template-columns: 1fr; }
}

/* ------------------------------------------------------- scroll progress */

.rail {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 2px;
  z-index: 120;
  background: rgba(255, 255, 255, 0.07);
  pointer-events: none;
}

.rail__fill {
  height: 100%;
  width: 100%;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: 0 50%;
  /* scaleX rather than width: width would relayout on every scroll frame. */
  will-change: transform;
  box-shadow: 0 0 12px var(--accent);
}

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

/* The two halves of the headline are pushed apart to open a gap, and the roll
   turns flat and grows into it. Rotating is what makes the scale honest: the
   roll's long axis is 2761px of real pixels, so laying it horizontal and
   spanning the viewport is a DOWNSCALE. Growing it while upright would have
   been a 2.5x upscale of a 570px-wide image, and it would look it. */

.hero__w1,
.hero__w2 {
  display: block;
  will-change: transform;
}

.hero__w1 { transform: translateX(calc(var(--x, 0) * -30vw)); }
.hero__w2 {
  font-style: normal;
  color: var(--accent);
  transform: translateX(calc(var(--x, 0) * 30vw));
}

/* The words must be gone BEFORE they reach the edge. At 42vw they were still
   near-opaque when the viewport cut them mid-letter, which read as a bug
   rather than an exit. Fade runs 0.35 → 0.75, comfortably ahead of the crop. */
.hero__title { opacity: calc(1 - max(0, var(--x, 0) - 0.35) * 2.5); }

.hero__roll {
  /* 94deg, not 90: the roll is photographed with a slight natural lean, so a
     true 90 leaves it looking a few degrees short of level.

     --grow is per-breakpoint because the rotation alone already converts the
     roll's long axis into screen width. On a 393px phone the desktop 1.55
     scaled a 336px roll to 857px — two viewports wide, so you saw a fragment
     of leaf and never the object. */
  --grow: 1.55;
  transform: rotate(calc(var(--x, 0) * 94deg)) scale(calc(1 + var(--x, 0) * var(--grow)));
  will-change: transform;
}

/* Kept immediately after the default on purpose. Both selectors are a single
   class, so the cascade is decided by source order alone — this override has
   to come last or it does nothing at all. */
@media (max-width: 900px) {
  .hero__roll { --grow: 0.16; }
}

/* The field behind it opens up as the roll takes over. */
.hero__grid {
  transform: scale(calc(1 + var(--x, 0) * 0.35));
  opacity: calc(1 - var(--x, 0) * 1.4);
}

.hero__glow { transform: scale(calc(1 + var(--x, 0) * 0.6)); }

.hero .eyebrow { opacity: calc(1 - var(--x, 0) * 2.4); }

.hero__meta { opacity: calc(1 - var(--x, 0) * 2.4); }

/* Swap the hint once the roll is open — "scroll to open" stops being true. */
.hero__scroll-t::after {
  content: '';
}

/* ------------------------------------------------------------------ reveals */

.reveal {
  opacity: 0;
  transform: translateY(22px);
  transition: opacity 0.8s var(--ease), transform 0.8s var(--ease);
  /* Set per element by the observer so siblings arrive in sequence rather
     than as one block. */
  transition-delay: var(--delay, 0ms);
}

.reveal.is-in { opacity: 1; transform: none; }

/* Headlines rise out from behind their own baseline instead of fading.

   The clip lives on an inner span, NOT on the observed element. Clipping the
   observed element to zero height shrinks its intersection rect to nothing, so
   IntersectionObserver never reports it as visible, so the class that would
   unclip it is never added — the element stays hidden because it is hidden.
   That silently removed three section headlines from the page. */
.reveal--wipe {
  opacity: 1;
  transform: none;
}

.reveal--wipe > .wipe {
  display: block;
  clip-path: inset(0 0 105% 0);
  transition: clip-path 1s var(--ease);
  transition-delay: var(--delay, 0ms);
}

/* Negative insets on the open state so glow and descenders aren't cropped. */
.reveal--wipe.is-in > .wipe { clip-path: inset(-30% 0 -30% 0); }

/* Cards in a row arrive left to right. */
.steps .step,
.formats .format,
.versus .versus__card { --delay: calc(var(--n, 0) * 90ms); }

.specs .spec { --delay: calc(var(--n, 0) * 70ms); }

/* Spec digits roll up as they count. */
.spec__k { font-variant-numeric: tabular-nums; }

/* Motion is decoration here, never the only channel — with it off, everything
   is simply already in place. */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    transition-delay: 0ms !important;
  }
  .reveal,
  .reveal--wipe,
  .reveal--wipe > .wipe { opacity: 1; transform: none; clip-path: none; }

  /* Nothing may depend on scroll position for legibility. The hero sits still.
     The strains need nothing here any more — they are single screens with
     click-driven angles for everyone. */
  .js .hero { min-height: 100svh; }
  .js .hero__pin { position: relative; height: auto; }
  .hero__inner,
  .hero__roll,
  .hero__grid,
  .hero__glow,
  .hero__meta,
  .hero__title,
  .hero__w1,
  .hero__w2,
  .hero .eyebrow { transform: none; opacity: 1; }

}

/* No-JS: nothing may be left hidden by a class the observer never removes. */
.no-js .reveal,
.no-js .reveal--wipe,
.no-js .reveal--wipe > .wipe { opacity: 1; transform: none; clip-path: none; }
