/* =========================================================================
   Plates — landing / sales / account page
   Palette and type are lifted from the desktop app's Retro Pixel theme and
   the Figma mockups, so the site and the product read as one thing.
   ========================================================================= */

/* ---------------------------------------------------------------- fonts -- */
/* Self-hosted from the same @fontsource packages the app bundles: the page
   makes no third-party requests, and the type matches the product exactly. */
@font-face {
  font-family: 'Silkscreen';
  src: url('../assets/fonts/silkscreen-400.woff2') format('woff2');
  font-weight: 400;
  font-display: swap;
}
@font-face {
  font-family: 'Silkscreen';
  src: url('../assets/fonts/silkscreen-700.woff2') format('woff2');
  font-weight: 700;
  font-display: swap;
}
/* Body face is Jersey 15, not the app's Pixelify Sans. Pixelify renders 2 and 8, 5 and S, and
   G and B identically — "+£2.99" read as "£8.99" and "70 GB" as "70 BB", which is not something
   a pricing page can ship with. Jersey 15 keeps the blocky pixel feel with unambiguous digits.
   Single weight; <strong> takes a synthesised bold, which stays legible. */
@font-face {
  font-family: 'Jersey 15';
  src: url('../assets/fonts/jersey15-400.woff2') format('woff2');
  font-weight: 400;
  font-display: swap;
}
@font-face {
  font-family: 'VT323';
  src: url('../assets/fonts/vt323-400.woff2') format('woff2');
  font-weight: 400;
  font-display: swap;
}
/* Hero lockup only. */
@font-face {
  font-family: 'Orbitron';
  src: url('../assets/fonts/orbitron-700.woff2') format('woff2');
  font-weight: 700;
  font-display: swap;
}
@font-face {
  font-family: 'Orbitron';
  src: url('../assets/fonts/orbitron-900.woff2') format('woff2');
  font-weight: 900;
  font-display: swap;
}

/* --------------------------------------------------------------- tokens -- */
:root {
  --font-display: 'Silkscreen', 'Courier New', monospace;
  --font-ui: 'Jersey 15', system-ui, sans-serif;
  --font-mono: 'VT323', ui-monospace, monospace;

  --ink: #14181d;
  --orange: #ff6600;
  --orange-lit: #ff8b3d;
  --blue: #007bff;
  --blue-flat: #0285ff;
  --green: #21ff63;
  --red: #ff2634;

  /* the app's Retro Pixel window colours */
  --cream: #fef6e4;
  --parchment: #fff4d6;
  --parchment-2: #f5e6c0;
  --brown: #3b2410;
  --brown-mid: #7a4a1e;
  --brown-dim: #8a6540;

  --dark: #1e1e1e;
  --dark-2: #282828;
  --bar: #14181d; /* the fixed top bar and the footer */

  --menubar-h: 46px;
  --edge: 3px;
  --maxw: 1180px;
  /* gap between the hero's blue frame and the stage; the bag cancels it out to hang from
     the frame itself rather than floating below it */
  --frame-pad: 20px;
}

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

html {
  scroll-behavior: smooth;
  /* every in-page jump has to clear the fixed menubar */
  scroll-padding-top: var(--menubar-h);
}

body {
  margin: 0;
  font-family: var(--font-ui);
  font-size: 17px;
  line-height: 1.65;
  color: var(--ink);
  background: #fff;
  -webkit-font-smoothing: antialiased;
  font-variant-ligatures: none;
}

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

/* Every sprite is small raster pixel art recovered from the mockups, so it must scale with
   nearest-neighbour — smooth interpolation turns it to mush. */
.menubar__logo,
.why__art img,
.prop img,
.prop,
.z,
.vhs img {
  image-rendering: pixelated;
}

h1,
h2,
h3,
h4 {
  font-family: var(--font-display);
  font-weight: 400;
  line-height: 1.25;
  margin: 0;
}

p {
  margin: 0 0 1em;
}

a {
  color: inherit;
}

/* Figures never wrap mid-number, and they're emphasised with a highlight rather than weight:
   Jersey 15's synthesised bold thickens strokes enough to start closing up 6s and 9s, which is
   the exact failure this page is trying to avoid. */
.fig {
  white-space: nowrap;
  background: rgb(251 208 0 / 0.45);
  box-shadow: 0 0 0 2px rgb(251 208 0 / 0.45);
}

.skip {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 100;
  padding: 10px 16px;
  background: var(--orange);
  color: #fff;
  font-family: var(--font-display);
}
.skip:focus {
  left: 0;
}

:where(a, button, input):focus-visible {
  outline: 3px solid var(--orange);
  outline-offset: 3px;
}

/* -------------------------------------------------------------- buttons -- */
.btn {
  display: inline-block;
  font-family: var(--font-display);
  font-size: 15px;
  text-transform: uppercase;
  text-decoration: none;
  padding: 14px 22px;
  border: var(--edge) solid var(--ink);
  border-radius: 0;
  background: var(--parchment);
  color: var(--ink);
  cursor: pointer;
  box-shadow: 4px 4px 0 rgb(0 0 0 / 0.45);
  transition:
    transform 80ms steps(2),
    box-shadow 80ms steps(2);
}
.btn:hover {
  transform: translate(-1px, -1px);
  box-shadow: 5px 5px 0 rgb(0 0 0 / 0.45);
}
.btn:active {
  transform: translate(4px, 4px);
  box-shadow: 0 0 0 rgb(0 0 0 / 0.45);
}
.btn--orange {
  background: var(--orange);
  color: #fff;
}
.btn--blue {
  background: var(--blue);
  color: #fff;
}
.btn--ghost {
  background: #fff;
  color: var(--ink);
}
.btn--small {
  font-size: 10px;
  padding: 10px 12px;
  line-height: 1.3;
  box-shadow: 3px 3px 0 rgb(0 0 0 / 0.45);
}

/* -------------------------------------------------------------- menubar -- */
.menubar {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 60;
  height: var(--menubar-h);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 0 16px;
  background: var(--bar);
  border-bottom: var(--edge) solid #000;
  color: #fff;
}
.menubar__brand {
  display: flex;
  align-items: center;
  gap: 10px;
  font-family: var(--font-display);
  font-size: 15px;
  text-decoration: none;
  letter-spacing: 0.04em;
}
.menubar__logo {
  width: 26px;
  height: 26px;
  border: 2px solid #000;
}
.menubar__nav {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 15px;
}
.menubar__nav a {
  text-decoration: none;
  padding: 6px 10px;
  border: 2px solid transparent;
}
.menubar__nav a:hover {
  border-color: #fff;
}
.menubar__cta {
  background: var(--orange);
  border-color: #000 !important;
  color: #fff;
}

/* Each slide fills the viewport and centres what's in it. min-height, not height: the
   Inside and Plans panels carry more than a screenful and have to be free to grow. */
main > section {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
}

/* =========================================================== 1. HERO ==== */
/* The frame grows to fill the slide rather than holding a fixed aspect ratio and leaving
   white space above and below it. The props are positioned in percentages, so they spread
   with the stage instead of being letterboxed inside it. */
.hero {
  padding: calc(var(--menubar-h) + 18px) 18px 18px;
  background: #fff;
}
.hero__frame {
  flex: 1;
  display: flex;
  width: 100%;
  max-width: 1500px;
  margin: 0 auto;
  border: 4px solid var(--blue);
  padding: var(--frame-pad);
}
.hero__stage {
  position: relative;
  flex: 1;
  /* Safe now that aspect-ratio is gone. The two together made min-height back-compute the
     *width*, which pushed the page wider than the viewport at ~768px. */
  min-height: 440px;
  /* So the orange squares can be sized against the STAGE rather than against whichever card
     they sit behind. They used to be a percentage of their own card, and the two cards are
     different sizes — the title card has a fixed 27% height and the tape card is sized by its
     contents — so "118% of mine" and "88% of mine" came out at wildly different absolute
     sizes. In the mockup they are one size. */
  container-type: size;
}

/* --- the stacked-frame title card --- */
/* Card insets follow the mockup, and they have to: the orange squares overhang each card by
   roughly a tenth of the stage, so a card flush to the frame pushes them outside it. */
/* Fixed height, so the card's bottom edge is a known 40% of the stage (13 + 27) and the rails
   above can be anchored to it in pure CSS rather than measured at runtime. */
.titlecard {
  position: absolute;
  left: 9%;
  top: 13%;
  width: 34%;
  height: 27%;
  z-index: 5;
}
/* The offset frames are SQUARES tucked behind a corner of each card, not copies of the card's
   own shape. Cloning the card's outline made them read as wide thin rectangles, which was the
   thing that looked wrong.
   Sized in `cqh` — against the stage, not against the card they hang off. Both sets are one
   size in the mockup, and they can only *be* one size if they are measured against something
   both cards share. */
.titlecard__stack,
.tapecard__stack {
  position: absolute;
  height: 32cqh;
  aspect-ratio: 1;
  border: 10px solid var(--orange);
  background: #fff;
}
/* Title card: squares poke out of its top-left. */
.titlecard__stack {
  left: -8.4%;
  top: -26%;
}
.titlecard__stack--2 {
  left: -15.8%;
  top: -45%;
}
/* Tape card: the same offsets, mirrored. `right`/`bottom` against the same percentages the
   title card uses for `left`/`top`, which is only equal in pixels because the two cards are
   now the same size — that is the whole reason they were made so. */
.tapecard__stack {
  left: auto;
  right: -8.4%;
  top: auto;
  bottom: -26%;
}
.tapecard__stack--2 {
  left: auto;
  right: -15.8%;
  top: auto;
  bottom: -45%;
}
.titlecard__box {
  position: relative;
  height: 100%;
  display: flex;
  flex-direction: column;
  justify-content: center;
  border: 4px solid var(--ink);
  background: #fff;
  padding: 20px 26px 18px;
}
.titlecard__title {
  font-family: 'Orbitron', var(--font-display);
  font-weight: 900;
  font-size: clamp(30px, 4.5vw, 69px);
  letter-spacing: 0.01em;
  margin: 0;
}
.titlecard__sub {
  font-family: 'Orbitron', var(--font-display);
  font-weight: 700;
  /* sits tight under the wordmark, as one lockup */
  margin: 2px 0 0;
  font-size: clamp(13px, 1.56vw, 24px);
  color: var(--ink);
}

/* --- the dumbbell bar --- */
.speedlines {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 1; /* behind both cards (5), so the rails run under their corners */
  pointer-events: none;
  overflow: visible;
}
.speedline {
  stroke: var(--blue);
  stroke-width: 9;
  /* the viewBox is scaled non-uniformly, so without this the stroke would be squashed
     thinner on one axis than the other */
  vector-effect: non-scaling-stroke;
}

/* --- props --- */
.prop {
  position: absolute;
  z-index: 2;
}
.prop--bag {
  /* mockup has it about a fifth of the frame in from the right edge */
  right: 18%;
  /* cancels the frame's padding so the chain's top link meets the blue outline; the pivot is
     at the very top of the sprite, so that contact point stays put through the swing */
  top: calc(-1 * var(--frame-pad));
  /* short enough that its base clears the dumbbell bar running underneath */
  height: 44%;
  /* The chain's top link sits at 86% across the sprite, not the middle — pivoting at 50%
     swung the bag around a point out in mid-air. */
  transform-origin: 86% 0;
  /* 1s per half-swing, so a full there-and-back cycle is 2s = 0.5 Hz. ease-in-out puts the
     slow moment at each extreme, which is how a pendulum actually moves.
     The stretch is a second animation on its own 1s timeline, peaking at the 50% mark — the
     bottom of the arc, where the bag is moving fastest. It drives the individual `scale`
     property rather than a `transform`, because two animations cannot share one `transform`;
     `rotate` and `scale` compose as R*S, so the stretch runs along the bag's own long axis,
     and transform-origin keeps it growing downward from the chain. */
  animation:
    swing 1s ease-in-out infinite alternate,
    stretch 1s ease-in-out infinite;
}
.prop--bag img {
  height: 100%;
  width: auto;
}
/* Bench + sleeping cat + squat rack, one strip. Proportions come straight from the mockup:
   it spans roughly 9%–44% of the frame's width and sits on the floor. */
.prop--gym {
  left: 3%;
  bottom: 0;
  width: 36%;
}

/* The arc is centred on -14.7deg because the sprite is drawn mid-swing. That figure is solved,
   not eyeballed: it is the rotation at which the bag body's centroid sits directly below the
   chain's pivot, i.e. where a real bag would hang at rest. At the ends of this arc the centroid
   is +/-18.4px either side of vertical, so the swing is genuinely symmetric.

   Two earlier attempts got this wrong. Centring on the body's own axis angle is a different
   quantity and lands ~5deg out, because the chain meets the bag off to one side of its top.
   Getting the rotation *sign* backwards is worse: the pivot is above the bag, so a positive
   (clockwise) CSS rotation drives the base to the left, and +14.7deg parked the bag 43.5px to
   one side of vertical for the whole cycle. That is what "only swings on one side" looked like. */
@keyframes swing {
  from {
    rotate: -26.7deg;
  }
  to {
    rotate: -2.7deg;
  }
}

/* Stretches 5% along its length at the bottom of the swing, back to normal at each extreme.
   The slight narrowing keeps it reading as a weighted bag being pulled long rather than just
   a taller sprite. */
@keyframes stretch {
  0%,
  100% {
    scale: 1 1;
  }
  50% {
    scale: 0.99 1.05;
  }
}

/* --- sleeping Z's --- */
/* Anchored just above the cat, which sits about 53% of the way along the gym strip. */
.zzz {
  position: absolute;
  left: 22%;
  bottom: 19%;
  z-index: 4; /* under .titlecard (5), so they vanish behind the white box */
  pointer-events: none;
}
.z {
  position: absolute;
  /* .zzz is an absolutely-positioned box whose children are all absolute too, so it computes
     to zero width — the base img rule's max-width:100% resolved against that and collapsed
     every Z to nothing. */
  max-width: none;
  height: auto;
  opacity: 0;
  animation: drift 5.4s ease-in infinite;
}
.z--1 {
  width: 17px;
  animation-delay: 0s;
}
.z--2 {
  width: 25px;
  animation-delay: 1.8s;
}
.z--3 {
  width: 35px;
  animation-delay: 3.6s;
}

/* Travels up-left into the title card's lower edge and is hidden by it. The
   opacity ramp only fades the tail end — the card does most of the work. */
@keyframes drift {
  0% {
    transform: translate(0, 0) scale(0.7) rotate(-6deg);
    opacity: 0;
  }
  12% {
    opacity: 1;
  }
  45% {
    transform: translate(-26px, -130px) scale(1) rotate(5deg);
    opacity: 1;
  }
  75% {
    transform: translate(6px, -250px) scale(1.25) rotate(-4deg);
    opacity: 1;
  }
  100% {
    transform: translate(-18px, -360px) scale(1.4) rotate(3deg);
    opacity: 0;
  }
}

/* --- the tape note card --- */
.tapecard {
  position: absolute;
  /* Inset from the right by exactly what the title card is inset from the left, and the same
     size. The two cards are one shape in two places: mirror them and the square offsets below
     can be plain percentages of the card and still come out identical in pixels. */
  left: auto;
  right: 9%;
  /* Placed so the OUTERMOST square clears the frame's bottom edge by exactly what the title
     card's outermost square clears the top edge by. Each stack reaches 45% of a card height
     past its own card, so the two gaps are equal only when the tape card's bottom sits as far
     above the floor as the title card's top sits below the ceiling: 13% + 27% + 45%·27% at the
     top, and the mirror of it here. Moving this number breaks the symmetry the eye actually
     reads, which is square-to-frame, not card-to-frame. */
  top: 60%;
  width: 34%;
  height: 27%;
  z-index: 5;
}
.tapecard__box {
  position: relative;
  height: 100%;
  display: flex;
  gap: 18px;
  align-items: center;
  justify-content: space-between;
  border: 4px solid var(--ink);
  background: #fff;
  padding: 18px 20px;
}
/* Orbitron, the hero lockup's face, so the note reads as the title card talking rather than as
   body copy — and at the SAME size as the title card's subtitle, which is the other half of
   the same lockup. It used to be shrunk by a fixed 6px offset to fit the text column, which
   is the card minus the cassette; the card is wider now and the cassette narrower, so the
   two halves of the hero can read as one voice instead of two. */
.tapecard__list {
  margin: 0;
  padding: 0;
  list-style: none;
  font-family: 'Orbitron', var(--font-display);
  font-weight: 700;
  font-size: clamp(13px, 1.56vw, 24px);
  /* Orbitron's taller caps close the gaps up at 1.2 */
  line-height: 1.32;
  letter-spacing: -0.01em;
}
.tapecard__aside {
  /* pushed out to the right, the way the mockup sets it off from the list above */
  text-align: right;
  padding-left: 0.5em;
}
.vhs {
  position: relative;
  overflow: hidden; /* clips the shine to the cassette's rectangle */
  flex: 0 0 auto;
  width: 31%;
  max-width: 168px;
  line-height: 0;
}
.vhs img {
  width: 100%;
  height: auto;
}
.vhs__shine {
  position: absolute;
  inset: 0;
  /* hard-edged band rather than a soft glow — a blurry gradient looks wrong
     sitting on top of pixel art */
  background: linear-gradient(
    108deg,
    transparent 42%,
    rgb(255 255 255 / 0.15) 45%,
    rgb(255 255 255 / 0.8) 49%,
    rgb(255 255 255 / 0.15) 53%,
    transparent 56%
  );
  transform: translateX(-120%);
  animation: shine 4.6s ease-in-out infinite;
}

@keyframes shine {
  0%,
  55% {
    transform: translateX(-120%);
  }
  85%,
  100% {
    transform: translateX(120%);
  }
}

.hero__actions {
  position: absolute;
  bottom: 1%;
  z-index: 5;
  /*
   * Under the tape card's left edge when there is room, and out of the way when there is not.
   *
   * The squares are sized against the stage's HEIGHT (32cqh), so as the page narrows they
   * take up more of its WIDTH and their left edge creeps leftwards — at around 1200px it
   * reaches this link. Rather than move the link permanently, the clamp tracks that edge:
   *
   *   93.9%      where the inner square's right edge sits (the card's right, 91%, plus its
   *              8.4%-of-card overhang)
   *   - 32cqh    back across the square to its left edge
   *   - 210px    room for the link and the note beneath it, plus a little air
   *
   * so the preferred value IS the rightmost position that still clears the square. The
   * upper bound holds it at 57.4% — the card's left edge, where it belongs — until the
   * squares actually encroach, and the lower bound stops it sliding into the gym prop,
   * which ends at 39%.
   */
  left: clamp(40%, calc(93.9% - 32cqh - 210px), 57.4%);
}
.hero__link {
  font-family: var(--font-display);
  font-size: clamp(12px, 1.4vw, 17px);
  color: var(--blue);
  text-underline-offset: 4px;
}
.hero__note {
  margin: 6px 0 0;
  font-size: 14px;
  color: #666;
  /*
   * Wraps to two lines exactly when it has to, and stays on one when it doesn't.
   *
   * The `.hero__actions` clamp above reserves 210px for this block and then tracks the
   * orange square's left edge — but the note itself had no width at all, so once the squares
   * grew past the reserved allowance the sentence carried on straight underneath one. The
   * link above it never did, because it is short.
   *
   * So the cap is the same arithmetic the clamp uses, read the other way round:
   *
   *   36.5cqw    the gap between the actions' resting position (57.4%) and where the inner
   *              square's right edge sits (93.9%)
   *   - 32cqh    back across the square to its left edge — the same term the clamp subtracts
   *
   * which is the real width available while the block is at rest. Once that falls below the
   * 210px allowance the block has started tracking instead, and 210px IS the room it has, so
   * `max()` holds there. Wide screens get one line; the moment the squares encroach, two.
   */
  max-width: max(210px, calc(36.5cqw - 32cqh));
  line-height: 1.4;
  text-wrap: balance;
}

/* ============================================================ 2. WHY ==== */
.why {
  position: relative;
  overflow: hidden;
  background: var(--blue-flat);
  color: #fff;
  /* The 120px tail was floor space for the barbell prop that used to sit in the bottom-right.
     With it gone that is just a void, so the section matches its neighbours' 96px. */
  padding: 84px 20px 96px;
  border-top: 4px solid var(--ink);
  border-bottom: 4px solid var(--ink);
}
.why__inner {
  max-width: var(--maxw);
  width: 100%;
  margin: 0 auto;
  display: grid;
  grid-template-columns: minmax(200px, 340px) 1fr;
  gap: 56px;
  align-items: start;
}
.why__art img {
  width: 100%;
  border: 4px solid var(--ink);
  box-shadow: 10px 10px 0 rgb(0 0 0 / 0.3);
}
.why__artcap {
  margin: 20px 0 0;
  font-family: var(--font-display);
  font-size: 12px;
  line-height: 1.7;
  color: #d8ecff;
}
.why__copy {
  font-size: 19px;
}
.why__points {
  list-style: none;
  margin: 26px 0;
  padding: 0;
}
.why__points li {
  position: relative;
  padding-left: 30px;
  margin-bottom: 10px;
}
.why__points li::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0.55em;
  width: 14px;
  height: 14px;
  background: var(--green);
  border: 3px solid var(--ink);
}

/* ========================================================= 3. INSIDE ==== */
/* Purple burning down to black, on the original's shape: three stops at 0 / 45 / 100, one smooth
   blend, no hard cut. A single hue makes that shape work where the old violet-to-sage could not —
   there is no opposite hue for sRGB to interpolate through, so the middle stays purple instead of
   going slate. The tail is a true near-black rather than a dark grey-violet, so the section meets
   the ink border at the bottom without a seam.
   Every stop clears 7.6:1 against the white text on top of it. */
.inside {
  padding: 84px 20px 96px;
  background: linear-gradient(135deg, #6d16d6 0%, #35097e 45%, #08010f 100%);
  color: #fff;
  border-bottom: 4px solid var(--ink);
}
.inside__inner {
  /* Only the space between the demo and the copy. The demo's left edge sits flush with the
     container, mirroring the copy's right edge, so both columns are inset from the page by the
     section's own 20px and nothing else. */
  --inside-gap: 60px;
  /* The demo is square, so its height is its width. Pinning the column to an explicit size
     rather than 1fr is what lets the sticky offset below be computed. */
  --demo-size: min(640px, (min(100vw - 40px, 1400px) - var(--inside-gap)) / 2);
  max-width: 1400px;
  width: 100%;
  margin: 0 auto;
  display: grid;
  grid-template-columns: var(--demo-size) 1fr;
  gap: var(--inside-gap);
  align-items: start;
}
/* Sticky *and* vertically centred. The obvious version — a viewport-tall box that centres the
   video inside itself — works, but the box then eats most of the column's height and the
   video unsticks a third of the way down the section. Hugging the video and computing the
   offset that lands it mid-screen keeps it centred for the whole scroll instead. */
.inside__demo {
  position: sticky;
  top: max(
    var(--menubar-h),
    calc(var(--menubar-h) + (100vh - var(--menubar-h) - var(--demo-size)) / 2)
  );
}
.placeholder {
  display: grid;
  place-items: center;
  text-align: center;
  background: #fff;
  color: var(--ink);
  border: 4px solid var(--ink);
  box-shadow: 10px 10px 0 rgb(0 0 0 / 0.3);
  padding: 28px;
}
.placeholder p {
  margin: 0;
  font-family: var(--font-display);
  font-size: 15px;
  line-height: 1.8;
}
.placeholder--video {
  aspect-ratio: 1 / 1;
  width: 100%;
}
.placeholder--montage {
  aspect-ratio: 1 / 1;
}
.inside h2 {
  font-size: clamp(22px, 3vw, 32px);
}
.inside__lede {
  margin: 14px 0 30px;
  font-size: 19px;
  color: #f0ecff;
}
.modules {
  margin: 0;
}
.module {
  padding: 16px 0;
  border-top: 3px solid rgb(255 255 255 / 0.35);
}
.module dt {
  font-family: var(--font-display);
  font-size: 15px;
  margin-bottom: 8px;
}
.module dd {
  margin: 0;
  color: #f2effc;
}
.inside__foot {
  margin-top: 34px;
  padding: 22px;
  background: rgb(0 0 0 / 0.28);
  border: 3px solid var(--ink);
}
.inside__foot h3 {
  font-size: 15px;
  margin-bottom: 10px;
}
.inside__foot p {
  margin: 0;
}

/* ========================================================== 4. PLANS ==== */
.plans {
  padding: 84px 20px 96px;
  background: var(--cream);
  color: var(--brown);
  border-bottom: 4px solid var(--brown);
}
.plans__inner {
  max-width: var(--maxw);
  width: 100%;
  margin: 0 auto;
}
.plans h2 {
  font-size: clamp(22px, 3vw, 32px);
  margin-bottom: 28px;
}
.steps {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 18px;
  margin: 0 0 56px;
  padding: 0;
  list-style: none;
  counter-reset: step;
}
.steps li {
  counter-increment: step;
  background: var(--parchment);
  border: 3px solid var(--brown);
  box-shadow: 6px 6px 0 rgb(59 36 16 / 0.25);
  padding: 42px 18px 18px;
  position: relative;
}
.steps li::before {
  content: counter(step);
  position: absolute;
  top: -3px;
  left: -3px;
  width: 32px;
  height: 32px;
  display: grid;
  place-items: center;
  font-family: var(--font-ui);
  font-weight: 700;
  font-size: 19px;
  background: var(--orange);
  color: #fff;
  border: 3px solid var(--brown);
}

.tiers {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: 24px;
  margin-bottom: 44px;
}
.tier {
  display: flex;
  flex-direction: column;
  background: #fff;
  border: 4px solid var(--brown);
  box-shadow: 8px 8px 0 rgb(59 36 16 / 0.3);
  padding: 26px 24px;
}
.tier--deluxe {
  position: relative;
  background: var(--parchment);
  border-color: var(--ink);
}
.tier__flag {
  position: absolute;
  top: -18px;
  right: 18px;
  margin: 0;
  padding: 6px 12px;
  font-family: var(--font-display);
  font-size: 12px;
  text-transform: uppercase;
  background: var(--blue);
  color: #fff;
  border: 3px solid var(--ink);
}
.tier h3 {
  font-size: 18px;
}
.tier__price {
  margin: 12px 0 4px;
}
/* Silkscreen's 9 reads as a 3 — the headline price came out as "£6.33". Same rule as the body
   text: the display face is for words, never for digits. */
.tier__num {
  font-family: var(--font-ui);
  /* Regular weight, not bold: Jersey 15 has no real bold, and at this size the synthesised one
     thickens strokes until a digit's counter closes up — observed at the old £6.99, which read
     as "£8.99". The price has changed; the rule is why it is still not bold. */
  font-weight: 400;
  font-size: 52px;
  letter-spacing: 0.01em;
}
.tier__per {
  font-size: 17px;
  color: var(--brown-dim);
}
/* The annual price, under the headline monthly one. Deliberately not a second .tier__num:
   two 52px prices in one card read as two products rather than one on two intervals. */
.tier__alt {
  margin: 0 0 4px;
  font-size: 15px;
  color: var(--brown-dim);
}
.tier__tag {
  margin: 0 0 18px;
  color: var(--brown-dim);
}
.tier__list {
  list-style: none;
  margin: 0 0 26px;
  padding: 0;
  flex: 1;
}
.tier__list li {
  position: relative;
  padding-left: 30px;
  margin-bottom: 11px;
  line-height: 1.5;
}
.tier__list li::before {
  position: absolute;
  left: 0;
  top: 0;
  font-family: var(--font-display);
  font-size: 14px;
}
.tier__list .yes::before {
  content: '+';
  color: #0a8a3c;
}
.tier__list .no {
  color: var(--brown-dim);
}
.tier__list .no::before {
  content: 'x';
  color: #b3211f;
}
/* Not shipped yet, and dated. Its own marker rather than a `yes` with a caveat in the text:
   a plus sign next to something you cannot use is a promise the product does not keep on the
   day somebody pays. */
.tier__list .soon {
  color: var(--brown-dim);
}
.tier__list .soon::before {
  content: '»';
  color: var(--blue);
}
.tier__list .soon .when,
.module__when {
  display: inline-block;
  margin-left: 6px;
  padding: 1px 7px;
  border: 2px solid var(--blue);
  background: #fff;
  color: var(--blue);
  font-family: var(--font-display);
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  vertical-align: 2px;
}
.module__when {
  margin-left: 8px;
}
.tier .btn {
  align-self: flex-start;
}

.extras {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 20px;
}
.extra {
  background: var(--parchment-2);
  border: 3px solid var(--brown);
  padding: 20px;
}
.extra h4 {
  font-size: 14px;
  margin-bottom: 10px;
}
.extra p {
  margin: 0;
}

/* ======================================================== 5. ACCOUNT ==== */
.account {
  padding: 84px 20px 96px;
  background: var(--dark);
  color: #fff;
}
.account__inner {
  max-width: 1400px;
  width: 100%;
  margin: 0 auto;
  display: grid;
  grid-template-columns: minmax(300px, 420px) 1fr;
  gap: 60px;
  align-items: center;
}
.authcard {
  background: var(--dark-2);
  border: 4px solid #000;
  box-shadow: 10px 10px 0 rgb(0 0 0 / 0.5);
  padding: 30px 28px;
}
.authcard__tabs {
  display: flex;
  gap: 8px;
  margin-bottom: 22px;
}
.authtab {
  flex: 1;
  font-family: var(--font-display);
  font-size: 12px;
  text-transform: uppercase;
  padding: 11px 8px;
  background: #3a3a3a;
  color: #cfcfcf;
  border: 3px solid #000;
  cursor: pointer;
}
.authtab.is-active {
  background: var(--orange);
  color: #fff;
}
.authform h2 {
  font-size: clamp(24px, 3.4vw, 34px);
  text-align: center;
  margin-bottom: 22px;
}
.field {
  display: block;
  margin-bottom: 14px;
}
.field__label {
  display: block;
  font-size: 14px;
  color: #b9b9b9;
  margin-bottom: 5px;
}
.field input {
  width: 100%;
  font-family: var(--font-mono);
  font-size: 21px;
  line-height: 1.2;
  padding: 9px 11px;
  color: var(--ink);
  background: #fff;
  border: 3px solid #000;
  border-radius: 0;
}
/* :user-invalid, not :invalid — an empty required field is invalid from the moment it renders,
   which painted every box red before the visitor had typed anything. */
.field input:user-invalid {
  border-color: var(--red);
}
.field__hint {
  margin: 6px 0 0;
  font-size: 14px;
  line-height: 1.45;
  color: #9b9b9b;
}
.field__hint strong {
  color: #d8d8d8;
  font-weight: 400;
}
/* Show/hide password. The button sits inside the input's own box rather than beside it, so the
   field keeps its single hard-edged rectangle; the input reserves room for it with padding
   instead of the button overlapping the text, which would hide the end of a long password. */
.field__reveal {
  position: relative;
  display: block;
}
.field__reveal input {
  padding-right: 84px;
}
.reveal {
  position: absolute;
  top: 3px;
  right: 3px;
  bottom: 3px;
  padding: 0 12px;
  font-family: var(--font-display);
  font-size: 11px;
  text-transform: uppercase;
  color: var(--ink);
  background: var(--parchment-2);
  border: 0;
  border-left: 3px solid #000;
  cursor: pointer;
}
.reveal:hover {
  background: var(--orange-lit);
}
.reveal[aria-pressed='true'] {
  background: var(--blue-flat);
  color: #fff;
}
.reveal:focus-visible {
  outline: 3px solid var(--orange);
  outline-offset: -3px;
}
.authform__actions {
  display: flex;
  align-items: stretch;
  gap: 12px;
  margin-top: 20px;
}
.authform__error {
  margin: 6px 0 0;
  font-size: 15px;
  color: #ff8b8b;
}
/* The health-metrics tick. Deliberately looks like a choice rather than a formality — it
   is the one control on this form that changes what the service will store. */
.field__consent {
  display: flex;
  gap: 10px;
  align-items: flex-start;
  margin: 4px 0 0;
  font-size: 14px;
  line-height: 1.5;
  color: #cfd6de;
  cursor: pointer;
}
.field__consent input {
  width: 18px;
  height: 18px;
  margin: 2px 0 0;
  flex: none;
  accent-color: var(--orange);
}

/* The agreement notice sits with the button that makes the agreement, so it is quiet but
   unmissable — dimmer than the form's own copy, with the two links picked out. */
.authform__legal {
  margin: 14px 0 0;
  font-size: 14px;
  line-height: 1.5;
  color: #9aa2ad;
}
.authform__legal a {
  color: #4da3ff;
}

.authform__forgot {
  display: block;
  margin-top: 16px;
  text-align: center;
  font-size: 15px;
  color: #4da3ff;
}
.authcard__status {
  margin: 18px 0 0;
  font-size: 15px;
  line-height: 1.55;
  color: #ffd08a;
  min-height: 1.55em;
}
.account__montage {
  /* fills its column and stays centred in it, capped so it can't outgrow the viewport */
  width: 100%;
  max-width: min(760px, calc(100vh - var(--menubar-h) - 120px));
  justify-self: center;
}
.account__montage .placeholder {
  background: #f2f2f2;
  border-color: #9a9a9a;
  box-shadow: 10px 10px 0 rgb(0 0 0 / 0.5);
}

/* =========================================================== FOOTER ===== */
.foot {
  background: var(--bar);
  color: #fff;
  border-top: 4px solid #000;
  padding: 30px 20px;
  text-align: center;
  font-size: 15px;
}
.foot p {
  margin: 0 0 6px;
}
.foot__links {
  font-size: 14px;
}
.foot__links a {
  color: #cfd6de;
  text-underline-offset: 3px;
}
.foot__links a:hover {
  color: #fff;
}

.foot__dim {
  color: #9aa2ad;
  font-size: 14px;
}

/* ======================================================= RESPONSIVE ===== */
@media (max-width: 980px) {
  .why__inner,
  .inside__inner,
  .account__inner {
    grid-template-columns: 1fr;
    gap: 34px;
  }
  .inside__demo {
    position: static;
  }
  .why {
    padding-bottom: 60px;
  }
  .account__montage {
    max-width: 440px;
  }
}

/* Below this the absolutely-positioned hero composition has no room, so it
   unwinds into ordinary stacked flow and the props line up along the floor. */
@media (max-width: 900px) {
  .hero__frame {
    padding: 16px;
  }
  .hero__frame {
    flex: 0 0 auto;
    display: block;
  }
  .hero__stage {
    min-height: 0;
    display: flex;
    flex-direction: column;
    gap: 22px;
    /* OFF here, and it must be. `container-type: size` (see the desktop rule) makes an
       element's size independent of its contents in BOTH axes — which is what lets the
       orange squares be measured against the stage up there, and what collapses the stage to
       zero height down here, where it is an ordinary flex column that gets its height from
       the cards inside it. Everything then spills out of the frame and over the section
       below. This layout sizes the squares off width instead, so it needs no container. */
    container-type: normal;
  }
  /* relative, not static: the offset frames are absolute children of these cards, and going
     static would re-anchor them to .hero__stage — they blew up to fill the whole section */
  .titlecard,
  .tapecard {
    position: relative;
    inset: auto;
    width: auto;
    /* Both back to content height: stacked in one column there is no stage to measure
       against, and the fixed 27% they carry on desktop would crop the copy once it wraps. */
    height: auto;
  }
  .hero__actions {
    position: static;
    width: auto;
    /* The desktop rule tracks the tape card's squares. Stacked in one column there are no
       squares to dodge. */
    left: auto;
  }
  .hero__note {
    /* And with no square to dodge there is nothing to measure against — cqw/cqh resolve to
       zero here anyway, because container-type is normal in this layout. */
    max-width: none;
  }
  /* Sized off width here, not height: once the copy wraps on a narrow screen the cards get
     tall, and a square measured against that height was wider than the phone. Both cards are
     full width in this layout, so one width percentage gives both sets the same size — which
     is the same rule as the desktop layout, reached by a different measurement. */
  .titlecard__stack,
  .tapecard__stack {
    height: auto;
    width: 32%;
    border-width: 7px;
  }
  /* The same mirrored pair as the desktop layout — title card top-left, tape card
     bottom-right — at offsets small enough that the outer square still clears the frame.
     At -11% it started exactly on the frame's inside edge and read as clipped. */
  .titlecard__stack {
    left: -4%;
    top: -7%;
  }
  .titlecard__stack--2 {
    left: -8%;
    top: -13%;
  }
  .tapecard__stack {
    left: auto;
    right: -4%;
    top: auto;
    bottom: -7%;
  }
  .tapecard__stack--2 {
    left: auto;
    right: -8%;
    top: auto;
    bottom: -13%;
  }
  /* Inset by the same amount, in opposite directions: one card indented from the left, the
     other from the right, so the column reads as two offset blocks. Equal insets also mean
     equal card widths, which is what makes the orange squares — sized at 32% of their card —
     come out the same size on both. They were 13% and 7%, and the squares differed by that
     ratio. */
  .titlecard {
    margin: 34px 0 0 10%;
  }
  .tapecard {
    margin-right: 10%;
  }
  .speedlines,
  .zzz {
    display: none;
  }
  .tapecard__box {
    flex-direction: column;
    align-items: flex-start;
  }
  /* Still the same size as the title card's subtitle, as on desktop — the two halves of the
     hero are one voice. The cassette drops below the note here, so nothing is competing for
     the width and both can take the same clamp. */
  .tapecard__list {
    font-size: clamp(13px, 1.56vw, 24px);
  }
  .vhs {
    width: 130px;
    align-self: center;
  }
  .prop {
    position: static;
  }
  .prop--bag {
    display: none;
  }
  .hero__stage::after {
    /* floor line the props sit on */
    content: '';
    order: 9;
    height: 4px;
    background: var(--ink);
  }
  .prop--gym {
    order: 10;
    width: 100%;
    margin-top: -4px;
  }
  .menubar__nav a {
    padding: 6px 7px;
    font-size: 13px;
  }
  .menubar__brand span {
    display: none;
  }
  .authform__actions {
    flex-direction: column;
    align-items: stretch;
  }
}

/* On a phone the four nav items plus the CTA wrap the fixed bar onto two lines, which then
   sits over the content. Both dropped links are just anchors further down the same page. */
@media (max-width: 560px) {
  .menubar__nav a[href='#why'],
  .menubar__nav a[href='#inside'] {
    display: none;
  }
  .menubar__cta {
    font-size: 12px;
  }
}

/* Motion is decoration here — the page reads identically without it. */
@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }
  .prop--bag,
  .z,
  .vhs__shine {
    animation: none;
  }
  .z {
    opacity: 1;
  }
  .z--1 {
    transform: translate(0, -30px);
  }
  .z--2 {
    transform: translate(-18px, -100px);
  }
  .z--3 {
    transform: translate(0, -170px);
  }
  .btn {
    transition: none;
  }
}

/* The price, on the sign-up card itself. Deliberately not quiet: it is the one fact a
   person needs before typing an email, and the paywall is a bad place to learn it. */
.authform__price {
  border-left: 3px solid var(--orange, #f59e0b);
  padding-left: 0.75rem;
}
