/* ===========================================================================
   TRACEY MEMBER APP
   Ported from the Claude Design handover, 2026-09-09. Type, colour, spacing
   and copy are theirs and signed off — do not paraphrase either.

   Animates transform and opacity ONLY, at both motion levels. Everything else
   forces layout or paint every frame and stutters on any device.          */

/* La Fabrique Pro is the brand face; Annabelle carries the one handwritten
   note on the login screen and nothing else.
   TODO before launch: convert to WOFF2. TTF is ~40% larger over the wire and
   this is 500KB on mall wifi. */
@font-face{font-family:"La Fabrique Pro";src:url("../assets/fonts/LaFabriquePro-Light.ttf") format("truetype");
  font-weight:300;font-style:normal;font-display:swap}
@font-face{font-family:"La Fabrique Pro";src:url("../assets/fonts/LaFabriquePro-Regular.ttf") format("truetype");
  font-weight:400;font-style:normal;font-display:swap}
@font-face{font-family:"La Fabrique Pro";src:url("../assets/fonts/LaFabriquePro-Italic.ttf") format("truetype");
  font-weight:400;font-style:italic;font-display:swap}
@font-face{font-family:"La Fabrique Pro";src:url("../assets/fonts/LaFabriquePro-SemiBold.ttf") format("truetype");
  font-weight:600;font-style:normal;font-display:swap}
@font-face{font-family:"Annabelle";src:url("../assets/fonts/Annabelle.ttf") format("truetype");font-display:swap}

:root{
  --ink:#14120f; --ink-2:#3b3730; --paper:#f4efe6; --paper-2:#e7e0d3;
  --line:rgba(20,18,15,.14); --accent:#c2411f; --coral:#ff5a37;
  --sans:"La Fabrique Pro",-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif;
  --script:"Annabelle","Snell Roundhand",cursive;
  /* NEVER REQUEST WEIGHT 500 — not in this family, silently resolves to 400.
     It is a grotesque, so display sizes take Light 300 and headings 600. */
  --dur:.16s; --ease:ease;
  --safe-top:max(env(safe-area-inset-top),16px);
  --safe-bottom:max(env(safe-area-inset-bottom),16px);
}
[data-motion="full"]{--dur:.42s; --ease:cubic-bezier(.2,.8,.2,1)}

*{box-sizing:border-box}
[hidden]{display:none!important}
html,body{margin:0;height:100%}
/* NO WORDING ANYWHERE CAN BE SELECTED — YC, 2026-09-23. Same pair as app.html,
   404.html, terms.html and privacy.html: user-select kills the selection,
   -webkit-touch-callout stops iOS raising Copy / Look Up on a long press over
   unselectable text. The input exemption is load-bearing — without it a member
   cannot put the caret back into the email field to fix a typo. */
html{-webkit-user-select:none;-ms-user-select:none;user-select:none;
  -webkit-touch-callout:none}
input,textarea{-webkit-user-select:text;user-select:text}
body{background:#191713;color:var(--ink);font-family:var(--sans);
  -webkit-font-smoothing:antialiased;overflow:hidden}
button{font:inherit;color:inherit;background:none;border:0;cursor:pointer}

.app{position:fixed;inset:0}
.view{position:absolute;inset:0;display:none;flex-direction:column;overflow:hidden}
.view[data-on]{display:flex}

/* ── photograph plate ─────────────────────────────────────────────────────
   The shot is the interface; type lives in the empty part of the frame. */
.plate{position:absolute;inset:0;overflow:hidden;background:var(--paper-2)}
/* both tones on the dark ground (27 Oct): a light first photo used to turn the ground cream in one frame and then fade
   in over the cream - with the 1.6 s fade that flash was plain to see. The ground shows only before a photo lands. */
.plate[data-tone="dark"],.plate[data-tone="light"]{background:#231f1b}
/* 1.6s ease-in-out: the login slideshow's cross-fade (YC, 27 Oct: "make the transition smooth"). login.js lays the new
   shot on top of the old one and hides the old only after the fade (its FADE must match this). */
/* THE PHOTO EXACTLY AS UPLOADED (YC, 27 Oct: "the size of the photo must be the size of the picture showing on the
   screen. assume the picture is centered. should not expand or contract the picture"). It was cropped to fill the
   screen and made 12% taller than it, so a laptop showed a thin, blown-up band of a tall photo. Now it is shown whole,
   centred, as large as the screen allows (contain - never cropped, never stretched). The space around it, where the
   screen is another shape, is the same photo again, soft and darkened, so there are no plain bars (YC, 27 Oct: "2. ok").
   A computer shows the 1920 x 1080 picture, a phone the 1080 x 1920 one (Office -> Login slideshow). login.js sets the
   photo as --img on the layer; :before is the soft copy, :after the photo itself. */
.plate .layer{position:absolute;inset:0;overflow:hidden;opacity:0;transition:opacity 1.6s ease-in-out}
.plate .layer:before,.plate .layer:after{content:"";position:absolute;background-image:var(--img,none);
  background-repeat:no-repeat;background-position:center}
.plate .layer:before{inset:-48px;background-size:cover;filter:blur(28px) brightness(.55)}
/* FROM THE TOP, centred side to side (YC, 27 Oct, on his phone: "the word tracey is higher up...the pic should at least
   reach this height"). Centred top to bottom, a tall phone showed a band above the photo; the spare space now goes
   to the foot, behind the sign-in form. A computer is unchanged: a photo that fits its height has no spare height. */
.plate .layer:after{inset:0;background-size:contain;background-position:50% 0}
.plate .layer[data-vis="1"]{opacity:1}
.plate .scrim{position:absolute;inset:0;pointer-events:none}
/* The type block starts around 55%, so the scrim has to be doing real work by
   then. These shots measure 0.64–0.69 luminance where it lands and cream
   9.5px type needs 4.5:1 — which is why "light" carries the heavier gradient. */
.plate[data-tone="dark"] .scrim{background:linear-gradient(180deg,rgba(12,10,9,.4) 0%,
  rgba(12,10,9,.08) 28%,rgba(12,10,9,.66) 56%,rgba(12,10,9,.88) 100%)}
.plate[data-tone="light"] .scrim{background:linear-gradient(180deg,rgba(12,10,9,.3) 0%,
  rgba(12,10,9,.06) 24%,rgba(12,10,9,.72) 55%,rgba(12,10,9,.9) 100%)}
.plate .grain{position:absolute;inset:0;pointer-events:none;opacity:.5;
  background-image:radial-gradient(rgba(255,255,255,.055) .5px,transparent .5px);
  background-size:3px 3px}

/* The scrim dips to α0.06 at 24% to keep the top of the shot bright, which
   leaves the wordmark on raw photograph at 1.6:1 on the brightest frames.
   This grounds that corner only and fades out by 176px.

   The handover's adaptive-ink sampler picks per-shot colours from the pixels
   behind each element — it is the one genuinely novel piece and deserves doing
   properly rather than approximately. NOT PORTED YET. This is its documented
   fallback: veil always on, cream throughout, measured safe on all five shots
   at the cost of a permanently darker top-left corner. */
.brandveil{position:absolute;top:0;left:0;right:0;height:176px;pointer-events:none;
  background:radial-gradient(120% 57% at 18% 42%,rgba(12,10,9,.66) 0%,rgba(12,10,9,.6) 45%,
    rgba(12,10,9,.22) 78%,transparent 100%)}

/* ── login ──────────────────────────────────────────────────────────────── */
#login{color:#f6f1e8}
.brandzone{position:absolute;top:calc(14px + var(--safe-top));left:28px;right:28px;z-index:6}

/* the wordmark is the way to our other places, so it has to look pressable */
.wordmark{display:inline-flex;align-items:center;min-height:44px;padding:0;
  font:400 18px/1 var(--sans);letter-spacing:.04em;color:#f6f1e8;
  filter:drop-shadow(0 2px 14px rgba(0,0,0,.5));
  transition:transform var(--dur) var(--ease),opacity var(--dur) var(--ease)}
.wordmark:active{transform:scale(.97)}
.wordmark[aria-expanded="true"]{opacity:.75}
.wordmark img{height:23px;width:auto;display:block;filter:brightness(0) invert(1)}

/* the nudge: a drawn scribble, because a chevron here would read as chrome */
.scribble{position:absolute;top:27px;left:-2px;pointer-events:none;color:#f0d9b4;
  display:flex;align-items:flex-end}
.scribble svg{width:44px;height:52px;overflow:visible;flex:none;
  filter:drop-shadow(0 1px 4px rgba(0,0,0,.45))}
.scribble em{margin:0 0 6px 5px;font:400 23px/1 var(--script);white-space:nowrap;
  font-style:normal;text-shadow:0 1px 8px rgba(0,0,0,.55)}
[data-motion="full"] .scribble{animation:nudge 2.8s ease-in-out infinite}
/* once the menu is open the nudge has done its job — animation:none is
   required, or the keyframes' own opacity keeps overriding this rule */
.brandzone[data-open] .scribble{animation:none;opacity:0;visibility:hidden;
  transition:opacity var(--dur) var(--ease)}

/* no panel — each dot has to hold its own over photography, so it carries a
   translucent ink fill, a hairline and a shadow to lift it off the shot */
.brandmenu{margin-top:12px}
.brandmenu .social{display:flex;flex-direction:column;gap:9px}
.soc{display:grid;place-items:center;width:46px;height:46px;border-radius:50%;
  border:1px solid rgba(246,241,232,.55);color:#f6f1e8;text-decoration:none;
  font:600 11px/1 var(--sans);letter-spacing:.02em;
  /* the chip is the dots' only ground; .64 is measured to clear 4.5:1 on every
     shot with the veil on or off, and is NOT adaptive. Do not reduce it. */
  background:rgba(20,18,15,.64);filter:drop-shadow(0 2px 9px rgba(0,0,0,.5));
  transition:transform var(--dur) var(--ease),opacity var(--dur) var(--ease)}
[data-motion="full"] .soc{backdrop-filter:blur(9px)}
.soc:active{transform:scale(.93)}
/* THE SAME MARKS THE APP USES, so the login page and the app do not disagree
   about what Tracey's buttons look like. The artwork is a MASK over
   currentColor, which is why a single file works on this dark plate and on the
   app's cream sheet without a second copy. The letterform underneath stays as
   the fallback: if a file ever fails to load, the button reads IG / f / TT / XHS
   instead of going blank, and nothing has to notice.
   XiaoHongShu is wider because its published mark is a wordmark, not a symbol —
   same reasoning as in app.html. */
.soc-i{display:none}
.soc-i[data-ready]{display:block;width:27px;height:27px;background:currentColor;
  -webkit-mask:var(--soc-src) center/contain no-repeat;mask:var(--soc-src) center/contain no-repeat}
.soc-i[data-soc="xiaohongshu"][data-ready]{width:34px;height:34px}
.soc-i[data-ready]+span{display:none}
[data-motion="full"] .brandmenu[data-open] .soc{animation:dotDrop .5s cubic-bezier(.22,.9,.28,1) backwards}
[data-motion="full"] .brandmenu[data-open] .soc:nth-child(2){animation-delay:.09s}
[data-motion="full"] .brandmenu[data-open] .soc:nth-child(3){animation-delay:.18s}
[data-motion="full"] .brandmenu[data-open] .soc:nth-child(4){animation-delay:.27s}
[data-motion="full"] .brandmenu[data-open] .soc:nth-child(5){animation-delay:.36s}

/* --kb is the keyboard's height, published by js/keyboard.js on Chrome for
   Android (0 everywhere else). Full screen does not shrink the page for the
   keyboard, so the panel lifts itself - gently - and settles back. YC, 29 Sep:
   "make it hover above the keyboard". While the keyboard is up it covers the
   gesture bar, so the safe-area allowance is dropped then. */
.login-body{position:relative;margin-top:auto;z-index:5;
  padding:34px 28px calc(28px + var(--safe-bottom));color:#f6f1e8;
  transition:padding-bottom .28s cubic-bezier(.2,.8,.2,1);
  background:linear-gradient(180deg,rgba(12,10,9,0) 0%,rgba(12,10,9,.26) 22%,rgba(12,10,9,.4) 100%)}
[data-kb] .login-body{padding-bottom:calc(20px + var(--kb,0px))}
/* with half the screen taken by the keyboard, the big heading steps down so the
   box being typed in, and the button under it, stay in view */
.display{transition:font-size .28s cubic-bezier(.2,.8,.2,1)}
[data-kb] .display{font-size:clamp(30px,9vw,38px)}

.eyebrow{font:600 10px/1 var(--sans);letter-spacing:.24em;text-transform:uppercase;margin:0 0 14px}
.display{font:300 clamp(40px,13vw,54px)/.92 var(--sans);margin:0 0 18px;
  letter-spacing:-.01em;text-wrap:balance}
.display em{font-style:italic;color:#e8c9a2}
/* The returning-device step sets this to the member's email address, which is
   the one piece of display type in the app whose length we do not control. At
   54px an unbroken address runs off the side of the phone, so it is allowed to
   break mid-word — only here, and only for that reason. Every other .display em
   is copy we wrote and balances on its own. */
#knownName{overflow-wrap:anywhere}
.lede{font:400 14px/1.5 var(--sans);margin:0 0 26px;max-width:30ch}

.field{display:block;margin:0 0 12px}
.field span{display:block;font:600 10px/1 var(--sans);letter-spacing:.16em;
  text-transform:uppercase;margin-bottom:8px}
.field input{width:100%;height:52px;padding:0 14px;border-radius:2px;
  border:1px solid rgba(246,241,232,.42);background:rgba(20,18,15,.34);
  color:#f6f1e8;font:400 16px var(--sans);outline:0;  /* 16px or iOS zooms on focus */
  transition:transform var(--dur) var(--ease),opacity var(--dur) var(--ease)}
.field input::placeholder{color:rgba(246,241,232,.45)}
.field input:focus{border-color:#f6f1e8}

/* Six boxes, one real input. The input is transparent and laid over the row so
   autofill, paste and the OS one-time-code suggestion all still work — a row
   of six separate inputs breaks every one of those and is a known accessibility
   trap. The boxes only mirror what the input holds. */
.pinwrap{position:relative;margin:0 0 14px}
.pinrow{display:flex;gap:8px}
.pinrow i{flex:1;height:58px;border-radius:2px;border:1px solid rgba(246,241,232,.42);
  background:rgba(20,18,15,.34);display:grid;place-items:center;
  font:400 24px/1 var(--sans);color:#f6f1e8;font-style:normal}
.pinrow i[data-filled]{border-color:#f6f1e8}
.pinwrap input{position:absolute;inset:0;width:100%;height:100%;opacity:0;
  border:0;background:none;font-size:16px;letter-spacing:2em;color:transparent;
  caret-color:transparent}
.pinwrap input:focus{outline:0}
.pinwrap input:focus + .pinrow i[data-next]{border-color:#f6f1e8}

.btn{display:flex;align-items:center;justify-content:center;gap:10px;width:100%;
  min-height:52px;border-radius:2px;background:#f6f1e8;color:#14120f;
  font:600 13px/1 var(--sans);letter-spacing:.1em;text-transform:uppercase;
  transition:transform var(--dur) var(--ease),opacity var(--dur) var(--ease)}
.btn:active{transform:scale(.985)}
.btn[disabled]{opacity:.55;cursor:default}
.btn.ghost{background:none;color:#f6f1e8;border:1px solid rgba(246,241,232,.42)}
.quiet{display:block;width:100%;text-align:center;min-height:44px;
  font:400 13.5px/44px var(--sans);color:#f6f1e8;text-decoration:underline;
  text-underline-offset:3px;border:0}

.sent{margin:0 0 18px;padding:12px 14px;border-left:2px solid #e8c9a2;
  background:rgba(20,18,15,.4);font:400 13.5px/1.5 var(--sans)}

/* Never "login failed". A code that did not match is ours to fix, not the
   member's to be told off about. */
.msg{margin:0 0 14px;padding:12px 14px;border-radius:2px;
  font:400 13px/1.45 var(--sans);
  background:rgba(20,18,15,.44);border:1px solid rgba(246,241,232,.3)}

.spinner{width:15px;height:15px;border-radius:50%;
  border:2px solid rgba(20,18,15,.25);border-top-color:var(--ink);
  animation:spin .7s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}

/* ── motion ───────────────────────────────────────────────────────────────
   backwards, NEVER both: a finished `both` animation keeps overriding normal
   declarations and silently kills every :active transform underneath it. */
@keyframes rise{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}}
@keyframes nudge{0%,100%{opacity:.62}50%{opacity:1}}
@keyframes dotDrop{from{opacity:0;transform:translateY(-14px) scale(.8)}to{opacity:1;transform:none}}
@keyframes dropBounce{
  0%{transform:translateY(-10px) scale(.98,1.07)}
  38%{transform:translateY(0) scale(1.07,.9)}      /* lands and squashes */
  60%{transform:translateY(-5px) scale(.99,1.04)}  /* one rebound, no more */
  100%{transform:none}
}
[data-motion="full"] .stagger > * > *{animation:rise .5s var(--ease) backwards}
[data-motion="full"] .stagger > * > *:nth-child(2){animation-delay:.06s}
[data-motion="full"] .stagger > * > *:nth-child(3){animation-delay:.12s}
[data-motion="full"] .stagger > * > *:nth-child(4){animation-delay:.18s}
[data-motion="full"] .stagger > * > *:nth-child(5){animation-delay:.24s}
[data-motion="full"] .wordmark{animation:dropBounce .95s var(--ease)}

/* Minimal drops the crossfade, the pulse, the droplet and the blur, and takes
   the 700px hero. It must still feel crisp — brisk, not absent. */
[data-motion="minimal"] .plate .layer{transition:none}

@media (prefers-reduced-motion:reduce){
  *,*::before,*::after{animation-duration:.001ms!important;animation-iteration-count:1!important;
    transition-duration:.001ms!important}
}
