/* YourrProxyy — landing page
   Black on white, Arial, all caps. Mobile-first.
   No color, no radii, no shadows, no gradients, no images. */

:root {
  --ink: #000;
  --paper: #fff;
  --rail-max: 1280px;
  --gutter: 1.25rem;
  --hair: 1px;
  --step: 1.5rem;
  --dur: 120ms;
  /* Social icons sit back from the black. #8a8a8a holds ~3.5:1 on white,
     clearing the 3:1 WCAG 1.4.11 floor for meaningful non-text graphics.
     Anything lighter reads as decoration and fails. They go full black on
     hover and focus. */
  --muted: #8a8a8a;

  /* Background texture. Measured, not guessed.

     Contrast is not the binding limit here — black text stays above 4.5:1 all
     the way down to a mid-grey ground, so the real constraint is that the page
     must still read as stark black-on-white.

     bg-1 and bg-2 are stored inverted (negated at encode time, not in CSS —
     a CSS invert would be undone by the .is-invert glitch class). That flips
     them from dark-dominant to light-dominant, which changes how much ink each
     one can put on the page. Measured mean luminance of the shipped files:

       bg-1  mean 191  ->  64 levels of ink available
       bg-2  mean 211  ->  44 levels   <- lightest, inherently the softest
       bg-3  mean 131  -> 124 levels   <- not inverted, nearly 3x bg-2

     So a single opacity cannot serve all three: the value that makes bg-2
     visible would make bg-3 turn the page grey. Each clip gets its own.

     Measured again on the rendered page with each clip pinned alone:

       bg-1  median 255, detail down to 157  -> white page, dark wisps
       bg-2  median 252, detail down to 170  -> white page, dark filigree
       bg-3  median 197 at opacity 0.55      -> greys the WHOLE page

     bg-3 is the only clip still un-inverted, so it alone carries a grey wash
     while the other two stay white with dark detail. At 0.55 a swap into it
     read as the lights dimming rather than the texture changing, so it is held
     well below the other two.

     bg-4 is inverted like bg-1 and bg-2 and measures mean 174 -> 81 levels of
     ink at full opacity, slightly heavier than bg-1's 73. Matching the means
     predicted 0.90, but the mean is the wrong instrument for this clip: it is
     bokeh, so its ink is concentrated in a few large soft masses rather than
     spread evenly, and 0.90 rendered the page at 188 — darker than bg-3, the
     clip these notes already flag as the heavy one. Measured on the rendered
     page instead, against the other three pinned alone:

       0.90 -> 188   0.85 -> 190   0.80 -> 192   0.75 -> 194   0.70 -> 194

     0.70 lands at 194 against bg-1's 195, so the two swap without the page
     changing weight.

     An earlier candidate was rejected here: a montage clip that measured fine
     on paper but rendered as a flat grey field with legible text in it. The
     number to watch is not the mean alone — these three all keep a white page
     with dark detail on it, and anything that fills the frame edge to edge
     fails no matter what opacity it is held at. */
  --bg-op-1: 1;
  --bg-op-2: 1;
  --bg-op-3: 0.34;
  --bg-op-4: 0.70;
  /* 12px, not more. Tested against a blur ladder on all three clips: at 24px
     they start converging and by 34px all three render as the same flat grey
     field, so a swap changes nothing on screen and the glitch reads as broken.
     12px still unmistakably reads as a gaussian blur while keeping each clip
     recognisably itself and its motion legible. */
  --bg-blur: 12px;
  /* Scrim alpha. The video sits behind this, never multiplied with the text,
     so this sets a hard floor on how dark the page can ever get. Lowered from
     0.44 to let the two inverted (lighter) clips register at all; the floor is
     re-verified against the rendered pixels below 4.5:1 cannot be reached. */
  --bg-scrim: 0.34;
}

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

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

body {
  margin: 0;
  background: var(--paper);
  /* The fixed background layer must not create a scroll container. */
  position: relative;
  color: var(--ink);
  font-family: Arial, Helvetica, sans-serif;
  font-size: 100%;
  line-height: 1.35;
  text-transform: uppercase;
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3, p, ol, ul { margin: 0; }
ol, ul { list-style: none; padding: 0; }

/* ---------- Shared rail: every section uses the same width and gutters ---------- */

.rail {
  max-width: var(--rail-max);
  margin: 0 auto;
  padding: calc(var(--step) * 1.5) var(--gutter) calc(var(--step) * 2);
}

.rule {
  border: 0;
  border-top: var(--hair) solid var(--ink);
  margin: var(--step) 0;
}

/* ---------- Skip link ---------- */

.skip {
  position: absolute;
  left: -9999px;
  top: 0;
  background: var(--ink);
  color: var(--paper);
  padding: 0.75rem 1rem;
  font-size: 0.75rem;
  letter-spacing: 0.08em;
  text-decoration: none;
  z-index: 10;
}
.skip:focus { left: 0; }

/* ---------- Masthead ---------- */

.wordmark {
  /* Anchors the absolutely positioned caret, and makes the h1 the offsetParent
     for the split chars so their positions can be measured directly. */
  position: relative;
  font-size: clamp(2rem, 10.4vw, 8.5rem);
  font-weight: 700;
  letter-spacing: -0.055em;
  line-height: 0.9;
  white-space: nowrap;
}

/* ---------- Wordmark typed reveal ----------

   Built by type.js (gsap core + SplitText). Structure:

     h1.wordmark              accessible name lives here (aria-label)
       .wordmark__text        the split line, aria-hidden
         .wordmark__char x11  one span per character
       .wordmark__caret       block caret, aria-hidden

   The h1 stays ONE static line of text. Characters are revealed with
   visibility, never display or width, so every glyph occupies its final
   position from the first frame. The line is therefore full width before a
   single character is visible, and nothing below the masthead can shift.

   The caret is absolutely positioned, so it is outside the flow entirely and
   cannot affect layout either. Its height and vertical offset are set in JS
   from the measured baseline; only what cannot be measured in CSS lives there. */

.wordmark__text { display: inline; }

/* SplitText sets inline-block itself. Chars are hidden by JS rather than here,
   so that if GSAP fails to load the wordmark still renders as plain text. */

.wordmark__caret {
  position: absolute;
  left: 0;
  width: 0.5em;
  background: var(--ink);
  /* 530ms on, 530ms off: the cadence of a terminal cursor, and the blink runs
     indefinitely rather than stopping when the word completes.

     steps(1) not a fade. A hard on/off suits the flat black-on-white treatment
     and reads as a real caret rather than a glow.

     1.06s per cycle is ~0.94Hz, an order of magnitude under the three flashes
     per second that WCAG 2.3.1 prohibits. Because it never stops on its own it
     IS blinking content under 2.2.2, so it must remain stoppable - see the
     is-caret-static rule below. */
  animation: caret-blink 1060ms steps(1, end) infinite;
}

/* The WCAG 2.2.2 stop. Applied by type.js when plain mode is active or the user
   prefers reduced motion; the caret stays visible but goes completely still. */
.wordmark.is-caret-static .wordmark__caret {
  animation: none;
  opacity: 1;
}

@keyframes caret-blink {
  0%, 49.999% { opacity: 1; }
  50%, 100%   { opacity: 0; }
}

/* ---------- Drop counter: the scarcity device ---------- */

.drop__title {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  column-gap: clamp(0.5rem, 2vw, 1.25rem);
  font-weight: 700;
}

.drop__count {
  font-size: clamp(7rem, 38vw, 20rem);
  letter-spacing: -0.07em;
  line-height: 0.78;
  font-variant-numeric: tabular-nums;
  /* Negative tracking leaves a -0.07em trailing space; pad it back so it
     doesn't swallow the column gap and collide with the word. */
  padding-right: 0.07em;
}

.drop__word {
  font-size: clamp(1.5rem, 7vw, 4rem);
  letter-spacing: -0.03em;
  line-height: 1;
}

/* ---------- Products ---------- */

.section-title {
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.22em;
  margin-bottom: var(--step);
}

.product {
  display: grid;
  grid-template-columns: 2.5rem 1fr;
  grid-template-areas:
    "index  name"
    ".      desc"
    ".      price"
    ".      cta";
  column-gap: 0.75rem;
  padding: calc(var(--step) * 0.9) 0;
  border-top: var(--hair) solid var(--ink);
}
.product:last-child { padding-bottom: 0; }

.product__index {
  grid-area: index;
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  line-height: 1.9;
  font-variant-numeric: tabular-nums;
}

.product__body { grid-area: name; }

.product__name {
  font-size: clamp(1.375rem, 6.5vw, 2rem);
  font-weight: 700;
  letter-spacing: -0.035em;
  line-height: 1.05;
}

.product__desc {
  grid-area: desc;
  margin-top: 0.6rem;
  max-width: 44ch;
  font-size: 0.75rem;
  font-weight: 400;
  letter-spacing: 0.05em;
  line-height: 1.65;
}

.product__price {
  grid-area: price;
  margin-top: 0.85rem;
  font-size: clamp(1.375rem, 6.5vw, 2rem);
  font-weight: 700;
  letter-spacing: -0.03em;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}

/* Featured row: the 50-slot race carries extra weight. */
.product--featured .product__name,
.product--featured .product__price { text-decoration: underline; text-underline-offset: 0.18em; }

/* ---------- CTA ---------- */

.cta {
  grid-area: cta;
  margin-top: 1rem;
  display: inline-flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.5rem;
  width: 100%;
  padding: 0.85rem 1rem;
  border: var(--hair) solid var(--ink);
  background: var(--paper);
  color: var(--ink);
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-decoration: none;
  transition: background-color var(--dur) linear, color var(--dur) linear;
}

.cta__arrow { font-weight: 400; }

.cta:hover,
.cta:focus-visible {
  background: var(--ink);
  color: var(--paper);
}

.cta:active { background: var(--ink); color: var(--paper); }

/* ---------- Background texture ---------- */

.bg {
  position: fixed;
  inset: 0;
  z-index: -1;
  overflow: hidden;
  background: var(--paper);
  pointer-events: none;
}

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

.bg__clip {
  position: absolute;
  /* The blur samples past the viewport edge; without this inset overscan the
     blur radius would feather in a visible pale border on all four sides. */
  inset: calc(var(--bg-blur) * -2);
  width: calc(100% + var(--bg-blur) * 4);
  height: calc(100% + var(--bg-blur) * 4);
  object-fit: cover;
  opacity: 0;
  filter: blur(var(--bg-blur)) grayscale(1) contrast(1.5);
  /* Promote to its own layer so swapping never repaints the text above it. */
  will-change: opacity;
}

/* Per-clip, because the four files carry very different amounts of ink.
   See the token block at the top for the measured luminance behind each. */
.bg__clip:nth-child(1).is-live { opacity: var(--bg-op-1); }
.bg__clip:nth-child(2).is-live { opacity: var(--bg-op-2); }
.bg__clip:nth-child(3).is-live { opacity: var(--bg-op-3); }
.bg__clip:nth-child(4).is-live { opacity: var(--bg-op-4); }

/* The scrim is what actually guarantees contrast. It sits between the video
   and the content, so no amount of video brightness can reach the text. */
.bg__scrim {
  position: absolute;
  inset: 0;
  background: rgba(255, 255, 255, var(--bg-scrim));
}

/* Glitch: a hard cut with no crossfade, plus an occasional 1-frame black
   inversion. Driven by class toggles from the script at irregular intervals.
   Deliberately capped well under the WCAG 2.3.1 three-flashes-per-second
   threshold — the script enforces a minimum gap between cuts. */
.bg__stack.is-cut .bg__clip:nth-child(1).is-live { opacity: calc(var(--bg-op-1) * 1.4); }
.bg__stack.is-cut .bg__clip:nth-child(2).is-live { opacity: calc(var(--bg-op-2) * 1.4); }
.bg__stack.is-cut .bg__clip:nth-child(3).is-live { opacity: calc(var(--bg-op-3) * 1.4); }
.bg__stack.is-cut .bg__clip:nth-child(4).is-live { opacity: calc(var(--bg-op-4) * 1.4); }

.bg__stack.is-invert {
  filter: invert(1);
  /* Inversion would turn the white ground black, which would destroy text
     contrast outright. Clamping opacity keeps the flicker as a grey pulse. */
  opacity: 0.5;
}

/* Reduced motion: no video, no swapping, no flicker. The poster stays as a
   still texture so the page does not visibly lose a layer. */
@media (prefers-reduced-motion: reduce) {
  .bg__stack { display: none; }
  .bg {
    background-image: url("assets/bg/poster-1.jpg");
    background-size: cover;
    background-position: center;
  }
  .bg__scrim { background: rgba(255, 255, 255, 0.88); }
}

/* Forced-colors: drop the texture entirely rather than fight the user's
   palette. Backgrounds are exactly what this mode exists to override. */
@media (forced-colors: active) {
  .bg { display: none; }
}

/* ---------- Background mode toggle ---------- */

/* Plain mode: the original black-on-white page, exactly as it was before the
   texture existed. The layer is removed rather than hidden behind a scrim, so
   nothing is decoding and the white is a true #fff. */
.is-plain .bg { display: none; }

.bg-toggle {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 2.75rem;
  height: 2.75rem;
  padding: 0;
  border: 0;
  background: none;
  color: var(--muted);
  cursor: pointer;
  transition: color var(--dur) linear;
}

.bg-toggle:hover,
.bg-toggle:focus-visible { color: var(--ink); }

/* Pressed means plain mode is on. Filling the glyph gives the state a second,
   non-colour cue rather than relying on the grey-to-black shift alone. */
.bg-toggle[aria-pressed="true"] { color: var(--ink); }

/* Click-sound toggle. Reuses .bg-toggle for sizing, hit area and focus; only
   the glyph swap is specific to it. Pressed = muted. */
.sound-toggle .sound-toggle__off { display: none; }
.sound-toggle[aria-pressed="true"] .sound-toggle__off { display: block; }
.sound-toggle[aria-pressed="true"] .sound-toggle__on { display: none; }

.bg-toggle .icon { pointer-events: none; }

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

/* ---------- Topbar socials ---------- */

.sprite { display: none; }

/* Visually hidden but still read aloud. The icons carry no text, so each
   link needs an accessible name or a screen reader announces the URL. */
.vh {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.topbar {
  display: flex;
  justify-content: flex-end;
  margin-bottom: calc(var(--step) * 0.75);
}

.social {
  display: flex;
  align-items: center;
  gap: 0.25rem;
}

.social__link {
  display: flex;
  align-items: center;
  justify-content: center;
  /* 44px hit target from a 20px glyph — padding does the work so the icons
     stay small while the tap area stays thumb-sized. */
  width: 2.75rem;
  height: 2.75rem;
  color: var(--muted);
  transition: color var(--dur) linear;
}

.social__link:hover,
.social__link:focus-visible { color: var(--ink); }

.icon {
  width: 1.25rem;
  height: 1.25rem;
  display: block;
  fill: none;
}

/* ---------- Focus ---------- */

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

main:focus { outline: none; }

/* ---------- Footer ---------- */

.footer__line {
  max-width: 52ch;
  font-size: 0.6875rem;
  font-weight: 400;
  letter-spacing: 0.08em;
  line-height: 1.7;
}
.footer__line + .footer__line { margin-top: 0.6rem; }
.footer__line a { color: var(--ink); text-underline-offset: 0.18em; }
.footer__line--quiet { margin-top: 1.25rem; font-weight: 700; letter-spacing: 0.16em; }

/* ---------- Footer legal nav ---------- */

.legal { margin-top: 1.25rem; }

.legal__list {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem 1.5rem;
}

.legal__link {
  color: var(--ink);
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-decoration: none;
  border-bottom: var(--hair) solid var(--ink);
  padding-bottom: 0.15rem;
}

.legal__link:hover,
.legal__link:focus-visible { background: var(--ink); color: var(--paper); }

/* ---------- Desktop: same layout, wider ---------- */

@media (min-width: 48rem) {
  :root { --gutter: 2.5rem; --step: 2.25rem; }

  .product {
    grid-template-columns: 4rem minmax(0, 1fr) 8rem 15rem;
    grid-template-areas:
      "index name price cta"
      ".     desc price cta";
    column-gap: 2rem;
    align-items: start;
  }

  .product__index { line-height: 1.6; }

  /* Let the measure follow the rail instead of a fixed ch cap. These are short
     single-idea lines, not body copy, and the grid column already bounds them
     — a ch cap on top only forced extra wraps and left dead space before the
     price column. */
  .product__desc { margin-top: 0.75rem; max-width: none; }
  .product__price { margin-top: 0; text-align: right; }
  .cta { margin-top: 0; align-self: start; }
}

@media (min-width: 80rem) {
  :root { --gutter: 3.5rem; }
  .product { grid-template-columns: 5rem minmax(0, 1fr) 10rem 18rem; }
}

/* ---------- Page transitions ---------- */

/* Native cross-document view transitions. No JS, no link interceptor.

   The opt-in has to exist in BOTH the outgoing and incoming document, which is
   exactly why it lives in the shared stylesheet: all five pages link style.css,
   so every same-origin navigation between them is covered by this one rule.

   The Whop checkout CTAs are cross-origin, and the spec only ever fires a
   cross-document transition between same-origin documents. Those navigations
   are therefore untouched by construction, with nothing intercepting the click
   and nothing to delay it. That is the whole reason for using the native
   feature rather than a JS interceptor: a script that hooks every click is one
   bug away from swallowing a $200 checkout. */
@view-transition { navigation: auto; }

/* The glitch. Blur ramp, a sub-pixel-ish horizontal slip, and a crossfade.

   NOTE: this blur is unrelated to --bg-blur. That token is the gaussian on the
   background VIDEO layer and is measured against text contrast. This one is a
   transient effect on a snapshot of the whole page and never coexists with
   reading, so it is deliberately not tied to that variable.

   The UA stylesheet blends the two snapshots with plus-lighter inside an
   isolated pair, which is what keeps a crossfade from dipping through grey in
   the middle. Blurring both halves rides on top of that and reads as a light
   bloom rather than a smear, which suits the flat black-on-white treatment.

   Direction is intentional: the outgoing page slips LEFT and the incoming page
   arrives from the RIGHT, so the motion reads as forward travel rather than as
   the page shivering in place. */
@keyframes yp-page-out {
  from { opacity: 1; filter: blur(0);     transform: translateX(0); }
  to   { opacity: 0; filter: blur(4.5px); transform: translateX(-1.6px); }
}

@keyframes yp-page-in {
  from { opacity: 0; filter: blur(4.5px); transform: translateX(1.6px); }
  to   { opacity: 1; filter: blur(0);     transform: translateX(0); }
}

/* 220ms on the house curve, the same ease as the CTA hovers and the GSAP
   default. Strongly ease-out, so it decelerates into the settled page instead
   of snapping. */
::view-transition-old(root) {
  animation: yp-page-out 220ms cubic-bezier(0.16, 1, 0.3, 1) both;
}

::view-transition-new(root) {
  animation: yp-page-in 220ms cubic-bezier(0.16, 1, 0.3, 1) both;
}

/* ---------- Reduced motion ---------- */

@media (prefers-reduced-motion: reduce) {
  .cta,
  .social__link { transition: none; }

  /* The wordmark is rendered complete and instantly by type.js. The caret is
     still drawn, but never blinks. */
  .wordmark__caret { animation: none; opacity: 1; }

  /* Kill the page transition outright. With no animations on the pseudo-
     elements the transition completes on the next frame, so navigation becomes
     an instant cut. The navigation itself still happens either way — the
     transition was only ever a visual layer on top of it. */
  ::view-transition-old(root),
  ::view-transition-new(root) { animation: none; }
}

/* ---------- Sub-pages: terms, privacy, faq, accessibility ---------- */

/* The landing page is all caps by design. Long-form legal copy is not — caps
   destroy word shape and roughly halve reading speed, and these pages exist to
   be read. Headings stay caps to hold the brand. */
.doc { text-transform: none; }

.doc .wordmark,
.doc .section-title,
.doc .doc__title,
.doc .doc__h2,
.doc .legal__link,
.doc .footer__line--quiet { text-transform: uppercase; }

/* Header row on the document pages: back link at the start, injected controls
   at the end. Matches the topbar's role on the landing page without importing
   its social list. */
.doc__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  /* Cancel the button's hit-area padding so the row's optical top edge lines up
     with the back link, not with the taller control. */
  margin-bottom: -0.85rem;
}

.doc__back {
  display: inline-block;
  color: var(--ink);
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  text-decoration: none;
  border-bottom: var(--hair) solid var(--ink);
  padding-bottom: 0.15rem;
}
.doc__back:hover,
.doc__back:focus-visible { background: var(--ink); color: var(--paper); }

.doc__title {
  margin-top: var(--step);
  font-size: clamp(1.75rem, 8vw, 3.5rem);
  font-weight: 700;
  letter-spacing: -0.04em;
  line-height: 1;
}

.doc__meta {
  margin-top: 0.75rem;
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

.doc__section + .doc__section { margin-top: calc(var(--step) * 1.25); }

.doc__h2 {
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.22em;
  margin-bottom: 0.75rem;
}

.doc p,
.doc li {
  max-width: 68ch;
  font-size: 0.875rem;
  font-weight: 400;
  letter-spacing: 0.01em;
  line-height: 1.7;
}

.doc p + p,
.doc__list + p { margin-top: 0.85rem; }

.doc__list {
  list-style: none;
  padding: 0;
  margin-top: 0.85rem;
}
.doc__list li { position: relative; padding-left: 1.1rem; }
.doc__list li + li { margin-top: 0.5rem; }
.doc__list li::before {
  content: "—";
  position: absolute;
  left: 0;
}

.doc dt {
  max-width: 68ch;
  margin-top: 1.35rem;
  font-size: 0.9375rem;
  font-weight: 700;
  line-height: 1.5;
}
.doc dd {
  max-width: 68ch;
  margin: 0.4rem 0 0;
  font-size: 0.875rem;
  line-height: 1.7;
}

.doc a:not(.doc__back):not(.legal__link) { color: var(--ink); }

/* Unfilled business details. Inverted so the client cannot ship one by
   accident — these are decisions I cannot invent on their behalf. */
.todo {
  background: var(--ink);
  color: var(--paper);
  padding: 0.05em 0.35em;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
}
