/* ==========================================================================
   Pixel Button 1.0 — pill button with a rotating beam and a pixel grid
   --------------------------------------------------------------------------
   Plain CSS. No framework, no build step, no external request.

   The component enhances a native <button> (or <a>). Everything you see here
   works with CSS alone: the background, the border, the rotating conic beam,
   the glow and the text outline. The script adds one thing — the canvas that
   draws the pixel grid — so a page never depends on JavaScript to show a
   finished button.

   PROPORTIONS
   Every metric derives from --pxb-font-size. The ratios were read off the
   reference implementation, not estimated:

     text            1                (the reference: 16px)
     padding         1.25 em / 2.5 em  (20px / 40px)
     border          1px              (kept in px, see the note below)
     radius          22.5 em          (360px — a pill at any size)
     glow spread     1 em out, 2 em blur, 0.25 em spread
     glow offset     -0.484 em        (the reference says -1ex; 0.484 is what
                                       1ex measures in the shipped face)
     text outline    0.1875 em radius, 1.25 em blur

   So changing --pxb-font-size rescales the whole button, and nothing else
   needs to be touched. One deliberate exception: the pixel grid keeps its
   step in px (see section 5) because a scaled-up grid stops being a grid.

   Order of this file:
     1. Font
     2. Registered properties
     3. Design tokens
     4. The button: box, beam
     5. Pixel layer
     6. Glow
     7. Label and text outline
     8. States: hover, focus, press
     9. Colour variants
    10. Fallbacks: reduced motion, forced colours, no @property

   Naming follows BEM: .pixel-button, .pixel-button__part, .pixel-button--variant.
   ========================================================================== */

/* ==========================================================================
   1. Font
   --------------------------------------------------------------------------
   Local file, no external request. The face matters: the button is exactly as
   wide as its label plus two paddings, so a different face means a different
   button. Measured against the reference, "Get unlimited access" is 144.000px
   here and 144px there — the same to a thousandth. In plain sans-serif it
   would be 148.5px, and the button would be 4.5px wider.
   ========================================================================== */

@font-face {
  font-family: "Cabinet Grotesk";
  src: url("CabinetGrotesk-500-latin.woff2") format("woff2");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}

/* ==========================================================================
   2. Registered properties
   --------------------------------------------------------------------------
   A custom property is a string until it is registered. Registering these
   four is what lets the browser interpolate them, which is what makes the
   beam turn and the hover spring travel. Without @property they still work as
   plain substitutions — the beam simply sits still (see section 10).
   ========================================================================== */

@property --pxb-beam-angle {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}

@property --pxb-beam-shift {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}

@property --pxb-beam-stop {
  syntax: "<percentage>";
  inherits: false;
  initial-value: 5%;
}

/* This one inherits, unlike the three above: it is set on the button and read
   by the ::after that draws the glow. With inherits: false the pseudo-element
   falls back to the initial value and the glow never lights up. */
@property --pxb-glow-opacity {
  syntax: "<number>";
  inherits: true;
  initial-value: 0;
}

/* Registered as lengths so the script reads a resolved px value. Left as
   plain strings, `--pxb-grid-gap: calc(5px * 2)` would reach getPropertyValue
   as the literal text "calc(5px * 2)", parse to NaN, and silently fall back
   to the default. Registering them is also what makes an em value work. */
@property --pxb-grid-gap {
  syntax: "<length>";
  inherits: true;
  initial-value: 5px;
}

@property --pxb-grid-size {
  syntax: "<length>";
  inherits: true;
  initial-value: 2.5px;
}

/* ==========================================================================
   3. Design tokens
   ========================================================================== */

.pixel-button {
  /* --- scale ------------------------------------------------------------ */
  /* The single knob. Everything below is em, so this rescales the button. */
  --pxb-font-size: 1rem;
  --pxb-font: "Cabinet Grotesk", system-ui, sans-serif;
  --pxb-font-weight: 500;

  /* --- colours ---------------------------------------------------------- */
  --pxb-bg: #000000;
  --pxb-border: #1a1818;
  --pxb-text: #ffffff;
  --pxb-beam-1: #00ff44;
  --pxb-beam-2: #94ffb0;
  --pxb-glow: #00ff44;
  /* Three greens, picked at random per pixel. */
  --pxb-pixel-1: #29ff0d;
  --pxb-pixel-2: #57ff5c;
  --pxb-pixel-3: #93ff87;
  --pxb-focus: #94ffb0;

  /* --- box -------------------------------------------------------------- */
  --pxb-padding-block: 1.25em;
  --pxb-padding-inline: 2.5em;
  --pxb-radius: 22.5em;
  /* Kept in px on purpose. Scaled in em, a border lands on a fraction of a
     pixel and renders as a dirty grey smear instead of a clean hairline —
     and the reference pins it in px too. */
  --pxb-border-width: 1px;

  /* --- beam ------------------------------------------------------------- */
  --pxb-beam-turn: 3s; /* measured: 120.1°/s, one turn in 3.00s */
  --pxb-beam-stop: 5%; /* stops land at 1x, 2x, 3x, 4x of this */
  --pxb-beam-stop-hover: 20%;
  --pxb-beam-shift-hover: 95deg;
  /* The reference springs these two (stiffness 120, damping 30, mass 0.6 —
     overdamped, so no overshoot). This curve is the closest cubic-bezier:
     0.55% RMS, 2.67% worst case, i.e. 2.5° on the angle. */
  --pxb-spring-duration: 1232ms;
  --pxb-spring: cubic-bezier(0.209, 0.705, 0.203, 0.997);

  /* --- glow ------------------------------------------------------------- */
  --pxb-glow-offset: -0.484em;
  --pxb-glow-blur: 2em;
  --pxb-glow-spread: 0.25em;
  --pxb-glow-duration: 800ms;
  /* Measured against the reference at 0.02% RMS — this one is not a guess. */
  --pxb-glow-ease: ease-out;

  /* --- pixel grid (read by the script) ---------------------------------- */
  /* In px, not em: the grid is a texture. Doubling the type size adds pixels
     rather than inflating them, which is what the reference does too. Set
     them in em instead (0.3125em / 0.15625em) and the grid scales with the
     button, keeping the same 495 squares at any size. */
  --pxb-grid-gap: 5px;
  --pxb-grid-size: 2.5px;
  --pxb-pixel-speed: 50; /* 0-100; the script scales it by 0.001 */
  --pxb-fade-out: 250ms; /* grid dissolve once the pointer leaves */

  /* --- ripple (read by the script) -------------------------------------- */
  --pxb-ripple-duration: 600ms;
  --pxb-ripple-scale: 3;
  --pxb-ripple-band: 50px; /* half-width of the travelling ring */

  /* --- layer filters ---------------------------------------------------- */
  --pxb-filter: brightness(1) contrast(1);
  --pxb-filter-hover: brightness(1.15) contrast(1.05);
  --pxb-filter-press: brightness(1.5) contrast(1.2);
  --pxb-filter-duration: 100ms;
}

/* Declared by the component rather than assumed from the page: without it, a
   host page with the default content-box adds the padding and the border on
   top of the height, and the pill comes out taller than it should. */
.pixel-button,
.pixel-button::before,
.pixel-button::after,
.pixel-button *,
.pixel-button *::before,
.pixel-button *::after {
  box-sizing: border-box;
}

/* ==========================================================================
   4. The button: box, beam
   --------------------------------------------------------------------------
   Two backgrounds in one declaration. The flat fill is clipped to the padding
   box, the conic beam to the border box: what shows through the 1px border is
   the beam, and the middle stays black. That is the whole trick — there is no
   second element behind the button.
   ========================================================================== */

.pixel-button {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  display: inline-flex;
  align-items: center;
  justify-content: center;

  margin: 0;
  padding: var(--pxb-padding-block) var(--pxb-padding-inline);
  border: var(--pxb-border-width) solid transparent;
  border-radius: var(--pxb-radius);

  font-family: var(--pxb-font);
  /* Set here, not on the parts: every em below then resolves against it. */
  font-size: var(--pxb-font-size);
  font-weight: var(--pxb-font-weight);
  font-style: normal;
  line-height: 1em;
  letter-spacing: 0em;
  text-align: center;
  text-decoration: none;
  color: var(--pxb-text);

  background:
    linear-gradient(var(--pxb-bg), var(--pxb-bg)) padding-box,
    conic-gradient(
        from calc(var(--pxb-beam-angle) - var(--pxb-beam-shift)),
        transparent,
        var(--pxb-beam-1) var(--pxb-beam-stop),
        var(--pxb-beam-2) calc(var(--pxb-beam-stop) * 2),
        var(--pxb-beam-1) calc(var(--pxb-beam-stop) * 3),
        transparent calc(var(--pxb-beam-stop) * 4)
      )
      border-box;
  /* The inner hairline. It is *inside* the box, which is why the padding
     above already accounts for it and the height comes out at 58px. */
  box-shadow: inset 0 0 0 var(--pxb-border-width) var(--pxb-border);

  cursor: pointer;
  outline: none;
  -webkit-font-smoothing: antialiased;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;

  animation: pxb-beam-turn var(--pxb-beam-turn) linear infinite;
  transition:
    --pxb-beam-shift var(--pxb-spring-duration) var(--pxb-spring),
    --pxb-beam-stop var(--pxb-spring-duration) var(--pxb-spring);
}

@keyframes pxb-beam-turn {
  to {
    --pxb-beam-angle: 360deg;
  }
}

/* An <a> styled as a button should not carry a link underline. */
a.pixel-button:hover,
a.pixel-button:focus {
  text-decoration: none;
}

.pixel-button:disabled,
.pixel-button[aria-disabled="true"] {
  cursor: not-allowed;
  opacity: 0.55;
  animation-play-state: paused;
}

/* ==========================================================================
   5. Pixel layer
   --------------------------------------------------------------------------
   Built by the script, because a canvas cannot live in a pseudo-element. The
   layer sits 1px in, so it stops exactly where the border starts, and its
   radius is one pixel tighter for the same reason.

   The canvas is sized 1:1 in CSS pixels, not scaled by devicePixelRatio. That
   is deliberate: on a retina screen the grid is resampled, and that softness
   is part of how the reference looks. Drawing it at 2x would give a crisper,
   finer texture — a different component.
   ========================================================================== */

.pixel-button__layer {
  position: absolute;
  inset: var(--pxb-border-width);
  z-index: 0;
  overflow: hidden;
  border-radius: calc(var(--pxb-radius) - var(--pxb-border-width));
  pointer-events: none;
  filter: var(--pxb-filter);
  transition: filter var(--pxb-filter-duration) linear;
}

.pixel-button__canvas {
  display: block;
  width: 100%;
  height: 100%;
}

/* ==========================================================================
   6. Glow
   --------------------------------------------------------------------------
   A pseudo-element rather than a node: it is pure decoration and the script
   should not have to build it. It is *larger* than the button (1em out on
   every side) so the inset shadow can pool along the bottom edge and spill
   past the pill instead of stopping at it.
   ========================================================================== */

.pixel-button::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  width: calc(100% + 1em);
  height: calc(100% + 1em);
  z-index: 3;
  border-radius: inherit;
  box-shadow: inset 0 var(--pxb-glow-offset) var(--pxb-glow-blur)
    var(--pxb-glow-spread) var(--pxb-glow);
  opacity: var(--pxb-glow-opacity);
  pointer-events: none;
  transition: opacity var(--pxb-glow-duration) var(--pxb-glow-ease);
}

/* ==========================================================================
   7. Label and text outline
   --------------------------------------------------------------------------
   The 29 shadows are not decoration for its own sake: the label sits on top
   of a field of bright green pixels, and without a dark halo around every
   glyph it becomes unreadable the moment the grid lights up.

   Their positions are every whole offset within 3px of the centre, each at
   max(0.2, (1 - distance/3) x 0.8) of the background colour — the same
   formula the reference runs at render time, frozen here into CSS.
   ========================================================================== */

.pixel-button__label {
  position: relative;
  z-index: 10;
  color: var(--pxb-text);
  text-shadow:
    -0.1875em 0 1.25em color-mix(in srgb, var(--pxb-bg) 20%, transparent),
    -0.125em -0.125em 1.25em color-mix(in srgb, var(--pxb-bg) 20%, transparent),
    -0.125em -0.0625em 1.25em
      color-mix(in srgb, var(--pxb-bg) 20.3715%, transparent),
    -0.125em 0 1.25em color-mix(in srgb, var(--pxb-bg) 26.6667%, transparent),
    -0.125em 0.0625em 1.25em
      color-mix(in srgb, var(--pxb-bg) 20.3715%, transparent),
    -0.125em 0.125em 1.25em color-mix(in srgb, var(--pxb-bg) 20%, transparent),
    -0.0625em -0.125em 1.25em
      color-mix(in srgb, var(--pxb-bg) 20.3715%, transparent),
    -0.0625em -0.0625em 1.25em
      color-mix(in srgb, var(--pxb-bg) 42.2876%, transparent),
    -0.0625em 0 1.25em color-mix(in srgb, var(--pxb-bg) 53.3333%, transparent),
    -0.0625em 0.0625em 1.25em
      color-mix(in srgb, var(--pxb-bg) 42.2876%, transparent),
    -0.0625em 0.125em 1.25em
      color-mix(in srgb, var(--pxb-bg) 20.3715%, transparent),
    0 -0.1875em 1.25em color-mix(in srgb, var(--pxb-bg) 20%, transparent),
    0 -0.125em 1.25em color-mix(in srgb, var(--pxb-bg) 26.6667%, transparent),
    0 -0.0625em 1.25em color-mix(in srgb, var(--pxb-bg) 53.3333%, transparent),
    0 0.0625em 1.25em color-mix(in srgb, var(--pxb-bg) 53.3333%, transparent),
    0 0.125em 1.25em color-mix(in srgb, var(--pxb-bg) 26.6667%, transparent),
    0 0.1875em 1.25em color-mix(in srgb, var(--pxb-bg) 20%, transparent),
    0.0625em -0.125em 1.25em
      color-mix(in srgb, var(--pxb-bg) 20.3715%, transparent),
    0.0625em -0.0625em 1.25em
      color-mix(in srgb, var(--pxb-bg) 42.2876%, transparent),
    0.0625em 0 1.25em color-mix(in srgb, var(--pxb-bg) 53.3333%, transparent),
    0.0625em 0.0625em 1.25em
      color-mix(in srgb, var(--pxb-bg) 42.2876%, transparent),
    0.0625em 0.125em 1.25em
      color-mix(in srgb, var(--pxb-bg) 20.3715%, transparent),
    0.125em -0.125em 1.25em color-mix(in srgb, var(--pxb-bg) 20%, transparent),
    0.125em -0.0625em 1.25em
      color-mix(in srgb, var(--pxb-bg) 20.3715%, transparent),
    0.125em 0 1.25em color-mix(in srgb, var(--pxb-bg) 26.6667%, transparent),
    0.125em 0.0625em 1.25em
      color-mix(in srgb, var(--pxb-bg) 20.3715%, transparent),
    0.125em 0.125em 1.25em color-mix(in srgb, var(--pxb-bg) 20%, transparent),
    0.1875em 0 1.25em color-mix(in srgb, var(--pxb-bg) 20%, transparent),
    0 0 0.625em var(--pxb-bg);
}

/* Turning the outline off is a token change, not a class change. */
.pixel-button--no-outline .pixel-button__label {
  text-shadow: none;
}

/* ==========================================================================
   8. States: hover, focus, press
   --------------------------------------------------------------------------
   Keyboard focus gets exactly what the pointer gets. The reference only reacts
   to hover, so a keyboard user saw a dead button; :focus-visible is added here
   deliberately (see README, "what differs").
   ========================================================================== */

.pixel-button:hover,
.pixel-button:focus-visible {
  --pxb-beam-shift: var(--pxb-beam-shift-hover);
  --pxb-beam-stop: var(--pxb-beam-stop-hover);
  --pxb-glow-opacity: 1;
  --pxb-filter: var(--pxb-filter-hover);
}

.pixel-button:disabled:hover,
.pixel-button[aria-disabled="true"]:hover {
  --pxb-beam-shift: 0deg;
  --pxb-beam-stop: 5%;
  --pxb-glow-opacity: 0;
  --pxb-filter: brightness(1) contrast(1);
}

/* The ring the reference never had. Two colours so it stays visible whichever
   side of the button the page background falls on. */
.pixel-button:focus-visible {
  outline: 2px solid var(--pxb-focus);
  outline-offset: 2px;
}

/* Pressed: the grid brightens while the ripple crosses it. The script sets
   this attribute rather than relying on :active, because the flash has to
   outlast the pointer being held down. */
.pixel-button[data-pxb-pressed] {
  --pxb-filter: var(--pxb-filter-press);
}

/* ==========================================================================
   9. Colour variants
   --------------------------------------------------------------------------
   Each one is a handful of tokens. The values are the other themes carried by
   the reference component, read from the same source as the green one.
   ========================================================================== */

.pixel-button--red {
  --pxb-beam-1: #ff1919;
  --pxb-beam-2: #ff9494;
  --pxb-glow: #ff1919;
  --pxb-pixel-1: #ff1919;
  --pxb-pixel-2: #ff5757;
  --pxb-pixel-3: #ffadad;
  --pxb-focus: #ff9494;
}

.pixel-button--amber {
  --pxb-beam-1: #ffbb00;
  --pxb-beam-2: #fffb94;
  --pxb-glow: #ffbb00;
  --pxb-pixel-1: #ffa600;
  --pxb-pixel-2: #ffdd33;
  --pxb-pixel-3: #fff787;
  --pxb-focus: #fffb94;
}

.pixel-button--magenta {
  --pxb-beam-1: #fb29ff;
  --pxb-beam-2: #fda3ff;
  --pxb-glow: #fb29ff;
  --pxb-pixel-1: #fb29ff;
  --pxb-pixel-2: #fd6bff;
  --pxb-pixel-3: #febdff;
  --pxb-focus: #fda3ff;
}

/* Light face: the reference swaps the background, the border and the ink, and
   leaves the beam alone. The text outline follows --pxb-bg on its own. */
.pixel-button--light {
  --pxb-bg: #ffffff;
  --pxb-border: #ababab;
  --pxb-text: #000000;
  --pxb-focus: #000000;
}

/* ==========================================================================
   10. Fallbacks
   ========================================================================== */

/* --- Reduced motion ------------------------------------------------------
   Nothing travels: the beam stops turning, the glow and the filter arrive at
   once, and the script draws the grid in one pass with no shimmer and no
   ripple. Delays go to zero too — a delay is motion spread over time. */
@media (prefers-reduced-motion: reduce) {
  .pixel-button {
    animation: none;
    transition-duration: 0s;
    transition-delay: 0s;
  }

  .pixel-button::after,
  .pixel-button__layer {
    transition-duration: 0s;
    transition-delay: 0s;
  }
}

/* --- Forced colours ------------------------------------------------------
   The system palette replaces every colour, and a canvas is a bitmap it
   cannot repaint: leaving the grid on would print green speckles over a
   high-contrast theme. So the decoration goes and the button stays a
   button. */
@media (forced-colors: active) {
  .pixel-button {
    animation: none;
    background: ButtonFace;
    border: 1px solid ButtonBorder;
    box-shadow: none;
    color: ButtonText;
    forced-color-adjust: none;
  }

  .pixel-button__layer,
  .pixel-button::after {
    display: none;
  }

  .pixel-button__label {
    color: ButtonText;
    text-shadow: none;
  }

  .pixel-button:hover,
  .pixel-button:focus-visible {
    background: Highlight;
    color: HighlightText;
  }

  .pixel-button:hover .pixel-button__label,
  .pixel-button:focus-visible .pixel-button__label {
    color: HighlightText;
  }

  .pixel-button:focus-visible {
    outline: 2px solid CanvasText;
  }
}

/* --- No @property --------------------------------------------------------
   Older engines treat the four properties above as plain strings: the beam
   holds still and the hover change is instant. The button is still a button,
   and the grid still runs, so this is a soft landing rather than a break. No
   rule is needed here — it is written down so the behaviour is not mistaken
   for a bug. */
