/* =========================================================================
   111 IN CHRIST — landing page
   Built from Warre's design export `subscribe.png` (1366x768).
   Every number marked "measured" was read off that PNG with Pillow, not eyeballed:

     background      #111111        (99.4% of the mock's pixels)
     pill stroke     rgb(166,163,160)  brightest stroke pixel
     logo            y  76-160  (h 85)   x 648-701  (w 54)
     "Subscribe"     y 312-334  (h 23)
     input pill      y 360-407  (h 48)   x 401-964  (w 564)
     "111INCHRIST.com" y 702-719 (h 18)
     pill v-centre   383.5 of 768 = 49.93%  -> the pill sits on the exact half.

   MOTION RULE (2026-09-06): the RESTING state is Warre's design and stays
   pixel-identical to it. Everything below moves on arrival, hover, focus or
   feedback — never in the state the mock describes. That is also why the
   geometry check renders with prefers-reduced-motion forced: it freezes the
   page in exactly the composition that was signed off.

   Typefaces are MATCHED, not confirmed — the deliverable was a PNG, not the Figma,
   so the real font names were not readable. See README "Open items".
   ========================================================================= */

/* ---- Self-hosted fonts. No runtime call to Google; the list is the brand's own. --- */
@font-face{
  font-family:'Unifraktur Maguntia';
  src:url('/assets/fonts/unifrakturmaguntia-400.woff2') format('woff2');
  font-weight:400; font-style:normal; font-display:swap;
  unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+2000-206F,U+2074,U+20AC,U+2122,U+2212;
}
@font-face{
  font-family:'Tinos';
  src:url('/assets/fonts/tinos-400.woff2') format('woff2');
  font-weight:400; font-style:normal; font-display:swap;
}
@font-face{
  font-family:'Tinos';
  src:url('/assets/fonts/tinos-700.woff2') format('woff2');
  font-weight:700; font-style:normal; font-display:swap;
}

:root{
  --bg:#111111;              /* measured */
  --ink:#ffffff;
  --line:#a6a3a0;            /* measured — this IS the resting stroke in the mock */
  --line-hi:#ffffff;         /* focus only; the design has no focus state of its own */
  --muted:#8d8a87;
  --err:#e0857c;

  /* Vertical rhythm, all taken from the mock's 768px-tall canvas. */
  --mast-top:9.9svh;         /* logo top 76/768  */
  --foot-bottom:6.4svh;      /* footer baseline gap 49/768 */
  --core-lift:7.05svh;       /* tuned against the render until the pill landed on y360 */

  --mark-h:clamp(64px,11.07svh,96px);   /* logo height 85/768 */
  --pill-h:clamp(46px,6.25svh,52px);    /* pill height 48/768 */
  --pill-w:clamp(260px,88vw,564px);     /* pill width 564 at 1366 wide */
  --btn:40px;                           /* submit circle; sits inside the pill */

  --fs-label:clamp(1.5rem,2.2vw,1.875rem);
  --fs-foot:clamp(1rem,1.64vw,1.4rem);

  --blackletter:'Unifraktur Maguntia','Old English Text MT','Cloister Black',serif;
  --serif:'Tinos','Times New Roman',Times,serif;

  /* One easing for the whole page. A single curve is what makes separate
     movements read as one hand rather than four. */
  --ease:cubic-bezier(.22,.7,.25,1);
}

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

html{ -webkit-text-size-adjust:100%; }

body{
  margin:0;
  background:var(--bg);
  color:var(--ink);
  font-family:var(--serif);
  -webkit-font-smoothing:antialiased;
  text-rendering:optimizeLegibility;
  -webkit-tap-highlight-color:transparent;   /* no grey flash on iOS taps */
}

::selection{ background:rgba(166,163,160,.32); color:#fff }

.page{
  min-height:100svh;
  display:flex;
  flex-direction:column;
  align-items:center;
  padding-inline:24px;
  /* keeps the composition clear of iOS home indicator / notch */
  padding-bottom:env(safe-area-inset-bottom);
}

/* =========================================================================
   ARRIVAL
   The page assembles itself in the reading order: mark, word, field, small
   print, wordmark. Nothing moves after ~1.5s.
   Base opacity stays 1 everywhere, so killing the animation (reduced motion)
   leaves the finished composition rather than an invisible page.
   ========================================================================= */
@keyframes rise{
  from{ opacity:0; transform:translateY(16px) }
  to  { opacity:1; transform:translateY(0) }
}
@keyframes markIn{
  from{ opacity:0; transform:translateY(10px) scale(.94) }
  to  { opacity:1; transform:translateY(0) scale(1) }
}
/* The footer is the bottom-most element, so it must NOT arrive from below:
   a transform reaching past the bottom edge enlarges the scrollable area and
   flashes a scrollbar on a phone (measured: +16px for ~1.2s at 390x844).
   It settles downward into place instead — same arrival, no overflow. */
@keyframes settleDown{
  from{ opacity:0; transform:translateY(-12px) }
  to  { opacity:1; transform:translateY(0) }
}

.mast  { animation:markIn 1s var(--ease) .05s both }
.label { animation:rise .85s var(--ease) .34s both }
.fieldwrap{ animation:rise .85s var(--ease) .46s both }
.consent{ animation:rise .8s var(--ease) .64s both }
.foot  { animation:settleDown .8s var(--ease) .78s both }

/* ---------- logo ---------- */
.mast{ padding-top:calc(var(--mast-top) + env(safe-area-inset-top)); }

.markwrap{
  position:relative;
  display:inline-block;
  height:var(--mark-h);
  line-height:0;
}
.mark{
  display:block;
  height:100%;
  width:auto;
  transition:transform .5s var(--ease);
  /* the export carries its own bevel highlight; no filter, keep it as drawn */
}

/* The mark is chrome. Light should travel across it — masked to the glyph so
   the sweep lives ON the metal, not in a rectangle around it. */
.markwrap::after{
  content:'';
  position:absolute; inset:0;
  pointer-events:none;
  -webkit-mask-image:url('/assets/mark-111.svg'); mask-image:url('/assets/mark-111.svg');
  -webkit-mask-size:contain;   mask-size:contain;
  -webkit-mask-repeat:no-repeat; mask-repeat:no-repeat;
  -webkit-mask-position:center; mask-position:center;
  background:linear-gradient(112deg,
      transparent 38%,
      rgba(255,255,255,.10) 46%,
      rgba(255,255,255,.72) 50%,
      rgba(255,255,255,.10) 54%,
      transparent 62%);
  background-size:260% 100%;
  background-repeat:no-repeat;
  background-position:-95% 0;   /* parked off the glyph */
  animation:sheen 9s ease-in-out 1.15s infinite;
}
@keyframes sheen{
  0%   { background-position:195% 0 }
  17%  { background-position:-95% 0 }
  100% { background-position:-95% 0 }   /* long rest, then it catches the light again */
}

@media (hover:hover){
  .markwrap:hover .mark{ transform:scale(1.045) }
  .markwrap:hover::after{ animation:sheen 1.5s ease-in-out }
}

/* ---------- the form block ---------- */
.core{
  flex:1;
  width:100%;
  display:flex;
  align-items:center;
  justify-content:center;
  padding-bottom:var(--core-lift);
}

.signup{
  display:flex;
  flex-direction:column;
  align-items:center;
  width:100%;
  gap:0;
}

.label{
  font-family:var(--blackletter);
  font-size:var(--fs-label);
  line-height:1;
  letter-spacing:.01em;
  margin:0 0 19px;                 /* 26px of GLYPH gap (334->360); the line box adds ~7 */
  -webkit-user-select:none; user-select:none;
  transition:text-shadow .5s var(--ease);
  text-align:center;
}
/* When the field is live, the word above it warms very slightly. Almost
   subliminal — it just stops the label feeling like a dead caption. */
.signup:focus-within .label{ text-shadow:0 0 22px rgba(255,255,255,.28) }

/* The arrival animation lives on the WRAPPER, never on .field itself, because
   JS animates .field for the shake and the exit. If both used the same
   element, removing an error class would re-trigger the entrance. */
.fieldwrap{ width:var(--pill-w); max-width:100% }
.field{ position:relative; width:100%; }

.input{
  width:100%;
  height:var(--pill-h);
  padding:0 52px 0 24px;   /* right pad reserves the arrow's seat: no reflow when it appears */
  color:var(--ink);
  caret-color:var(--line);
  background:transparent;          /* measured: pill interior == page background */
  border:1px solid var(--line);
  border-radius:999px;             /* measured: fully rounded ends */
  font-family:var(--serif);
  font-size:1rem;
  line-height:1;
  outline:none;
  transition:border-color .28s var(--ease),
             box-shadow .28s var(--ease),
             background-color .28s var(--ease);
  /* iOS zooms the page when a focused input is <16px — 1rem keeps it at 16 */
}
.input::placeholder{ color:transparent }   /* the mock shows an empty field */

@media (hover:hover){
  .input:hover{
    border-color:#cfccc9;
    background-color:rgba(255,255,255,.022);
  }
}
/* The mock draws no focus state, so one is added rather than invented into the
   resting design: the stroke goes white and gains a soft outer ring. */
.input:focus-visible{
  border-color:var(--line-hi);
  box-shadow:0 0 0 3px rgba(255,255,255,.13), 0 0 26px rgba(255,255,255,.07);
  background-color:rgba(255,255,255,.03);
}
.signup.is-touched .input:invalid{ border-color:var(--err) }
.signup.is-touched .input:invalid:focus-visible{
  box-shadow:0 0 0 3px rgba(224,133,124,.18);
}

/* Kill the UA's autofill repaint — it would flood the pill with blue/white. */
.input:-webkit-autofill,
.input:-webkit-autofill:hover,
.input:-webkit-autofill:focus{
  -webkit-text-fill-color:var(--ink);
  -webkit-box-shadow:0 0 0 1000px var(--bg) inset;
  caret-color:var(--ink);
  transition:background-color 9999s ease-out;
}

/* ---------- submit ----------
   The mock shows no button, so at rest there is none. The moment there is
   something to send, an arrow takes the seat already reserved for it inside
   the pill — people should not have to guess that Enter is the only way. */
.vh{
  position:absolute;
  width:1px;height:1px;
  margin:-1px;padding:0;border:0;
  clip-path:inset(50%);
  overflow:hidden;white-space:nowrap;
}
/* Empty field, keyboard user tabs to it: it has to become a real visible control. */
.vh:focus-visible{
  position:static;
  width:auto;height:auto;
  margin:14px 0 0;padding:10px 22px;
  clip-path:none;
  background:transparent;
  color:var(--ink);
  border:1px solid var(--line);
  border-radius:999px;
  font-family:var(--serif);
  font-size:.95rem;
  cursor:pointer;
}

@keyframes popIn{
  from{ opacity:0; transform:translateY(-50%) scale(.6) }
  to  { opacity:1; transform:translateY(-50%) scale(1) }
}
.signup.has-content .submit{
  position:absolute;
  right:calc((var(--pill-h) - var(--btn)) / 2);
  top:50%;
  width:var(--btn); height:var(--btn);
  margin:0; padding:0;
  clip-path:none; overflow:visible;
  display:grid; place-items:center;
  border:1px solid var(--line);
  border-radius:50%;
  background:transparent;
  color:var(--ink);
  cursor:pointer;
  font-size:0;                       /* hides the word; screen readers still read it */
  opacity:1;
  transform:translateY(-50%);
  animation:popIn .3s cubic-bezier(.2,.9,.3,1.35) both;
  transition:background-color .22s var(--ease), border-color .22s var(--ease);
}
/* The icon is the brand mark laid on its side. The long stem of the centre "1"
   becomes the shaft and the crossbar becomes the head — the page's own arrow,
   rather than a typographic one borrowed from somewhere else.
   rotate(-90deg) maps the glyph's "down" onto screen "right". */
.submit-mark{
  display:none;
  width:15px; height:24px;   /* big enough that the three serif '1's survive */
  background:currentColor;
  -webkit-mask:url('/assets/mark-111.svg') center/contain no-repeat;
          mask:url('/assets/mark-111.svg') center/contain no-repeat;
  transform:rotate(-90deg);
  transition:transform .3s var(--ease), opacity .18s linear;
}
.signup.has-content .submit .submit-mark{ display:block }

@media (hover:hover){
  .signup.has-content .submit:hover{ background:var(--ink); border-color:var(--ink); color:var(--bg) }
  /* translateY in the rotated frame reads as "forward" on screen. */
  .signup.has-content .submit:hover .submit-mark{ transform:rotate(-90deg) translateY(3px) }
}
.signup.has-content .submit:active{ transform:translateY(-50%) scale(.92) }
.signup.has-content .submit:focus-visible{
  box-shadow:0 0 0 3px rgba(255,255,255,.28);
}

/* ---- sending: the button becomes the spinner --------------------------
   Placed AFTER the has-content rules on purpose — same specificity, so source
   order is what lets this win and replace popIn. The mark itself never spins;
   a rotating cross reads badly. The ring spins, the mark steps aside. */
@keyframes spin{
  from{ transform:translateY(-50%) rotate(0deg) }
  to  { transform:translateY(-50%) rotate(360deg) }
}
.signup.is-sending .submit{
  border-width:2px;
  border-color:rgba(255,255,255,.16);
  border-top-color:var(--ink);
  background:transparent;
  cursor:default;
  animation:spin .72s linear infinite;
}
.signup.is-sending .submit .submit-mark{ opacity:0 }

.hp{
  position:absolute;
  left:-9999px;
  width:1px;height:1px;
  overflow:hidden;
}

.turnstile:not(:empty){ margin-top:18px }

.consent{
  max-width:var(--pill-w);
  margin:16px 0 0;
  font-size:.75rem;
  line-height:1.5;
  color:var(--muted);
  text-align:center;
  text-wrap:pretty;
  transition:color .4s var(--ease);
}
.consent:empty{ display:none }
.signup:focus-within .consent{ color:#a6a3a0 }   /* legible right when it matters */

.msg{
  /* Reserved height: the message must not resize the block, or the pill would
     jump off centre the moment an error or the confirmation appears. */
  min-height:2.8em;
  margin:14px 0 0;
  font-size:.9rem;
  line-height:1.4;
  text-align:center;
  color:var(--muted);
  max-width:var(--pill-w);
  text-wrap:pretty;
}
.msg.is-error{ color:var(--err) }
.msg.is-ok{ color:var(--ink) }
/* app.js focuses this after submit so a keyboard user is not dumped back at the
   top of the document. It is not interactive (tabindex="-1", never in the Tab
   order), so the UA focus ring would only draw a box around a sentence — and
   the message is announced through aria-live either way. */
.msg:focus{ outline:none }
/* Each new message arrives rather than blinking into place. JS re-triggers this
   by toggling the class, so a repeated identical message still animates. */
.msg.is-fresh{ animation:rise .42s var(--ease) both }

/* ---------- sending ----------
   No spinner: the design has nowhere to put one. Instead light travels through
   the pill, which says "working" using the page's own vocabulary. */
.field::after{
  content:'';
  position:absolute; inset:0;
  border-radius:999px;
  pointer-events:none;
  opacity:0;
  background:linear-gradient(90deg,transparent,rgba(255,255,255,.16),transparent);
  background-size:45% 100%;
  background-repeat:no-repeat;
  background-position:-60% 0;
  transition:opacity .2s var(--ease);
}
.signup.is-sending{ pointer-events:none }
.signup.is-sending .input{ color:var(--muted) }
.signup.is-sending .field::after{
  opacity:1;
  animation:sweep 1.05s linear infinite;
}
@keyframes sweep{
  from{ background-position:-60% 0 }
  to  { background-position:160% 0 }
}

/* ---------- the result screen ----------
   No design reference existed for this state, so it does not invent a new
   screen: the form's own heading becomes the result, the field steps out, and
   what is left re-centres on the viewport. Same type, same rhythm, same page. */
.signup.is-done .fieldwrap,
.signup.is-done .consent,
.signup.is-done .turnstile{ display:none }
.signup.is-done .label{ margin-bottom:18px }
.signup.is-done .msg{ font-size:1.05rem; color:var(--ink) }

/* The lift exists to sit the PILL on the exact half of the viewport. Once the
   pill is gone there is nothing to hold there, so the result takes the centre. */
.page.is-done .core{ padding-bottom:0 }

.signup.is-failed .label{ color:var(--err) }
.signup.is-failed .msg{ color:var(--muted) }

.retry{
  margin:22px 0 0;
  padding:11px 26px;
  background:transparent;
  color:var(--ink);
  border:1px solid var(--line);
  border-radius:999px;
  font-family:var(--serif);
  font-size:.95rem;
  cursor:pointer;
  transition:background-color .22s var(--ease), color .22s var(--ease),
             border-color .22s var(--ease);
  animation:rise .5s var(--ease) .12s both;
}
.retry[hidden]{ display:none }
@media (hover:hover){
  .retry:hover{ background:var(--ink); color:var(--bg); border-color:var(--ink) }
}
.retry:focus-visible{ box-shadow:0 0 0 3px rgba(255,255,255,.25) }

/* ---------- footer wordmark ---------- */
.foot{ padding-bottom:var(--foot-bottom); }
.wordmark{
  font-family:var(--serif);
  font-size:var(--fs-foot);
  line-height:1;
  letter-spacing:.005em;
  white-space:nowrap;
  color:var(--ink);
  transition:letter-spacing .5s var(--ease), text-shadow .5s var(--ease);
}
@media (hover:hover){
  .wordmark:hover{ letter-spacing:.045em; text-shadow:0 0 20px rgba(255,255,255,.25) }
}
.tld{ font-size:.62em }   /* measured: ".com" sits noticeably smaller than the caps */

/* ---------- responsive ---------- */
/* Phones: the mock is a desktop render, so the lift is eased off — a tall narrow
   viewport reads better with the form nearer the optical centre. */
@media (max-width:640px){
  :root{
    --core-lift:8svh;
    --mast-top:8svh;
    --foot-bottom:5svh;
  }
}
/* Short/landscape phones: give the composition room instead of clipping it. */
@media (max-height:520px){
  :root{ --core-lift:4svh; --mast-top:5svh; --foot-bottom:4svh; --mark-h:clamp(48px,14svh,72px) }
  .label{ margin-bottom:16px }
}

/* Motion is decoration here, never information: every state above also reads
   through colour and text. So this can turn all of it off without loss.
   app.js checks the same query before running its JS-driven animations. */
@media (prefers-reduced-motion:reduce){
  *,*::before,*::after{
    transition:none !important;
    animation:none !important;
  }
}

/* Forced-colours (Windows high contrast): keep the field findable. */
@media (forced-colors:active){
  .input{ border-color:CanvasText }
  .markwrap::after{ display:none }
}
