/* =============================================================
   osc.barakahgames.com — page composition

   Composes from tokens.css only. No hex values live here, and no
   token is redefined here: tokens.css is duplicated verbatim into the
   Shopify repo, so editing it in one place silently diverges the two.
   Anything this site needs and tokens.css lacks gets composed, not
   added.

   The @font-face blocks are the one exception that has to live here.
   The family NAMES are shared brand tokens; the font FILES cannot be,
   because Shopify serves its assets through asset_url. Only the names
   travel between repos.
   ============================================================= */

/* ---------- Faces ----------
   Subset from the exact TTFs the app ships (@expo-google-fonts), so
   the two products render identical letterforms. Basic Latin plus
   smart quotes and dashes — subset by unicode range rather than by
   the strings currently on the page, so a copy edit never means
   regenerating a font. Both faces are SIL OFL; the licences ship next
   to the files, in /fonts. */

@font-face {
  font-family: "Playfair Display";
  src: url("/fonts/playfair-display-400.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

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

/* Weight 400 exists only for the letterspaced uppercase controls —
   the app's AuthAction label is bodyRegular, not bodyLight. Subset to
   uppercase, digits, space and a full stop, which is the entire
   character set that treatment will ever use. */
@font-face {
  font-family: "Montserrat";
  src: url("/fonts/montserrat-400-upper.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

/* ---------- Ground ---------- */

html {
  min-height: 100%;
}

body {
  display: flex;
  min-height: 100vh;
  min-height: 100dvh;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: var(--bg-space-xl) var(--bg-space-md);
  box-sizing: border-box;
  text-align: center;
  overflow-wrap: break-word;
}

main {
  width: 100%;
  max-width: var(--bg-measure);
}

/* tokens.css caps p at the measure but leaves it left-aligned in flow;
   centred layout needs the auto margins. */
p {
  margin-inline: auto;
}

a {
  color: inherit;
}

/* ---------- Labels ----------
   tokens.css .label carries the face, size, tracking and gold. The one
   thing it cannot know is that this layout is centred, and centring is
   where 0.4em of tracking causes a problem: it leaves a 0.4em space
   after the final letter, and centring counts that space as part of the
   text, so the glyphs land half a space left of true centre — visibly
   off-axis from the headline beneath them.

   Padding the left by the full tracking moves the content box's centre
   right by half of it, which is exactly the correction. Padding rather
   than a negative margin, so it survives the margin shorthands
   below. */
.label {
  padding-left: var(--bg-tracking-label);
}

/* ---------- The eyebrow ----------
   The slow breath is the app's Eyebrow component: every screen carries
   it, at the same timing, so the whole product feels like it is in
   unison. It is a fade, not a movement, so it sits inside the motion
   rules. The app's trough is 0.4; this holds at the label step of the
   ladder instead, because 0.4 is under the 58% contrast floor and the
   floor is deliberate. See docs/app-side-handoff.md. */

.eyebrow {
  display: block;
  margin: 0 0 var(--bg-space-md);
  animation: bg-breathe 4000ms var(--bg-motion-ease) infinite;
}

@keyframes bg-breathe {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: calc(var(--bg-alpha-label) / var(--bg-alpha-primary));
  }
}

@media (prefers-reduced-motion: reduce) {
  .eyebrow {
    animation: none;
  }
}

/* ---------- The terracotta line ----------
   The same curve as components/TerracottaLine.tsx, not a redraw: the
   app's Catmull-Rom control points converted to cubic Béziers, which
   is exact for tension 0.5. preserveAspectRatio="none" reproduces the
   app's independent width/height scaling, and non-scaling-stroke keeps
   the weight at the app's 1.6 regardless of the box. */

.line {
  display: block;
  width: 100%;
  height: clamp(2.25rem, 11vw, 3.5rem);
  margin: 0 auto var(--bg-space-lg);
  overflow: visible;
}

.line path {
  fill: none;
  stroke: var(--p-accent);
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
  /* pathLength="1" on the element normalises the dash maths, so the
     draw-on needs no measured length. */
  stroke-dasharray: 1;
  stroke-dashoffset: 1;
  animation: line-draw calc(var(--bg-motion-duration) * 2.5)
    var(--bg-motion-ease) both;
}

@keyframes line-draw {
  to {
    stroke-dashoffset: 0;
  }
}

/* ---------- Copy ---------- */

h1 {
  margin-bottom: var(--bg-space-md);
}

.lede {
  color: var(--p-text-body);
  font-size: var(--bg-size-body-lg);
  max-width: 28rem;
}

/* ---------- The code ----------
   Four hairline-underlined slots, which is the form of CodeSlots in the
   app rather than the form of the smaller reminder on /pair. The
   partner types into that exact component minutes later, so the shape
   should already be familiar. Sized to be memorised, not to be read. */

.code {
  margin: var(--bg-space-xl) auto var(--bg-space-lg);
}

.code__label {
  display: block;
  /* Not gold. Gold marks the label above a headline, and it is
     rationed; this is a field label. The app uses cream at 0.5, which
     is below the floor — this holds at the floor. */
  color: var(--p-text-hint);
  margin-bottom: var(--bg-space-md);
}

.code__slots {
  display: flex;
  justify-content: center;
  gap: clamp(0.625rem, 3vw, 1rem);
}

.code__slot {
  /* Sized off the widest glyph the code alphabet can produce. Playfair's
     W runs to about 0.9em, so the slot has to be comfortably wider than
     the type or a code like WWWW fills its rules edge to edge and the
     four characters stop reading as four. */
  width: clamp(3.25rem, 20vw, 5rem);
  /* Clears Playfair's Q tail, which otherwise lands on the hairline. */
  padding-bottom: var(--bg-space-sm);
  border-bottom: var(--bg-hairline-width) solid var(--p-hairline);
  font-family: var(--bg-font-display);
  font-size: clamp(2.25rem, 13.5vw, 3.75rem);
  line-height: 1.1;
  color: var(--p-text);
  /* Fixed-width slot, glyph centred in it — proportional figures stay
     optically aligned without needing a tabular face. */
  text-align: center;
}

.code__copy {
  margin-top: var(--bg-space-md);
}

.code__note {
  color: var(--p-text-hint);
  font-size: var(--bg-size-small);
  margin-top: var(--bg-space-md);
}

/* ---------- Actions ----------
   AuthAction, ported: a 10px label at 0.3em beside a 44px hairline
   circle, gap 14. No fill — --bg-fill-permitted is 0, and the one
   filled-action question in the website plan is a commerce question.
   There is no commerce here, and this visitor's spouse just asked them
   to do something for a year. Motivation is not the constraint. */

.actions {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--bg-space-lg);
}

.action {
  display: inline-flex;
  align-items: center;
  gap: 14px;
  text-decoration: none;
  color: var(--p-text);
  transition: opacity var(--bg-motion-duration-fast) var(--bg-motion-ease);
}

.action:hover,
.action:active {
  opacity: 0.75;
}

.action__label {
  font-family: var(--bg-font-body);
  font-weight: var(--bg-weight-body);
  font-size: 0.625rem;
  letter-spacing: 0.3em;
  /* The tracking adds a trailing space after the last letter; pulling
     it back keeps the optical gap to the circle at 14. */
  margin-right: -0.3em;
  text-transform: uppercase;
  white-space: nowrap;
}

.action__circle {
  flex: none;
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  border: var(--bg-hairline-width) solid
    rgb(from var(--p-ink) r g b / 0.35);
  border-radius: var(--bg-radius-full);
}

.action__circle svg {
  display: block;
  width: 18px;
  height: 18px;
  fill: none;
  stroke: var(--p-ink);
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* The hint under the action. CONTINUE alone does not say where the tap
   goes; this does, at the floor so it stays subordinate. */
.action__hint {
  display: block;
  margin: var(--bg-space-xs) 0 0;
  color: var(--p-text-hint);
  font-size: var(--bg-size-small);
}

/* Secondary — a text action, no circle, no fill. */
.secondary {
  font-family: var(--bg-font-body);
  font-weight: var(--bg-weight-secondary);
  font-size: var(--bg-size-small);
  color: var(--p-text-body);
  background: none;
  border: 0;
  border-bottom: var(--bg-hairline-width) solid var(--p-hairline);
  border-radius: var(--bg-radius-none);
  padding: var(--bg-space-2xs) 0;
  /* 44px of vertical target around a 14px line, without the hairline
     moving. */
  margin: 0;
  cursor: pointer;
  font-feature-settings: inherit;
  transition: border-color var(--bg-motion-duration-fast)
    var(--bg-motion-ease);
}

.secondary:hover {
  border-bottom-color: var(--p-accent);
}

.secondary-wrap {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
}

/* Recessive — the in-app-browser rescue. Deliberately not a second
   primary action: no circle, no arrow, floor opacity. */
.recessive {
  margin: var(--bg-space-lg) auto 0;
  font-size: var(--bg-size-small);
  color: var(--p-text-hint);
}

.recessive a {
  text-decoration: none;
  border-bottom: var(--bg-hairline-width) solid var(--p-hairline);
  padding-bottom: 1px;
  transition: border-color var(--bg-motion-duration-fast)
    var(--bg-motion-ease);
}

.recessive a:hover {
  border-bottom-color: var(--p-accent);
}

/* Available to a screen reader, absent for everyone else. Carries the
   code as spaced characters, since four letters in four boxes read as
   noise. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ---------- Entry motion ----------
   Fade and rise, staggered. tokens.css owns the keyframes, the
   distance and the easing, and zeroes all three under
   prefers-reduced-motion. */

.enter-1 { animation-delay: 0ms; }
.enter-2 { animation-delay: 90ms; }
.enter-3 { animation-delay: 180ms; }
.enter-4 { animation-delay: 270ms; }
.enter-5 { animation-delay: 360ms; }

/* ---------- No JavaScript ----------
   The code lives in the URL path and a static file cannot see it, so
   the slots and the platform routing both need script. invite.js sets
   data-js and data-platform on <html> from the document head, before
   the body is parsed, so neither of these states flashes.

   Defaults here are the no-script states — if the script 404s or is
   blocked, the page is still coherent: both stores are offered and the
   address bar is where the code is. */

.code__slots,
.code__copy {
  display: none;
}

[data-js] .code__slots {
  display: flex;
}

[data-js] .code__copy {
  display: flex;
}

[data-js] .code__note--noscript {
  display: none;
}

/* Platform routing. With no data-platform — script absent, or a
   platform we could not identify — both stores show, which is also
   the correct desktop state. */
[data-platform="ios"] .action--android,
[data-platform="android"] .action--ios {
  display: none;
}

/* The error state is chosen by script; without script the invitation is
   the safe assumption, since most links are valid. */
.state--invalid {
  display: none;
}

[data-state="invalid"] .state--valid {
  display: none;
}

[data-state="invalid"] .state--invalid {
  display: block;
}
