/* Parlimetrics promo site, milestone 2.
   All colours live in the :root block below. No colour values elsewhere. */

:root {
  --ink: #0A0B0D;
  --ink-2: #111315;
  --ink-3: #191B1D;
  --hairline: #212529;
  --hairline-2: #31363C;
  --text-3: #6E757C;
  --text-2: #A2A8AE;
  --text: #E6E7E8;
  --data-dim: #2E4860;
  --data: #4D79A3;
  --data-hi: #819FBB;
  --lime: #92D050;
  --lime-dim: #5E8235;
  --max-width: 1120px;
}

* {
  box-sizing: border-box;
}

/* The glow is 114% of a heading that is itself 88vw, or 92vw on small
   screens, so it is wider than the viewport below about 1136px and would
   otherwise push out a horizontal scrollbar. The clip is set on both html
   and body because overflow on body alone is propagated to the viewport
   and leaves body itself visible, which prevents nothing. It is "clip"
   rather than "hidden" so no scroll container is created and overflow-y
   stays visible. The boundary is the viewport edge, so nothing on screen
   is affected: both gradients reach zero alpha at the edges of the glow
   box, measured at page background luminance. */
html,
body {
  margin: 0;
  padding: 0;
  overflow-x: clip;
}

body {
  background: var(--ink);
  color: var(--text);
  font-family: "Inter", system-ui, "Segoe UI", Arial, sans-serif;
  font-weight: 400;
  line-height: 1.6;
}

::selection {
  background: var(--hairline-2);
  color: var(--text);
}

/* Hero layout: one viewport tall, content centred, footer at the bottom. */

.hero {
  height: 100vh;
  height: 100svh;
  max-width: var(--max-width);
  margin: 0 auto;
  padding: 16px 32px 14px;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
}

/* Only the strata graphic gives up height when the viewport is tight.
   Everything else holds its natural size. */
.hero > * {
  flex-shrink: 0;
}

/* The wordmark is the page heading. It carries no text of its own, so the
   img alt supplies the accessible name. */
.wordmark-heading {
  position: relative;
  /* Establishes a stacking context so the glow, at z-index -1, sits behind
     the wordmark without dropping behind the page background. */
  z-index: 0;
  width: 88vw;
  max-width: 1000px;
  margin: auto auto 0;
  padding: 0;
  font-size: 0;
  font-weight: 400;
  line-height: 0;
  list-style: none;
}

/* The glow. Painted behind the wordmark, sized against the heading box.
   Stop values come from an approved reference render, so they are written
   out in full rather than reduced to a two stop shorthand. */
.wordmark-heading::before {
  content: "";
  position: absolute;
  z-index: -1;
  width: 114%;
  left: -7%;
  height: 146%;
  /* Centres the glow at 92% of the heading height: 92% minus half of 146%. */
  top: calc(92% - 73%);
  pointer-events: none;
  background:
    radial-gradient(ellipse 25% 23% at 50% 47%,
      rgba(146,208,80,0.160) 0%, rgba(146,208,80,0.105) 15%, rgba(146,208,80,0.063) 30%,
      rgba(146,208,80,0.034) 45%, rgba(146,208,80,0.015) 60%, rgba(146,208,80,0.004) 75%,
      rgba(146,208,80,0) 100%),
    radial-gradient(ellipse 50% 50% at 50% 50%,
      rgba(150,176,200,0.260) 0%, rgba(150,176,200,0.170) 15%, rgba(150,176,200,0.103) 30%,
      rgba(150,176,200,0.055) 45%, rgba(150,176,200,0.024) 60%, rgba(150,176,200,0.007) 75%,
      rgba(150,176,200,0) 100%);
}

/* The fade towards the letterforms' feet now lives inside the asset, as a
   gradient mask in parlimetrics-wordmark-reversed-fade.svg, so there is no
   CSS mask here. It measured identically to the CSS mask for edge
   sharpness, and it avoids the extra compositing layer along with the
   cross browser differences that come with masking a replaced element. */
.wordmark {
  display: block;
  width: 100%;
  height: auto;
}

/* Roughly one sixth of the wordmark height, which is the heading width
   divided by the wordmark aspect ratio of 6.623. */
.tagline {
  color: var(--text-2);
  font-family: "Inter", system-ui, "Segoe UI", Arial, sans-serif;
  font-weight: 400;
  font-size: min(2.214vw, 25.2px);
  letter-spacing: 0.02em;
  line-height: 1.4;
  margin: 0;
}

#hero-strata {
  display: block;
  width: 100%;
  max-width: 860px;
  height: auto;
  margin: 0 auto 6px;
  /* The only item that gives up height when the viewport is tight, down to
     a floor of 300px of rendered graphic. */
  flex-shrink: 1;
  min-height: 300px;
}

.label {
  font-family: "JetBrains Mono", ui-monospace, Consolas, monospace;
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.2em;
  color: var(--lime);
  margin: 16px 0 0;
}

/* Early access disclosure: a quiet text button that swaps itself for the
   email field once opened. */

.reveal {
  margin-top: 14px;
  width: 100%;
}

.reveal-btn {
  font: inherit;
  background: none;
  border: 0;
  border-bottom: 1px solid var(--hairline-2);
  border-radius: 0;
  padding: 2px 1px;
  color: var(--text-2);
  cursor: pointer;
}

.reveal-btn:hover {
  color: var(--text);
  border-bottom-color: var(--lime);
}

.reveal-btn:focus-visible {
  border-bottom-color: var(--lime);
}

.panel-label {
  font-family: "JetBrains Mono", ui-monospace, Consolas, monospace;
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.2em;
  color: var(--text-3);
  margin: 0 0 12px;
}

.capture-form {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  justify-content: center;
}

.capture-input {
  font: inherit;
  background: transparent;
  border: 1px solid var(--hairline-2);
  border-radius: 2px;
  padding: 9px 12px;
  width: 100%;
  max-width: 320px;
  color: var(--text);
}

.capture-input::placeholder {
  color: var(--text-3);
}

.capture-input:focus {
  border-color: var(--lime);
}

.capture-submit {
  font: inherit;
  background: none;
  border: 1px solid var(--hairline-2);
  border-radius: 2px;
  padding: 9px 18px;
  color: var(--text-2);
  cursor: pointer;
}

.capture-submit:hover {
  color: var(--text);
  border-color: var(--lime);
}

.capture-submit:disabled {
  color: var(--text-3);
  border-color: var(--hairline);
  cursor: default;
}

/* Live region for the send result. It collapses when empty so it does not
   leave a gap under the form. */
.capture-status {
  font-size: 0.8125rem;
  line-height: 1.5;
  color: var(--text-2);
  margin: 10px 0 0;
}

.capture-status:empty {
  display: none;
}

/* Failure state. The rule to the left and the heavier weight carry the
   meaning alongside the colour, so it does not depend on colour vision,
   and the message itself always says what went wrong. */
.capture-status.is-error {
  display: inline-block;
  color: var(--text);
  font-weight: 500;
  border-left: 2px solid var(--hairline-2);
  padding-left: 10px;
  text-align: left;
}

.capture-done {
  font-size: 0.9375rem;
  color: var(--text-2);
  margin: 0;
}

.capture-done:focus-visible {
  outline: 2px solid var(--lime);
  outline-offset: 4px;
}

.capture-privacy {
  font-size: 0.75rem;
  line-height: 1.5;
  color: var(--text-3);
  margin: 10px 0 0;
}

/* Off screen but still reachable by a screen reader. */
.visually-hidden {
  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;
}

footer {
  margin-top: auto;
  width: 100%;
  padding-top: 14px;
  border-top: 1px solid var(--hairline);
  color: var(--text-3);
  font-size: 0.9375rem;
}

:focus-visible {
  outline: 2px solid var(--lime);
  outline-offset: 4px;
}

/* Small screens: step type down and keep at least 24px side padding. */

@media (max-width: 640px) {
  .hero {
    padding-left: 24px;
    padding-right: 24px;
  }

  .wordmark-heading {
    width: 92vw;
    max-width: none;
  }

  /* Holding the strict one sixth here would drop the tagline to under 9px,
     so it gets a legibility floor on small screens. */
  .tagline {
    font-size: max(2.315vw, 11px);
  }

  /* At this width the graphic is already limited by the column, not by
     vertical space, so its natural height is well under the floor. Keeping
     the floor here would only pad the box with dead space. */
  #hero-strata {
    min-height: 0;
  }

  /* The mono annotations are illegibly small on a phone, so drop them. */
  #hero-strata .an-label {
    display: none;
  }
}

/* Hero build animation.

   Base state, outside any media query: the graphic is complete and static.
   The draw elements all carry pathLength="100", so a dash pattern of 100
   spans the whole path whatever its real length, and an offset of 100
   hides it completely. Holding the offset at 0 here means the default
   rendering, and the reduced motion rendering, is the finished graphic.

   Everything that moves lives in the single no-preference query below,
   so requesting reduced motion leaves nothing to run. Every animation
   fills backwards, so each element holds its hidden start state through
   its delay rather than flashing in before its turn. Nothing fills
   forwards: each animation resolves to the element's own painted state,
   including the opacity attributes carried by the threads and traces.

   The order reads bottom to top: ground plane, constituent nodes,
   threads inward, chamber, seat columns, active seats, threads upward,
   market traces, reaction marks. The last finite animation, trace lane
   5, finishes at 4400ms. After that only the six live markers and the
   55 twinkling constituent dots keep going. */

#hero-strata .an-col,
#hero-strata .an-down,
#hero-strata .an-up,
#hero-strata .an-trace {
  stroke-dashoffset: 0;
}

/* The twinkling dots are plain opaque dots by default, so the static
   rendering and the reduced motion rendering both show them in full. */
#hero-strata .an-twinkle {
  opacity: 1;
}

@media (prefers-reduced-motion: no-preference) {
  @keyframes strata-fade {
    from { opacity: 0; }
  }

  @keyframes strata-draw {
    from { stroke-dashoffset: 100; }
  }

  @keyframes strata-pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.72; }
  }

  @keyframes strata-twinkle {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.28; }
  }

  .reveal-btn {
    transition: color 180ms ease-out, border-bottom-color 180ms ease-out;
  }

  .capture-input,
  .capture-submit {
    transition: color 180ms ease-out, border-color 180ms ease-out;
  }

  .an-col,
  .an-down,
  .an-up,
  .an-trace {
    stroke-dasharray: 100;
  }

  .an-plane {
    animation: strata-fade 700ms ease-out 0ms backwards;
  }

  /* The twinkling dots carry .an-node too, so they are excluded here and
     given their own rule below. Letting both rules apply would declare
     the animation shorthand twice for the same opacity property. */
  .an-node:not(.an-twinkle) {
    animation: strata-fade 600ms ease-out 250ms backwards;
  }

  .an-down {
    animation: strata-draw 700ms ease-out 700ms backwards;
  }

  .an-chamber {
    animation: strata-fade 600ms ease-out 1150ms backwards;
  }

  .an-col {
    animation: strata-draw 450ms ease-out 1550ms backwards;
  }

  .an-coldot {
    animation: strata-fade 350ms ease-out 1950ms backwards;
  }

  .an-up {
    animation: strata-draw 550ms ease-out 2250ms backwards;
  }

  /* Each trace draws and fades together, staggered lane by lane. */
  .an-trace {
    animation-name: strata-draw, strata-fade;
    animation-duration: 1300ms, 300ms;
    animation-delay:
      calc(2650ms + (var(--lane, 0) * 90ms)),
      calc(2650ms + (var(--lane, 0) * 90ms));
    animation-timing-function: ease-out, ease-out;
    animation-iteration-count: 1, 1;
    animation-fill-mode: backwards, backwards;
  }

  .an-label {
    animation: strata-fade 600ms ease-out 3400ms backwards;
  }

  .an-mark {
    animation: strata-fade 400ms ease-out 3900ms backwards;
  }

  /* The live markers also carry .an-mark, so this rule has to follow it
     to win on source order. The pulse must not fill backwards: a later
     animation in the list beats an earlier one on the same property, so
     a backwards fill here would hold opacity 1 through the fade and show
     the marker from the first frame. */
  .an-live {
    animation-name: strata-fade, strata-pulse;
    animation-duration: 400ms, 4000ms;
    animation-delay: 3900ms, 4600ms;
    animation-timing-function: ease-out, ease-in-out;
    animation-iteration-count: 1, infinite;
    animation-fill-mode: backwards, none;
  }

  /* The constituent dots fade in with the rest of the lower plane, then
     drift on their own period. Each dot carries its own --tw duration and
     --td offset, so the field never falls into step. Same trap as the live
     markers above: the twinkle must not fill backwards, or it would beat
     the fade on opacity and show every dot from the first frame. */
  .an-twinkle {
    animation-name: strata-fade, strata-twinkle;
    animation-duration: 600ms, var(--tw);
    animation-delay: 250ms, calc(850ms + var(--td));
    animation-timing-function: ease-out, ease-in-out;
    animation-iteration-count: 1, infinite;
    animation-fill-mode: backwards, none;
  }
}
