/* preambient.com — one screen.
   See docs/decisions/2026-09-06-one-screen-landing.md for why this file is 300 lines
   and not 1200: the previous stylesheet's components no longer exist, and every
   visitor downloaded them forever. */

/* ---------------------------------------------------------------- fonts

   These two files are NOT fingerprinted. They are referenced from inside this
   stylesheet, which templ never renders, so there is no Go-side hook the way
   there is for style.css and hero.js. The rule instead is RENAME ON CHANGE:
   if you resubset either face, ship it under a new filename and edit the
   @font-face in the same commit. Fonts change roughly never.

   newsreader-wordmark.woff2 is subset to the letters in "preambientlabs" and
   nothing else. Change the wordmark string and you must rebuild it, or the new
   letters fall back to a system serif. Both faces are SIL OFL 1.1; the licences
   ship beside them as OFL-Archivo.txt and OFL-Newsreader.txt. */

@font-face {
  font-family: "Newsreader";
  src: url("/assets/fonts/newsreader-wordmark.woff2") format("woff2");
  font-weight: 300;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Archivo";
  src: url("/assets/fonts/archivo-ui.woff2") format("woff2");
  font-weight: 300 600;
  font-style: normal;
  font-display: swap;
}

/* ---------------------------------------------------------------- tokens */

:root {
  /* The field, measured off the reference. */
  --stage-top: #dcdfe4;
  --stage-mid: #e6e8ec;
  --stage-bot: #edeef1;

  --ink: #171815;
  /* The only accent, and it earns its keep in exactly one place: the focus ring.
     It is NOT in the shader -- hero.js is a port of the reference's raymarch and
     its colour comes from a cosine palette keyed on distance travelled, with no
     relationship to this hex. An earlier comment here claimed the opposite and
     would have got the token deleted as shader-only. */
  --violet: #482873;

  --headline: rgba(0, 0, 0, 0.875);
  /* Functional text -- the status line, the placeholder, the footer. Not a taste
     value. It was 0.37, which measures 2.28:1 on the one element that carries
     form errors.
     0.62 is what the STATIC gradient needs. It is not enough: #note sits inside
     the masked band, and the shader's smoke drives that backdrop to (192,180,173)
     on its darkest frames, where 0.62 falls to 3.74:1. Measured over ten frames,
     0.70 is the floor; 0.74 carries margin for frames not sampled.
     If you lower this, re-measure against the RENDER across frames, not against
     the gradient -- the static calculation says PASS and is wrong. */
  --muted: rgba(23, 24, 26, 0.74);
  /* Removed: --paper (#fbfaf7) and --rule. Both were declared and used zero
     times -- this page is silver, not paper, and nothing draws a rule. A token
     nobody consumes is a claim about the design that the design does not make. */
  /* Display-only, for the 26px line on the social card. Never for body or UI
     text -- at this alpha it does not clear 4.5:1 anywhere. */
  --faint: rgba(23, 24, 26, 0.37);

  --display: "Newsreader", Georgia, "Times New Roman", serif;
  --ui: "Archivo", system-ui, "Helvetica Neue", Helvetica, Arial, sans-serif;
  --mono: Consolas, Monaco, "Lucida Console", "Courier New", monospace;

  --gutter: clamp(20px, 4vw, 56px);
  /* The band the absolutely-positioned footer occupies: its own height plus its
     offset plus breathing room. .stage reserves it as padding so the centred
     core is centred in the space actually available to it and can never reach
     into the footer -- at any viewport, without a breakpoint guessing where
     "short" begins. */
  /* The vertical rhythm between the three stacked pieces. It existed twice as
     clamp(28px, 6vh, 64px) and clamp(28px, 6vh, 60px) -- same min, same rate,
     4px apart by accident. */
  --stack: clamp(28px, 6vh, 62px);
}

/* ---------------------------------------------------------------- reset */

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

html,
body {
  margin: 0;
  padding: 0;
  height: 100%;
}

html {
  /* The gradient lives on body, and body is sized to the SMALL mobile viewport
     (100svh). When a phone hides its address bar the visible area grows past
     that box, and pulling past the bottom shows the overscroll area. Both are
     painted with the root's background, which was unset -- so iPhone Safari
     showed a white strip at the bottom, and Chromium showed the gradient
     restarting with a seam. Give the root the colour the gradient ends on and
     every uncovered pixel matches. Setting any background here also stops the
     body gradient propagating to the canvas, which is what caused the seam. */
  background-color: var(--stage-bot);
}

body {
  /* The page does not scroll at any height it was designed for. It is NOT
     overflow:hidden -- below about 420px of viewport height the form, the
     status line and the footer stop fitting, and clipping them silently is
     worse than a scrollbar nobody sees. The short-viewport rule at the bottom
     of this file releases the layout; this just refuses to trap anyone. */
  overflow-x: hidden;
  /* The footer is absolutely positioned. Without a positioned ancestor it
     resolves against the initial containing block, which is 100lvh -- so with a
     mobile toolbar expanded it lands below the fold while .stage is sized in
     svh. Anchoring it to body keeps both on the same measure. */
  position: relative;
  min-height: 100svh;
  background: linear-gradient(var(--stage-top) 0%, var(--stage-mid) 42%, var(--stage-bot) 78%);
  color: var(--ink);
  font-family: var(--ui);
  font-size: 15px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

.sr-only {
  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;
}

/* ---------------------------------------------------------------- stage */

.stage {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 100vh;
  min-height: 100svh;
  padding: 0 var(--gutter);
  /* clip, NOT hidden, and not overflow-x either. Setting one axis to anything
     other than `visible` computes the OTHER axis to `auto`, which makes this a
     scroll CONTAINER -- that once turned the stage into a 1058px scroller
     inside a 900px box, and one wheel gesture moved the content 158px while the
     pinned footer stayed put, on the page whose whole premise is that it does
     not scroll. `overflow: clip` clips and refuses to scroll. Nothing overflows
     here today; this is the guard that keeps it that way. The short-viewport
     rule below releases it so nothing is trapped when the layout genuinely does
     not fit. */
  overflow: clip;
}

/* ---------------------------------------------------------------- the ribbon

   This gradient is the DEFAULT state, not a fallback that something has to
   switch on. hero.js adds .is-live only after the WebGL2 program links, and
   .is-live clears the gradient. A blocked, 404'd or CSP-refused script never
   runs, so it can never add a class -- which is why the inverse arrangement
   (a .no-webgl class added on failure) ships an empty band and still passes
   every test. See the ADR. */

.prism {
  position: absolute;
  left: 0;
  top: 21%;
  width: 100%;
  height: 58%;
  display: block;
  pointer-events: none;
  -webkit-mask-image: linear-gradient(to bottom, transparent, #000 22%, #000 78%, transparent);
  mask-image: linear-gradient(to bottom, transparent, #000 22%, #000 78%, transparent);
  /* Tuned to land near the shader's average frame, not to outshine it:
     swapping between the two states should not read as two different designs.
     The live version is a moving volumetric sheet, so this is necessarily a
     still of one mood rather than a copy. */
  background:
    radial-gradient(130% 62% at 50% 58%, rgba(150, 158, 174, 0.3), transparent 74%),
    linear-gradient(
      104deg,
      rgba(138, 156, 200, 0) 0%,
      rgba(138, 156, 200, 0.3) 15%,
      rgba(120, 190, 214, 0.28) 27%,
      rgba(244, 246, 250, 0.72) 38%,
      rgba(228, 198, 152, 0.34) 49%,
      rgba(146, 158, 194, 0.3) 63%,
      rgba(156, 162, 176, 0.14) 82%,
      rgba(156, 162, 176, 0) 100%
    );
  filter: blur(22px);
}

.prism.is-live {
  background: none;
  filter: none;
}

/* ---------------------------------------------------------------- core */

.core {
  position: relative;
  z-index: 2;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  width: 100%;
  /* Balance: the mark + its gap above the wordmark is offset by equal space
     below, so the wordmark itself sits on the centre line. */
  padding-bottom: calc(60px + var(--stack));
}

.mark {
  display: block;
  width: 60px;
  height: 60px;
  margin-bottom: var(--stack);
  /* Quiet Agency: one entrance, then stillness. The mark arrives at the
     Phase-13 attention turn and settles to resting. */
  animation: mark-settle 420ms cubic-bezier(0.22, 1, 0.36, 1) both;
}

@keyframes mark-settle {
  from {
    transform: rotate(90deg);
    opacity: 0;
  }
  to {
    transform: rotate(0deg);
    opacity: 1;
  }
}

.wordmark {
  margin: 0;
  font-family: var(--display);
  font-weight: 300;
  font-size: clamp(42px, 10.4vw, 148px);
  /* Newsreader has real descenders; 0.98 clipped the p of "preambient". */
  line-height: 1.08;
  letter-spacing: -0.022em;
  color: var(--headline);
  white-space: nowrap;
}

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

/* ---------------------------------------------------------------- footer */

.site-footer {
  position: absolute;
  z-index: 2;
  left: var(--gutter);
  right: var(--gutter);
  bottom: calc(28px + env(safe-area-inset-bottom, 0px));
  text-align: center;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.06em;
  color: var(--muted);
}

/* ---------------------------------------------------------------- narrow */

@media (max-width: 900px) {
  .wordmark {
    white-space: normal;
    /* Was clamp(40px, 15vw, 96px), which STEPPED UP from 93.7px to 96px at the
       breakpoint while simultaneously starting to wrap -- the wordmark got
       bigger as the screen got smaller. Same min as the base rule, and a cap
       that meets it continuously. */
    font-size: clamp(42px, 12.2vw, 94px);
    line-height: 1.04;
  }
}

@media (max-width: 520px) {
  .mark {
    width: 46px;
    height: 46px;
  }

  .site-footer {
    bottom: calc(20px + env(safe-area-inset-bottom, 0px));
  }
}

/* ---------------------------------------------------------------- short

   Browsers at 200%+ zoom and the very shortest landscape phones land here.
   The composition is vertically centred against 100svh and the footer is
   pinned to the bottom, so below a certain height the wordmark lands on the
   footer. Let the page scroll and unpin the footer instead of clipping it.

   360, not 520: 520 was sized for the signup form. Without it the centred
   block is about 280px tall (mark 60 + gap 28 + wordmark ~100 + the balancing
   space under it), and the footer needs ~45px. That fits centred down to
   ~360px, so a landscape iPhone (430px high) keeps the centred composition
   instead of a huddle at the top of an empty screen. */

@media (max-height: 360px) {
  /* .stage centres a single row by default. Unpinning the footer makes it a
     second flex item, so the stage has to become a column or the footer lands
     beside the wordmark. */
  .stage {
    min-height: auto;
    /* Release the clip: down here the content is meant to exceed the viewport
       and body should scroll rather than swallow it. */
    overflow: visible;
    flex-direction: column;
    justify-content: flex-start;
    padding-top: 32px;
    padding-bottom: 20px;
  }

  .site-footer {
    position: static;
    left: auto;
    right: auto;
    bottom: auto;
    margin-top: 24px;
  }

  .mark {
    width: 40px;
    height: 40px;
    margin-bottom: 16px;
  }

  .wordmark {
    font-size: clamp(32px, 7vh, 64px);
  }

  /* The footer is in flow here, so the balancing space under the wordmark
     would only push it further down. */
  .core {
    padding-bottom: 0;
  }
}

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

@media (prefers-reduced-motion: reduce) {
  /* One policy: stop the motion, keep the composition. Reduced motion is not
     "hide the decorative layer" -- it should hand a vestibular-sensitive
     visitor a calmer page, not a different one. hero.js draws one still frame.

     animation-* belongs here alongside transition-*. Without it every future
     keyframe on this page is missed by default, which is why .mark needed a
     hand-written override below. */
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }

  .mark {
    opacity: 1;
    transform: none;
  }
}
