/*
 * StepKnock. Page-local styles.
 *
 * Boundaries, so the next person does not have to guess:
 *   sk-*        design system. Vendored at ds/. Never restyled here.
 *   sk-plate-*  this build's chrome. Ours.
 *   sk-mast-*   this build's masthead. Ours.
 *   sk-hero     this build's hero sizing. Ours.
 *   sk-lit      this build's lit half of the world. Ours.
 *   sk-door     this build's vault door target. Ours.
 *   sc-*        the scrollcraft engine. Themed by token, never edited.
 *
 * Token discipline, stated accurately rather than aspirationally:
 *
 *   Every brand colour, every space step, every radius, every duration and
 *   every type step resolves to a design system token. None is written literally.
 *
 *   Three things are literal, on purpose:
 *     - the sun's two source colours, declared once below with the reason
 *     - the shadow tints under the plate, which match the rgba the design
 *       system's own shadow tokens use and exist to swing with the light
 *     - optical pixel values that are not spacing: a 2px lip, a 28px fade,
 *       a 44px tap target, the shadow's travel
 */

/* ------------------------------------------------------------- theming ---- *
 * The engine's six colour roles and two fonts, pointed at StepKnock tokens.
 * This is the whole of the engine's theming surface.
 */
:root {
  --sc-canvas:     var(--sk-color-background);
  --sc-surface:    var(--sk-color-surface);
  --sc-ink:        var(--sk-color-text-primary);
  --sc-ink-soft:   var(--sk-color-text-secondary);
  --sc-accent:     var(--sk-color-accent);
  --sc-accent-ink: var(--sk-color-ink-900);

  --sc-font-display: var(--sk-font-family-heading);
  --sc-font-text:    var(--sk-font-family-body);

  /* Width the plate occupies on a wide screen. The copy layer is inset by this
     so a headline never runs underneath the form. */
  --plate-w: clamp(360px, 30vw, 420px);

  /* Height the sheet occupies on a phone. The world keeps the rest. */
  --sheet-h: 46vh;

  /*
   * ONE NUMBER RUNS THE WHOLE PAGE.
   *
   * page.js writes --sun as 0 through 1: 0 while the model is in night,
   * ramping across the leg marked data-sun-sweep, 1 once the visitor is round
   * into daylight. It is derived from scroll position measured against the leg
   * weights, which is the same track measure the engine runs the flight on.
   *
   * It drives two things that are really one thing:
   *   - the light front that wipes the lit world in over the dark one
   *   - the light falling on the bench plate, which is the signature move
   *
   * The vault door latches it to 1 permanently, at any scroll position.
   *
   * Defaults are the night state, so the page is correct before the first
   * frame of script runs. The engine is not touched by any of this.
   */
  --sun: 0;

  /*
   * The two ends of the light, and the one non-token colour in this build.
   *
   * The night rim is the corroded orange of a failure light. The StepKnock
   * palette has no orange, correctly, because it is a UI palette and this is a
   * light source in a photographed world rather than an interface colour. It is
   * declared once, here, and used only for the plate's edge light and specular.
   * It never touches text, a control, or a state.
   *
   * The day rim is the brand's own cyan accent, used small, which is the only
   * way that token is allowed to be used.
   */
  --sun-rim-night: #C2571E;
  --sun-spec-color: #78604A;

  /* Live values. page.js interpolates and overwrites these as the sun crosses.
     The defaults are the night state, so the page is correct before the first
     frame of script runs. */
  --sun-rim: var(--sun-rim-night);
  --sun-spec: var(--sun-spec-color);
  --sun-spec-a: 0;
}

html { background: var(--sk-color-background); }

body {
  /* The world is fixed and the spacer carries the scroll, so nothing here
     should ever produce a horizontal bar. */
  overflow-x: hidden;
}

/* ====================================================== THE LIGHT FRONT ==== *
 *
 * Each leg is two clips stacked in the same box. The dark one is the segment's
 * own video and the engine drives it. The lit one is inside .sk-lit, which the
 * engine never looks inside, and page.js keeps its playhead on the dark one's.
 *
 * .sk-lit is masked so it is only present to the left of the light front. As
 * --sun runs 0 to 1 the front travels left to right across the viewport and
 * the world turns from corroded night to lit day underneath it, in place, on
 * the same camera move. The plate is docked right, so the light reaches the
 * form last. That is not a coincidence, it is why the plate is on that side.
 *
 * ---------------------------------------------------------------------------
 * THE FRONT IS A 30% SOFT GRADIENT BAND AND IT CANNOT BE MADE CRISPER.
 * This is load-bearing. Do not tighten it to "clean up" the wipe.
 *
 * The two halves are frame-locked only at their head and tail anchors. Nothing
 * locks the middle, and the model does not return two identical camera moves
 * from identical prompts. Measured across all five legs, the dark and lit
 * halves drift apart mid-clip by up to 40px of vertical registration at 960px
 * wide, roughly 4% of frame height, and the excursion is mid-clip rather than
 * at the ends: the head and tail are the BEST registered moments, not the
 * worst.
 *
 * A hard edge, or a narrow one, puts that 40px of disagreement on a single
 * line, where the eye reads it instantly as two photographs sliding against
 * each other. A band 30% of the viewport wide spreads the same disagreement
 * over roughly 400px, which is below the threshold at which a soft luminance
 * ramp reads as an edge at all. The band absorbs the drift. That is its job.
 *
 * Two things were tried and did not work, so nobody spends the credits again:
 *   - splitting a leg into two shorter frame-locked 5s clips. Measured, and
 *     disproven: drift is not length-driven. The 5s legs came back at 40px and
 *     the 10s peak at 34px.
 *   - giving the two halves different camera-move text to compensate. Worse.
 *     Same words in is the only lever there is.
 *
 * The 30% is set from the WORST leg with headroom, not from the average. If
 * the anchors are ever regenerated, re-measure with tools/seam-measure.py
 * before narrowing this. Narrowing it without new footage just puts the seam
 * back.
 * ---------------------------------------------------------------------------
 *
 * The front over-travels from -0.15 to 1.15 so that at --sun 0 the band sits
 * fully off the left edge and at --sun 1 fully off the right. Without the
 * over-travel a 15% smear of night would still be clinging to the right hand
 * edge of a world that is supposed to have finished turning.
 */
.sk-lit {
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  --front: calc(-0.15 + var(--sun) * 1.3);
  -webkit-mask-image: linear-gradient(to right,
    #000 calc(var(--front) * 100% - 15%),
    transparent calc(var(--front) * 100% + 15%));
  mask-image: linear-gradient(to right,
    #000 calc(var(--front) * 100% - 15%),
    transparent calc(var(--front) * 100% + 15%));
}

.sk-lit__poster,
.sk-lit__clip {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* The same handover the engine performs on the dark half: the poster holds the
   frame until a real decoded frame exists, then steps aside. page.js adds
   is-ready once the lit clip has actually painted. */
.sk-lit__poster { z-index: 0; transition: opacity 420ms var(--sc-ease-out); }
.sk-lit__clip { z-index: 1; opacity: 0; }
.sk-lit.is-ready .sk-lit__poster { opacity: 0; }
.sk-lit.is-ready .sk-lit__clip { opacity: 1; }

/* ======================================================== THE VAULT DOOR === *
 *
 * The trigger. A real button, sitting on the door in the frame, carrying no
 * hint, no tooltip, no pulse and no instruction. It is found by tab, by touch,
 * or by trying the door because it is a door.
 *
 * It is invisible until focused, and then it is unmistakable. That is the
 * whole visual design: an unlabelled control that announces itself only to
 * someone navigating by keyboard, who otherwise has no way at all to know it
 * is there.
 *
 * page.js positions it every frame from the interpolated door fractions on the
 * active segment, mapped through the same arithmetic object-fit: cover uses,
 * so it lands on the door at any viewport shape rather than only the one it
 * was calibrated at. Its x is clamped clear of the plate.
 */
.sk-door {
  position: fixed;
  left: 0;
  top: 0;
  /* Above the copy layer at 20, so a legible copy block cannot swallow the
     click. Below the plate at 30, so it can never cover the form. */
  z-index: 21;
  width: var(--door-d, 44px);
  height: var(--door-d, 44px);
  transform: translate(calc(var(--door-x, 50vw) - 50%), calc(var(--door-y, 50vh) - 50%));
  appearance: none;
  background: none;
  border: 0;
  padding: 0;
  border-radius: var(--sk-radius-full);
  cursor: pointer;
}

.sk-door:focus-visible {
  outline: 2px solid var(--sk-color-focus-ring);
  outline-offset: 3px;
}

/* Once the world is latched lit, the door has done the only thing it does. It
   stays in the accessibility tree reporting its state, and stops offering a
   cursor for an action it can no longer perform. */
.sk-door[aria-pressed="true"] { cursor: default; }

/* ------------------------------------------------------------- masthead --- */
.sk-mast {
  position: fixed;
  top: var(--sk-space-5);
  left: var(--sk-space-5);
  z-index: 40;
  display: flex;
  align-items: center;
  gap: var(--sk-space-3);
  pointer-events: none;
}

.sk-mast__word {
  font-family: var(--sk-font-family-heading);
  font-size: var(--sk-font-size-h3);
  font-weight: var(--sk-font-weight-medium);
  letter-spacing: var(--sk-letter-spacing-h3);
  color: var(--sk-color-text-primary);
  text-shadow: 0 1px 3px rgba(6, 14, 18, 0.7);
}

/* ------------------------------------------------------------ world copy -- */
/*
 * The copy layer is inset so it never runs under the plate. The engine sets
 * position fixed and inset 0 on this element; only the inset is overridden.
 */
.sc-world__copy { right: var(--plate-w); }

.sc-copy--lead {
  max-width: min(38rem, calc(100vw - var(--plate-w) - 2 * var(--sk-space-10)));
  display: grid;
  gap: var(--sk-space-5);
}

/*
 * Hero type. The design system's display step is a desktop floor, and this
 * headline is thirteen words. Full size on a wide screen, stepped down on a
 * phone so it does not eat the viewport. The copy is not cut.
 */
.sk-hero {
  text-wrap: balance;
  margin: 0;
}

/* ---------------------------------------------------------------- plate --- *
 * The bench plate. Fixed, present from the first pixel, never scrolls away.
 * It is a raised object: offset shadow, edge light, its own grain.
 */
.sk-plate {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  width: var(--plate-w);
  z-index: 30;
  display: flex;
  flex-direction: column;
  background-color: var(--sk-color-surface);
  border-left: 1px solid var(--sk-color-border);
  /* The light source swings as the sun crosses, so the plate's own cast shadow
     swings with it. Offset and blur, never a zero-offset halo. */
  box-shadow:
    calc((var(--sun) - 0.5) * -22px) 0 48px rgba(6, 14, 18, 0.55),
    var(--sk-shadow-lg);
  isolation: isolate;
}

/* Edge light along the lip that faces the world. A real lip catches light, and
   which light it catches is the whole move. */
.sk-plate::before {
  content: "";
  position: absolute;
  inset-block: 0;
  left: -1px;
  width: 2px;
  z-index: 2;
  pointer-events: none;
  background: var(--sun-rim);
  opacity: calc(0.35 + var(--sun) * 0.45);
}

/* The specular band. It travels down the plate as the terminator passes, and it
   is only really present during the crossing itself. */
.sk-plate::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background: linear-gradient(
    to bottom,
    transparent 0%,
    var(--sun-spec) calc(var(--sun) * 130% - 15%),
    transparent 100%);
  /* Peaks mid-crossing and is absent at both ends. Computed in page.js rather
     than with CSS abs(), which is too recent to rely on here. */
  opacity: var(--sun-spec-a, 0);
  mix-blend-mode: screen;
}

.sk-plate__inner {
  position: relative;
  z-index: 3;
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  display: flex;
  flex-direction: column;
  gap: var(--sk-space-6);
  /* The bottom step is deliberately large: it is the scrollable room the last
     field needs in order to clear the fade below, which scroll-padding alone
     cannot provide because nothing follows it. */
  padding: var(--sk-space-8) var(--sk-space-8) var(--sk-space-12);
  /*
   * The fields are taller than the plate on a short viewport, so this region
   * scrolls. Without a soft bottom edge the last field is sliced off flat by
   * the action band and reads as a rendering fault rather than as "there is
   * more below". The fade is the affordance.
   */
  -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 28px), transparent 100%);
  mask-image: linear-gradient(to bottom, #000 calc(100% - 28px), transparent 100%);
  /*
   * The fade has a cost: a control tabbed to at the bottom edge lands inside it
   * and its focus ring fades out with it. Measured at -28px on the last field,
   * which is a focus indicator nobody can see. Scroll padding keeps anything
   * focused clear of the gradient.
   */
  scroll-padding-bottom: var(--sk-space-10);
}

/* Night dust on the plate, settling as the light arrives. */
.sk-plate__inner::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 0;
  opacity: calc((1 - var(--sun)) * 0.10);
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='120' height='120'><filter id='d'><feTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='3'/></filter><rect width='120' height='120' filter='url(%23d)'/></svg>");
}

.sk-plate__inner > * { position: relative; z-index: 1; }

.sk-plate__title {
  font-family: var(--sk-font-family-heading);
  font-size: var(--sk-font-size-h2);
  line-height: var(--sk-line-height-h2);
  letter-spacing: var(--sk-letter-spacing-h2);
  font-weight: var(--sk-font-weight-medium);
  color: var(--sk-color-text-primary);
  margin: 0;
  text-wrap: balance;
}

.sk-plate__form {
  display: grid;
  gap: var(--sk-space-5);
}

/* The submit sits on the design system's own primary fill. page.js moves it
   across the brand's teal range as the sun crosses, by writing the fill token
   on this scope. It is never dimmed to the point of reading as disabled. */
.sk-plate__submit { margin-top: 0; }

.sk-plate__reassure { margin: 0; }

/* ----------------------------------------------------------- form status -- *
 * The one thing the reader sees after pressing Send it over. It sits in the
 * pinned band beside the button rather than anywhere in the scrolling form,
 * because the form scrolls behind the band and a confirmation the reader has
 * to scroll to find is not a confirmation.
 *
 * Typography comes from sk-caption. Only colour is set here, and only through
 * tokens: secondary while in flight, accent on success, error on failure. All
 * three clear 4.5:1 on the surface the band paints (error is the closest at
 * 5.4:1). No design system class is restyled.
 */
.sk-plate__status {
  margin: 0;
  color: var(--sk-color-text-secondary);
}

.sk-plate__status[data-state="ok"] { color: var(--sk-color-accent); }
.sk-plate__status[data-state="error"] { color: var(--sk-color-error); }

/* ------------------------------------------------------------ base band -- *
 * Pinned by flex rather than scrolling with the form.
 *
 * This is not only a layout preference. The plate is a fixed element whose
 * height is exactly the viewport, so a flex child at its end has bottom equal
 * to the fold. Put the footer inside the scrolling region instead and its
 * children sit below the fold, which the worldflight assertion reads, correctly
 * on its own terms, as content in document flow.
 */
.sk-plate__base {
  flex: 0 0 auto;
  position: relative;
  z-index: 3;
  display: grid;
  gap: var(--sk-space-3);
  padding: var(--sk-space-5) var(--sk-space-8) var(--sk-space-6);
  border-top: 1px solid var(--sk-color-divider);
  background-color: var(--sk-color-surface);
}

/* --------------------------------------------------------- waypoint rail -- */

.sk-plate__rail { margin-top: var(--sk-space-2); }

.sk-plate__rail-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: var(--sk-space-1) var(--sk-space-2);
}

.sk-plate__rail-btn {
  appearance: none;
  background: none;
  border: 0;
  padding: var(--sk-space-1) var(--sk-space-2);
  border-radius: var(--sk-radius-sm);
  font-family: var(--sk-font-family-body);
  font-size: var(--sk-font-size-caption);
  line-height: var(--sk-line-height-caption);
  letter-spacing: var(--sk-letter-spacing-caption);
  color: var(--sk-color-text-secondary);
  cursor: pointer;
  transition: var(--sk-transition-control);
}

.sk-plate__rail-btn:hover { color: var(--sk-color-text-primary); }

.sk-plate__rail-btn:focus-visible {
  outline: 2px solid var(--sk-color-focus-ring);
  outline-offset: 2px;
}

.sk-plate__rail-btn[aria-current="true"] {
  color: var(--sk-color-accent);
  background-color: var(--sk-color-teal-wash);
}

.sk-plate__rail-btn:active { transform: translateY(1px); }

/* --------------------------------------------------------------- footer --- */
.sk-plate__foot {
  display: flex;
  align-items: center;
  gap: var(--sk-space-3);
}

.sk-plate__foot p { margin: 0; }

.sk-plate__foot-text {
  display: grid;
  gap: var(--sk-space-1);
  min-width: 0;
}

/* The only two off-page links on the page. Spaced far enough apart to be
   separate targets rather than one run of underlined text. */
.sk-plate__legal {
  display: flex;
  flex-wrap: wrap;
  gap: 0 var(--sk-space-4);
}

/* ----------------------------------------------------------- phone ------- *
 * The sheet. No tap, no scroll: the title and the first fields are on screen
 * from the first frame, and the rest of the form is reached by scrolling inside
 * the sheet, which never moves the page.
 */
@media (max-width: 1023px) {
  :root { --plate-w: 100vw; --sheet-h: 54vh; }

  /*
   * Sheet economy. The first shoot at 390x844 showed the buyer a submit button
   * and not one usable field: the title, the waypoint labels and the footer
   * logo had eaten the whole sheet. On the device most of these visitors are
   * on, the fields and the button are the page. Everything else gives way.
   */

  /* The hero directly above already says this, word for word. Kept in the
     accessibility tree because it is the section's label. */
  .sk-plate__title {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
  }

  /* One mark on screen is enough at this size, and the brand floor is 40px so
     it cannot simply be shrunk. The masthead keeps it. */
  .sk-plate__foot .sk-logo { display: none; }

  /* Waypoints become dots. The label stays as the accessible name, so the world
     is still skippable rather than a video, which is what the grammar asks for.
     44px targets, because this is a thumb. */
  .sk-plate__rail-list { flex-wrap: nowrap; gap: var(--sk-space-1); }

  .sk-plate__rail-btn {
    font-size: 0;
    line-height: 0;
    width: 44px;
    height: 44px;
    padding: 0;
    display: grid;
    place-items: center;
  }

  .sk-plate__rail-btn::before {
    content: "";
    display: block;
    width: 10px;
    height: 10px;
    border-radius: var(--sk-radius-full);
    background: currentColor;
  }

  .sk-plate__rail-btn[aria-current="true"]::before {
    box-shadow: 0 0 0 4px var(--sk-color-teal-wash);
  }

  .sk-plate {
    top: auto;
    height: var(--sheet-h);
    width: 100%;
    border-left: 0;
    border-top: 1px solid var(--sk-color-border);
    box-shadow:
      0 calc((var(--sun) - 0.5) * -14px) 40px rgba(6, 14, 18, 0.6),
      var(--sk-shadow-lg);
  }

  /* The lip that catches the light is the top edge on a phone, not the side. */
  .sk-plate::before {
    inset-block: auto;
    inset-inline: 0;
    top: -1px;
    left: auto;
    width: auto;
    height: 2px;
  }

  .sk-plate__inner {
    padding: var(--sk-space-6) var(--sk-space-5) var(--sk-space-6);
    gap: var(--sk-space-5);
  }

  /*
   * The band pays for the footer, not the fields.
   *
   * Adding the full postal address and the two legal links grew this band by
   * about 40px, and on a 360px phone that came straight out of the scrolling
   * region: one field fully visible instead of two. The address and the links
   * are not negotiable and the sheet stays at 54vh, so the space comes back
   * out of this band's own padding and gaps instead.
   *
   * Nothing here shrinks type. Legal text at 14px stays at 14px.
   */
  .sk-plate__base {
    gap: var(--sk-space-2);
    padding: var(--sk-space-3) var(--sk-space-5) var(--sk-space-4);
  }

  .sk-plate__rail { margin-top: 0; }

  /* The address wraps to two lines at this width and the links sit under it.
     Tighter leading between those three lines only, never inside them. */
  .sk-plate__foot-text { gap: 0; }

  /* Copy sits in the band above the sheet. */
  .sc-world__copy { right: 0; bottom: var(--sheet-h); }

  .sc-copy--lead {
    max-width: none;
    left: var(--sk-space-5);
    right: var(--sk-space-5);
    bottom: var(--sk-space-6);
    gap: var(--sk-space-4);
  }

  /* The band scrim tops out at 58% of the frame. The copy band is shorter on a
     phone, so the density is pulled up to cover where the type actually sits. */
  .sc-world__scrim {
    background: linear-gradient(to top,
      color-mix(in oklab, var(--sc-canvas) 96%, transparent) 0%,
      color-mix(in oklab, var(--sc-canvas) 82%, transparent) 34%,
      color-mix(in oklab, var(--sc-canvas) 40%, transparent) 60%,
      transparent 78%);
  }

  /* Thirteen words at the desktop display floor wraps to six lines at 390px.
     One rung down, copy intact. */
  .sk-hero {
    font-size: var(--sk-font-size-h2);
    line-height: var(--sk-line-height-h2);
    letter-spacing: var(--sk-letter-spacing-h2);
  }

  .sc-lede { font-size: var(--sk-font-size-body); line-height: var(--sk-line-height-body); }

  .sk-mast { top: var(--sk-space-4); left: var(--sk-space-4); }
  .sk-mast__word { font-size: var(--sk-font-size-body); }
}

/* Smaller phones get MORE sheet, not less: the fields do not shrink but the
   world can. 52vh here was a leftover from the 46vh default and made the
   narrowest screens the worst ones. */
@media (max-width: 380px) {
  :root { --sheet-h: 60vh; }
}

/* -------------------------------------------------------- reduced motion -- *
 * Fewer and gentler, not zero. The sun still crosses, because it is meaning
 * rather than decoration: it is how the page says the world turned. What goes
 * is the travelling specular band and the swinging shadow, both of which are
 * pure movement.
 *
 * The lit layer and its light front stay too. The engine fetches no clip at
 * all under reduced motion, so both halves are held by their posters, and the
 * front wipes one still over the other. The world still turns from night to
 * day. It just does it without the camera move.
 */
@media (prefers-reduced-motion: reduce) {
  .sk-plate {
    box-shadow: var(--sk-shadow-lg);
  }
  .sk-plate::after { display: none; }
  .sk-plate__rail-btn:active { transform: none; }
  .sk-lit__poster { transition: none; }
}
