@charset "utf-8";

/* ==========================================================================
   Clemaine pre-launch waitlist page
   --------------------------------------------------------------------------
   Design: "One Slot Open". The page is a single ruled column torn out of an
   appointment book. Two full-height hairlines are the page edges; a ribbon of
   the booked-dot pattern runs OUTSIDE each rule, so the pattern frames the
   text and can never sit behind it (brand book §5 forbids pattern behind body
   text). Here that is structurally impossible rather than merely arranged
   around.

   The dot is the brand, and it appears at three scales, each one meaning
   something:
     1. the wordmark's violet tittle              (small)
     2. the headline's full stop                  (large, the one that moves)
     3. the hollow slot beside the input          (the one YOU fill)

   Brand refs: docs/brand/README.md in the main Clemaine repo.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. Reset
   Box-sizing first and unconditionally: every ribbon offset below is derived
   from calc(50% ± var(--col)/2), and in the default content-box the padded
   column measures 768px rather than 672px, which throws both flanks 48px out
   and laps the ribbons over the rules.
   -------------------------------------------------------------------------- */
*,
*::before,
*::after {
  box-sizing: border-box;
}

html {
  /* Stops iOS bumping type on rotate, which would desync the lattice. */
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
}

body {
  margin: 0;
}

/* --------------------------------------------------------------------------
   2. Fonts. Self-hosted, no third-party request.

   Fraunces here is a STATIC instance (verified: no fvar table, usWeightClass
   600, cut at optical size 144). That is deliberate. A variable Fraunces would
   need "opsz" pinned via font-variation-settings or the browser optically
   sizes it and thickens exactly the hairlines the brand book protects. And if
   font-variation-settings is set without listing "wght", weight silently falls
   back to default. A static 144/600 cut cannot drift either way.

   Inter IS variable (wght 100 to 900), so one file serves 400/500/600.
   Licences: assets/fonts/LICENSE.md (both SIL OFL 1.1).
   -------------------------------------------------------------------------- */
@font-face {
  font-family: "Fraunces";
  src: url("assets/fonts/fraunces-144-600-latin.woff2") format("woff2");
  font-weight: 600;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
    U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "Inter";
  src: url("assets/fonts/inter-var-latin.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
    U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}

/* Metric-matched stand-in for the ~100ms before Fraunces lands. Georgia is on
   effectively every Windows/macOS/iOS box; size-adjust pulls its larger x-height
   back to Fraunces' so the headline's line breaks do not move on swap. Paired
   with min-height on the headline (§7) this keeps CLS at zero. */
@font-face {
  font-family: "Fraunces Fallback";
  src: local("Georgia"), local("Times New Roman");
  size-adjust: 107%;
  ascent-override: 92%;
  descent-override: 24%;
  line-gap-override: 0%;
}

/* --------------------------------------------------------------------------
   3. Tokens

   Every hex is a canonical Clemaine token, nothing invented. Contrast figures
   are measured against the ACTUAL adjacent ground (the brand book's 5.9:1 for
   violet-600 is measured on white; on Crème it is 5.37:1). All text pairs
   clear WCAG AA 4.5:1; the field border clears the 3:1 non-text threshold of
   WCAG 1.4.11.

   Light is defined on bare :root so a page with no media query and no
   data-theme still has a complete palette.
   -------------------------------------------------------------------------- */
:root {
  color-scheme: light dark;

  /* Ground & text */
  --ground: #faf8f4;        /* Crème  / neutral-50                          */
  --text: #1a1712;          /* Ink    / neutral-900, 16.85:1 on Crème       */
  --text-2: #6f685c;        /* Taupe  / neutral-500, 5.20:1 on Crème        */

  /* Structure */
  --rule: #d6cec1;          /* neutral-300. Sand (#e7e1d7) is 1.23:1 on
                               Crème and disappears on a cheap panel or a
                               phone in sunlight; the rules ARE the design,
                               so they get the next step down the ramp.     */
  --field-border: #6f685c;  /* Taupe. 5.20:1 light / 3.24:1 dark. The input
                               is the page's one job, so its boundary must
                               clear 3:1 in BOTH modes, unlike the purely
                               decorative page rules.                       */

  /* Accent */
  --accent: #7c3aed;        /* violet-600, 5.37:1 on Crème                  */
  --accent-hover: #6d28d9;  /* violet-700                                    */
  --on-accent: #faf8f4;     /* Crème on violet-600, 5.37:1                  */

  /* The one sanctioned gradient. It caps the column and carries no text.
     It is deliberately NOT the submit button: Crème on the gradient's light
     stop (#8b5cf6) measures 3.99:1, and 16px/600 is not WCAG "large text"
     (that needs 18.66px bold), so a gradient CTA fails AA at its lightest
     stop at any angle. Solid violet-600 gives 5.37:1. Do not "improve" this. */
  --gradient: linear-gradient(135deg, #8b5cf6 0%, #6d28d9 100%);

  --pattern-opacity: 1;

  /* Geometry
     MEASURED from docs/brand/assets/wordmark.svg. Do not guess these.
     viewBox="-5.28 -128.32 601.76 140.32", uniform 9.6-unit padding on all
     sides (left x=4.32, top y=-118.72, right x=586.88).
       --wm-ratio : 601.76 / 140.32
       --wm-lsb   : 9.6 / 601.76   (dead space before the C, as a width frac)
       --wm-dot-y : (-93.32 - -128.32) / 140.32  (tittle centre, height frac)   */
  --wm-ratio: 4.2885;
  --wm-lsb: 0.015953;
  --wm-dot-y: 0.24943;

  --wm-h: 32px;
  --col: 672px;             /* 28 × the pattern's 24px lattice               */
  --pad: 48px;
  --pad-top: 72px;
  /* Two lattice columns. A full 96px tile was rendered against 72px and 48px
     side by side: at 96px each flank carries four columns of dots and roughly
     a dozen violet ones, which out-shouts the submit button (the one mass on
     the page meant to be the brightest) and stops the pattern being
     "quiet" in the sense brand §5 means. 48px reads as a deliberate edge and
     the eye goes to the headline. It is also a multiple of the 24px lattice,
     so the tile still slices cleanly. */
  --ribbon: 48px;
  --bead: 9px;

  /* Pins the pattern's lattice to the wordmark's tittle: the tile's first dot
     row sits at y=12, so offsetting by (tittle y - 12) lands a row of pattern
     dots on the exact horizontal line as the violet tittle. Costs one calc and
     is the difference between a strip that looks drawn and one that looks
     dropped in. */
  --lattice-y: calc(var(--pad-top) + (var(--wm-h) * var(--wm-dot-y)) - 12px);

  --ease: cubic-bezier(0.2, 0, 0, 1);        /* product standard, 120-240ms */
  --ease-pop: cubic-bezier(0.34, 1.4, 0.64, 1); /* the dot's entrance        */
}

/* Dark. Declared twice on purpose: once for the OS preference (guarded so an
   explicit light choice wins) and once for an explicit dark choice, so a future
   toggle works in BOTH directions rather than only away from default. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --ground: #1a1712;
    --text: #faf8f4;
    /* NOT Taupe: #6f685c on Ink is 3.24:1 and fails AA. neutral-400 is
       6.83:1 and is still an in-system token. */
    --text-2: #a89f90;
    --rule: #3a352d;        /* neutral-700                                   */
    --accent: #a78bfa;      /* violet-400, 6.57:1 on Ink                     */
    --accent-hover: #c4b5fd;
    --on-accent: #2e1065;   /* violet-950 on violet-400, 5.60:1              */
    /* The tile is dimmed, never rebuilt. Recolouring it, including
       "reconstructing the same geometry" in CSS at dark values, is a recolour
       by another name and §5 forbids it. Opacity only. */
    --pattern-opacity: 0.24;
  }
}

:root[data-theme="dark"] {
  --ground: #1a1712;
  --text: #faf8f4;
  --text-2: #a89f90;
  --rule: #3a352d;
  --accent: #a78bfa;
  --accent-hover: #c4b5fd;
  --on-accent: #2e1065;
  --pattern-opacity: 0.24;
}

/* --------------------------------------------------------------------------
   4. Page frame: rules and ribbons
   -------------------------------------------------------------------------- */
body {
  position: relative;
  background: var(--ground);
  color: var(--text);
  font-family: "Inter", ui-sans-serif, system-ui, sans-serif;
  font-size: 16px;
  line-height: 1.5;
  overflow-x: hidden;
}

/* The two page edges. Absolute (not fixed) and full document height, so on a
   short phone in landscape they scroll WITH the content instead of sitting
   still behind moving text. */
.edge {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 1px;
  background: var(--rule);
  pointer-events: none;
}

.edge--left  { left:  calc(50% - var(--col) / 2); }
.edge--right { left:  calc(50% + var(--col) / 2); }

/* The booked-dot ribbons, flush against the OUTSIDE of each rule.
   repeat-y only, so the tile never re-tiles horizontally and can never spread
   into a field. background-position-x is `right` so the tile's edge meets the
   rule cleanly at every ribbon width. */
.ribbon {
  position: absolute;
  top: 0;
  bottom: 0;
  width: var(--ribbon);
  background-image: url("assets/pattern-booked.svg");
  background-repeat: repeat-y;
  background-position: right var(--lattice-y);
  opacity: var(--pattern-opacity);
  pointer-events: none;
}

.ribbon--left  { right: calc(50% + var(--col) / 2); }
.ribbon--right {
  left: calc(50% + var(--col) / 2);
  transform: scaleX(-1); /* mirror, so the flanks face each other */
}

/* --------------------------------------------------------------------------
   5. Column
   -------------------------------------------------------------------------- */
.page {
  position: relative;
  width: var(--col);
  max-width: 100%;
  margin-inline: auto;
  padding: var(--pad-top) var(--pad) 40px;
  /* svh, not dvh: on mobile Safari dvh changes as the toolbar collapses, which
     would visibly grow and shrink the ruled page edges mid-scroll. */
  min-height: 100svh;
  display: flex;
  flex-direction: column;
}

/* The head of the bound page: 4px of the sanctioned gradient, spanning the
   column exactly. The only gradient on the site. */
.page::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 4px;
  background: var(--gradient);
}

/* Optically centres the middle group between masthead and footer. */
main {
  margin-block: auto;
}

/* --------------------------------------------------------------------------
   6. Masthead
   -------------------------------------------------------------------------- */
.masthead {
  position: relative;
}

.wordmark {
  display: block;
  height: var(--wm-h);
  width: auto;
  /* The wordmark box carries 9.6 units of uniform viewBox padding before the
     C. Without this pull the mark sits ~2px right of the text edge below it
     and the whole left margin reads soft. Height-relative so it holds at any
     --wm-h. */
  margin-left: calc(var(--wm-h) * var(--wm-ratio) * var(--wm-lsb) * -1);
}

.wordmark .wm-letters { fill: var(--text); }
.wordmark .wm-dot     { fill: var(--accent); }

/* Bead 1, the tittle's twin, projected onto the left rule at the identical
   height. This is the visible proof that the lattice line is real. */
.bead {
  position: absolute;
  left: calc(-1 * var(--pad));
  width: var(--bead);
  height: var(--bead);
  border-radius: 50%;
  transform: translate(-50%, -50%);
}

.bead--mark {
  top: calc(var(--wm-h) * var(--wm-dot-y));
  background: var(--accent);
}

/* --------------------------------------------------------------------------
   7. Headline

   The final full stop is the brand dot at its largest scale, the same object
   as the tittle above it and the slot below it. It is the one thing on the
   page that moves.
   -------------------------------------------------------------------------- */
.headline {
  margin: 96px 0 0;
  font-family: "Fraunces", "Fraunces Fallback", ui-serif, Georgia, serif;
  font-weight: 600;
  font-size: clamp(2.5rem, 1.6rem + 4.4vw, 3.875rem); /* 40px → 62px */
  line-height: 1.05;
  letter-spacing: -0.01em;
  color: var(--text);
  text-wrap: balance;
  /* One bad copy edit must not produce a horizontal scrollbar. */
  hyphens: none;
  overflow-wrap: normal;
}

/* Reserves the two-line box so the column cannot jump if Fraunces swaps in at
   a slightly different measure. Pairs with the metric-matched fallback in §2.
   It is a MODIFIER, not a base style: the home headline is two lines (three on
   a phone, where a two-line floor is simply inert), but /thanks and /404 are
   one line each and would otherwise carry a dead line of reserved space under
   the headline. */
.headline--reserve {
  min-height: calc(2 * 1.05em);
}

.full-stop {
  display: inline-block;
  /* MEASURED, not guessed: a Fraunces SemiBold period at 62px carries exactly
     10x10px of ink and its advance is 12.44px. 0.19em = 11.8px = 1.18x that
     ink. Large enough to read as the brand mark, small enough to still read
     as terminal punctuation rather than a bullet parked after the word. The
     side bearings are ~1.2px each at this size, hence the 0.02em nudge. */
  width: 0.19em;
  height: 0.19em;
  margin-left: 0.02em;
  /* The real period overshoots 1px below the baseline, as round glyphs do so
     they don't optically float. Scaled to this dot that is 1.18px = 0.019em.
     With vertical-align: baseline the bottom MARGIN edge sits on the baseline,
     so a negative bottom margin drops the circle by exactly that overshoot. */
  margin-bottom: -0.019em;
  border-radius: 50%;
  background: var(--accent);
  vertical-align: baseline;
  /* The origin matters: the dot rises onto the baseline and settles, rather
     than inflating about its own middle. */
  transform-origin: 50% 100%;
  /* The RESTING state is visible, and the animation only hides the dot during
     its delay (fill-mode: backwards). The obvious construction (rest at
     scale(0) and animate to scale(1) with `forwards`) permanently deletes the
     headline's period on any page where the animation never runs: a background
     tab that is never focused, a paused/throttled compositor, or animations
     disabled outright. The period is punctuation, not decoration; it must
     survive its own animation failing. */
  transform: scale(1);
  animation: pop 500ms var(--ease-pop) 300ms backwards;
}

/* 300ms, not the 620ms a longer swap-clearing delay would want. Both fonts are
   preloaded and self-hosted, so there is no swap reflow to outwait, and the
   delay is the one window in which the headline renders with no punctuation:
   `backwards` fill shows the 0% keyframe until the animation starts. Keep it
   short. (A frozen animation clock, such as a never-focused background tab or
   a headless screenshotter, holds the dot in that window indefinitely; it
   resolves as soon as the page is actually visible, which is the only case a
   reader is in.) */
@keyframes pop {
  from { transform: scale(0); }
  to   { transform: scale(1); }
}

.lede {
  /* Ink, not Taupe: this is the only place the page says what Clemaine is, and
     its audience is salon owners, not all of whom are 28. */
  margin: 24px 0 0;
  max-width: 42ch;
  font-size: clamp(1.0625rem, 1.02rem + 0.18vw, 1.125rem); /* 17 → 18px */
  line-height: 1.55;
  color: var(--text);
}

/* --------------------------------------------------------------------------
   8. Form: the page's one job
   -------------------------------------------------------------------------- */
.signup {
  position: relative;
  margin-top: 40px;
}

.row {
  display: flex;
  gap: 8px;
  align-items: stretch;
}

.field {
  flex: 1 1 auto;
  min-width: 0;
}

.email {
  width: 100%;
  height: 52px;
  padding: 0 14px;
  border: 1px solid var(--field-border);
  border-radius: 2px;
  background: transparent;
  color: var(--text);
  font: inherit;
  /* Exactly 16px. Anything smaller and iOS zooms the viewport on focus, which
     throws the whole composition. */
  font-size: 16px;
  transition: border-color 160ms var(--ease), box-shadow 160ms var(--ease);
}

.email::placeholder {
  color: var(--text-2);
  opacity: 1; /* Firefox dims placeholders by default, which would drop it
                 below AA. */
}

.email:focus-visible {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent);
}

.submit {
  flex: 0 0 auto;
  height: 52px;
  padding: 0 22px;
  border: 1px solid var(--accent);
  border-radius: 2px;
  background: var(--accent);
  color: var(--on-accent);
  font: inherit;
  font-size: 16px;
  font-weight: 600;
  letter-spacing: 0.005em;
  cursor: pointer;
  transition: background-color 160ms var(--ease), border-color 160ms var(--ease);
}

.submit:hover {
  background: var(--accent-hover);
  border-color: var(--accent-hover);
}

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

/* Bead 3, the open slot, on the rule at the input's exact half-height.
   Hollow until you fill it. */
.bead--slot {
  top: 26px;
  background: transparent;
  /* The field border, not the page rule: this bead belongs to the form and
     carries meaning (the slot you fill), so it has to be perceptible. At
     --rule it was a ghost. */
  border: 1.5px solid var(--field-border);
  transition: border-color 200ms var(--ease), background-color 200ms var(--ease);
}

/* :user-valid is a nicety, not the affordance. The bead sits 48px away in the
   gutter, and engine timing for :user-valid differs. The input keeps its own
   conventional focus treatment above regardless. */
.signup:has(.email:user-valid) .bead--slot {
  border-color: var(--accent);
}

.fine {
  margin: 12px 0 0;
  font-size: 13px;
  line-height: 1.45;
  color: var(--text-2);
}

.fine--spaced {
  margin-top: 40px;
}

.fine a {
  color: var(--text);
  text-decoration: underline;
  text-underline-offset: 2px;
  text-decoration-color: var(--rule);
  transition: color 160ms var(--ease), text-decoration-color 160ms var(--ease);
}

.fine a:hover {
  color: var(--accent);
  text-decoration-color: var(--accent);
}

.fine a:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 1px;
}

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

/* --------------------------------------------------------------------------
   9. Footer
   -------------------------------------------------------------------------- */
.colophon {
  margin-top: 72px;
  padding-top: 20px;
  display: flex;
  flex-wrap: wrap;
  gap: 8px 24px;
  font-size: 13px;
  color: var(--text-2);
}

.colophon a {
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 2px;
  text-decoration-color: var(--rule);
  transition: color 160ms var(--ease), text-decoration-color 160ms var(--ease);
}

.colophon a:hover {
  color: var(--accent);
  text-decoration-color: var(--accent);
}

.colophon a:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 1px;
}

/* --------------------------------------------------------------------------
   10. Utility
   -------------------------------------------------------------------------- */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* --------------------------------------------------------------------------
   11. Responsive

   The ribbons narrow but never disappear. Most traffic to a coming-soon page
   is mobile, and deleting the pattern below 768px would delete the organising
   idea for the majority of visitors. The tile repeats on a 24px lattice,
   so it slices cleanly to 48px (two lattice columns) and 24px (one).
   -------------------------------------------------------------------------- */
@media (max-width: 767px) {
  :root {
    --ribbon: 24px;
    --wm-h: 28px;
    --pad: 24px;
    --pad-top: 48px;
    --col: 100%;
  }

  /* The column now fills the space between the ribbons rather than being a
     fixed 672px, so the rules sit just inside them. */
  .edge--left   { left: var(--ribbon); }
  .edge--right  { left: auto; right: var(--ribbon); }
  .ribbon--left { right: auto; left: 0; }
  .ribbon--right{ left: auto; right: 0; }

  .page {
    width: 100%;
    padding-inline: calc(var(--ribbon) + var(--pad));
  }

  .bead { left: calc(-1 * var(--pad)); }

  .headline { margin-top: 64px; }
  .colophon { margin-top: 56px; }
}

@media (max-width: 479px) {
  .row { flex-direction: column; }
  .submit { width: 100%; }
  /* With the button stacked, the slot bead still marks the input, not the
     group. */
  .bead--slot { top: 26px; }
}

/* --------------------------------------------------------------------------
   12. Motion & print
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }

  /* Belt and braces: the full stop is transform: scale(0) at rest and is only
     brought to 1 by the animation. If animations are suppressed entirely the
     headline would lose its period, so pin it visible. */
  .full-stop {
    animation: none;
    transform: none;
  }
}

@media print {
  .ribbon,
  .edge { display: none; }
  .full-stop { transform: none; animation: none; }
  body { background: #fff; color: #000; }
}
