﻿/* Portal screens (form / loading / success). Tokens come from tokens.css. */

* { box-sizing: border-box; }

body {
  margin: 0;
  padding: 0 1.25rem;
  min-height: 100dvh;
  background: var(--paper);
  color: var(--ink);
  /* Calibre is the brand face, pending license (build plan 1.3). */
  font-family: 'Calibre', Inter, system-ui, 'Segoe UI', Roboto, Arial, sans-serif;
  font-size: 1rem;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

/* Vertically centered pages: margin:auto (not place-items) so tall content
   scrolls instead of clipping at the top */
body.vcenter { display: grid; }
body.vcenter .wrap { margin: auto; }

.wrap { width: 100%; max-width: 380px; margin: 0 auto; padding: 2rem 0 2.5rem; }
.center { text-align: center; }

@media (min-width: 640px) {           /* desktop = graceful scale-up, nothing more */
  .wrap { max-width: 26rem; padding-top: 3.5rem; }
}

/* Brand block -- official logo asset (unlicensed seeklogo copy: demo only).
   The logo is the page's anchor: the only strong mark on the screen. */
.brand { text-align: center; margin-bottom: 2rem; }
.brand img {
  width: 140px; height: auto;
  aspect-ratio: 129.645 / 140.258;   /* SVG intrinsic: layout never shifts on load */
}

h1 { font-size: 1.4rem; font-weight: 500; letter-spacing: .01em; margin: 1.2rem 0 0; }
.sub { color: var(--soft); font-size: .9rem; margin: .8rem 0 0; }
.brand .sub { text-align: left; }   /* subtitle shares the fields' left edge */

/* Fields -- Variant B: notched outline, label masks the border. Requires
   placeholder=" " on the input and the label directly after it in the DOM.
   28px between groups; proximity does the grouping. */
/* Collision-proof geometry, all reserved IN THIS BOX:
   - padding-top: 10px swallows the raised label's protrusion (label line-height
     18px -> protrudes 9px above the control's border; 10 >= 9). Absolute
     children anchor to the wrapper's padding box, so every top anchor below is
     offset by the same 10px.
   - .msg is a permanent one-line slot toggled via visibility (never display),
     so validation firing shifts nothing. */
/* margin-bottom stays small: the permanent .msg slot (6+19px) already spaces
   the groups, so big margins double-count */
.field { position: relative; padding-top: 10px; margin-bottom: 8px; }
.msg {
  min-height: 19px;              /* exactly one line: 12.8px font x 19px lh */
  line-height: 19px;
  margin: 6px 0 0;
  font-size: .8rem;
  color: var(--error);
  visibility: hidden;
}
.msg:not(:empty) { visibility: visible; }

/* Direct-child scoping throughout: Tom Select nests its own <input> inside
   .field, and a descendant selector would balloon it to 52px full-width. */
.field > input {
  width: 100%; height: 52px; padding: 0 14px;
  border: 1px solid var(--field-line); border-radius: var(--r);
  background: transparent;
  font: inherit;                 /* 16px floor: stops iOS auto-zoom */
  color: var(--ink);
}
.field > label {
  /* 36px = wrapper padding-top (10) + control half-height (26); anchored to
     the control's fixed center, never to a % of the growing wrapper */
  position: absolute; left: 10px; top: 36px;
  transform: translateY(-50%);
  padding: 0 5px;
  background: var(--paper);      /* masks the border = the notch */
  color: var(--ink-mute);
  font-size: .95rem;
  transition: all .18s ease;
  pointer-events: none;
}
.field > input:focus + label,
.field > input:not(:placeholder-shown) + label {
  top: 10px;                     /* the control's top border (= padding-top) */
  font-size: 12px; color: var(--ink);
}
.field > input:focus { border-color: var(--ink); outline: none; }

/* Error: one signal -- a single thin red border; icon + message carry the rest */
.field > input[aria-invalid="true"] { border-color: var(--error); }
.field > input[aria-invalid="true"] + label { color: var(--error); }

/* Phone: +216 always visible; label rests over the input area, notches left */
.field > .prefix {
  position: absolute; left: 14px; top: 36px; transform: translateY(-50%);
  color: var(--soft); pointer-events: none;
}
.field.has-prefix > input { padding-left: 62px; }
.field.has-prefix > label { left: 58px; }
.field.has-prefix > input:focus + label,
.field.has-prefix > input:not(:placeholder-shown) + label { left: 10px; }

/* Select -- Tom Select (vendored) skinned to the B fields; label permanently
   notched (a select is never empty). */
.nlabel {
  position: absolute; top: 10px; left: 10px; transform: translateY(-50%);
  padding: 0 5px; background: var(--paper);
  font-size: 12px; color: var(--ink-mute);
  pointer-events: none; z-index: 2;
}
.field .ts-wrapper .ts-control {
  border: 1px solid var(--field-line); border-radius: var(--r);
  padding: 0 40px 0 14px; min-height: 52px;
  display: flex; align-items: center;
  font: inherit; color: var(--ink);
  box-shadow: none;
}
.field .ts-wrapper.focus .ts-control { border-color: var(--ink); box-shadow: none; }
.field.err .ts-wrapper .ts-control { border-color: var(--error); }
.field.err .nlabel { color: var(--error); }
.field .ts-wrapper::after {        /* animated caret, rotates on open */
  content: ''; position: absolute; right: 16px; top: 50%;
  width: 8px; height: 8px; margin-top: -6px;
  border-right: 1.5px solid var(--ink-mute); border-bottom: 1.5px solid var(--ink-mute);
  transform: rotate(45deg); transform-origin: 66% 66%;
  transition: transform .2s ease;
  pointer-events: none;
}
.field .ts-wrapper.dropdown-active::after { transform: rotate(225deg); }
.field .ts-dropdown {
  border: 1px solid var(--field-line); border-radius: var(--r);
  margin-top: 4px; font: inherit;
}
.field .ts-dropdown .active { background: var(--paper-hint, #F5F0E6); color: var(--ink); }

.consent {
  display: flex;
  gap: .5rem;
  align-items: flex-start;
  margin: 22px 0 0;              /* the .msg slot below provides the bottom rhythm */
}
/* Bootstrap-style custom checkbox, our tokens: 24px square in a 44px tap
   target, black fill + white checkmark on check, 8px radius to match buttons */
.checkwrap {
  width: 44px; height: 44px;
  flex: none;
  display: grid; place-items: center;
  margin: -10px 0 0 -10px;       /* big target without breaking the field edge */
  cursor: pointer;
}
.consent input {
  appearance: none;
  -webkit-appearance: none;
  width: 24px; height: 24px;
  margin: 0;
  background: var(--paper);
  border: 1px solid var(--field-line);
  border-radius: var(--r);
  cursor: pointer;
  transition: background-color .15s, border-color .15s;
}
.consent input:checked {
  background-color: var(--ink);
  border-color: var(--ink);
  background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='%23fff' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='M5 10.5l3.5 3.5L15 7'/%3e%3c/svg%3e");
  background-size: 16px;
  background-position: center;
  background-repeat: no-repeat;
}
.consent input:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
.consent label { font-size: .8rem; color: var(--soft); margin: 0; }

/* Buttons -- black, 14px bold, 8px corners */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: .55rem;
  width: 100%;
  min-height: 48px;
  padding: 0 1rem;
  font: inherit;
  font-size: 14px;
  font-weight: 700;
  letter-spacing: .02em;
  color: var(--paper);
  background: var(--ink);
  border: 0;
  border-radius: var(--r);
  cursor: pointer;
  text-decoration: none;
  transition: opacity .15s;
}
.btn:hover { opacity: .88; }
.btn[aria-disabled="true"] { opacity: .45; cursor: default; }
.btn svg { flex: none; }

/* Phase-1 spinner: the button holds the wait while the backend dedupe runs.
   State class is `busy`, NOT `loading` -- .loading is the fullscreen overlay,
   and reusing it turned the button INTO the overlay (the white-veil bug).
   Pattern per progress-button practices: progress inside, label shown, no
   color change. */
.btn .spinner {
  display: none;
  width: 1em; height: 1em;
  margin-right: .5em;
  border: 2px solid currentColor;
  border-top-color: transparent;
  border-radius: 50%;
  animation: spin .6s linear infinite;
}
.btn.busy { pointer-events: none; }
.btn.busy .spinner { display: inline-block; }
@keyframes spin { to { transform: rotate(360deg); } }
.btn:focus-visible, a:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 2px;
}

/* Consent's slot sits tight under the consent text, with air before the button */
.consent + .msg { margin-bottom: 14px; }
.banner {
  color: var(--error);
  border: 1px solid var(--error);
  border-radius: var(--r);
  padding: .75rem;
  font-size: .875rem;
}

/* ============ Screen 2 -- single-stage loading: the card IS the progress ============ */
.loading {
  position: fixed; inset: 0; z-index: 50;
  display: grid; place-items: center;
  background: var(--paper);
  padding: 1.25rem;
  overflow: hidden;             /* the card enters/exits off-screen */
}
.loading[hidden] { display: none; }
.loading-inner { width: min(240px, 80vw); text-align: center; }

.card-stage { margin-bottom: 1.6rem; }
.card-stage svg {
  display: block;
  width: 100%; height: auto;
  stroke: var(--ink); stroke-width: var(--stroke-w);
  stroke-linecap: round; stroke-linejoin: round; fill: none;
}

/* Two-stage text, synced to the card's lifecycle (never to timers):
   text1 fades in when idle starts; on the 200 it crossfades to text2 while
   the card is still center-stage; both fade out as the card exits. */
.loading-label { font-size: 1rem; margin: 0; opacity: 0; transition: opacity 200ms; }
.loading.waiting .loading-label { opacity: 1; }
.loading.waiting .loading-label.swap { opacity: 0; }   /* mid-crossfade */
.loading.leaving .loading-label { opacity: 0; }

/* Page 1 -> 2 departure: the form content fades left fast, then the card
   animation takes the stage. */
body.depart .wrap { opacity: 0; transform: translateX(-28px); }
.wrap { transition: opacity .18s ease, transform .18s ease; }

/* Phase 1/3: position is the progress -- enter from off-screen left, exit right */
.card-stage.enter { animation: stage-in 600ms ease-out both; }
.card-stage.exit  { animation: stage-out 500ms ease-in both; }
@keyframes stage-in  { from { transform: translateX(calc(-50vw - 160px)); } to { transform: none; } }
@keyframes stage-out { from { transform: none; } to { transform: translateX(calc(50vw + 160px)); } }

/* Drift + streaks run through ALL three phases (as in the committed baseline,
   b18e429) -- they compose with the stage's enter/exit transform, so the card
   trails streaks while entering and exiting too.
   Subtle parallelogram lean -- understated, not dramatic. */
.card-g {
  transform-box: fill-box;
  transform-origin: center;
  transform: skewX(-7deg);
  animation: drift .6s ease-in-out infinite alternate;
}
@keyframes drift {
  from { transform: skewX(-7deg) translateX(-4px); }
  to   { transform: skewX(-7deg) translateX(4px); }
}

/* The card barely drifts -- the speed lines do the work */
.speed-line { opacity: 0; animation: speed .5s linear infinite; }
.speed-line:nth-child(2) { animation-delay: .09s; }
.speed-line:nth-child(3) { animation-delay: .18s; }
.speed-line:nth-child(4) { animation-delay: .27s; }
@keyframes speed {          /* trailing fade is what reads as speed, not jitter */
  0%   { transform: translateX(30px);  opacity: 0; }
  50%  {                               opacity: 1; }
  100% { transform: translateX(-60px); opacity: 0; }
}

/* ============ Screen 3 -- ready ============ */
/* Checkmark: user-supplied draw animation, color mapped to the portal ink.
   JS offsets it to viewport center for the draw, then clears the transform --
   this transition is the "slide up" to its layout slot. */
.tick {
  width: 100px;
  display: block;
  margin: 0 auto 1.2rem;
  color: var(--ink);
  transition: transform .45s ease;
}

/* Everything below the checkmark hides until the draw completes, then fades
   in upward (staggered via JS transition-delay). */
.reveal { opacity: 0; transform: translateY(14px); transition: opacity .4s ease, transform .4s ease; }
.reveal.in { opacity: 1; transform: none; }
.path {
  stroke-dasharray: 1000;
  stroke-dashoffset: 0;
}
.path.circle { animation: dash .9s ease-in-out; }
.path.check {
  stroke-dashoffset: -100;
  animation: dash-check .9s .35s ease-in-out forwards;
}
@keyframes dash {
  0%   { stroke-dashoffset: 1000; }
  100% { stroke-dashoffset: 0; }
}
@keyframes dash-check {
  0%   { stroke-dashoffset: -100; }
  100% { stroke-dashoffset: 900; }
}
/* Failure cross (Styling/assets/check_cross): two strokes draw after the circle,
   reusing the `dash` keyframe. No tint -- it inherits ink via currentColor,
   same as the success check (black/white scheme, spec: de-claudified). */
.path.line {
  stroke-dashoffset: 1000;
  animation: dash .9s .35s ease-in-out forwards;
}

.actions { display: grid; gap: 22px; margin: 2.2rem 0 0; }

/* Icon + label as a centered group; the shared center axis is what the eye
   reads. Fixed-width icon slots normalize glyph side-bearings so the apparent
   icon-to-label gap matches across buttons; display:block on the SVG kills the
   baseline/descender drift of inline SVGs. */
.icon-slot {
  width: 20px; height: 20px;
  flex: 0 0 20px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.icon-slot svg { width: 100%; height: 100%; display: block; }
/* The G fills its viewBox edge-to-edge while the lucide download icon carries
   ~25% internal padding -- at 100% the G reads oversized, so it gets a smaller
   box (15px + round-glyph overshoot ~= the download icon's ~15px apparent). */
.icon-slot svg.g-logo { width: 16px; height: 16px; }

.note { font-size: .75rem; color: var(--soft); margin-top: 1rem; }
.note + .note { margin-top: .3rem; }

/* Escape hatch, not a third choice: a plain underlined link */
.back { color: var(--soft); font-size: .875rem; text-decoration: underline; }

/* ============ Transition E arrival (from /welcome): the logo FLIPs from its
   recorded welcome position into the header slot; the rest fades in after.
   Only active when sessionStorage carries the handoff -- direct visits render
   normally. ============ */
/* transform-only (compositor thread -- no layout/paint per frame, no first-
   frame jolt while the page is still busy) with a soft-start curve (initial
   slope 0, unlike the old .2,.7 launch at 3.5x). */
#flip-clone {
  position: fixed; z-index: 60; margin: 0;
  will-change: transform;
  transition: transform .45s cubic-bezier(.45, 0, .25, 1);
}
body.arriving .fadein { opacity: 0; transform: translateY(10px); }
body.arriving.arrived .fadein { opacity: 1; transform: none; transition: opacity .3s ease, transform .3s ease; }
/* The logo is NOT a .fadein (the clone flies in its place): hide the real one
   pre-paint during arrival or it pops at its destination before the glide.
   flow.js's inline visibility juggling still applies; .arrived releases it. */
body.arriving:not(.arrived) #brand-logo { visibility: hidden; }

/* ============ Reduced motion: static card at center, no drift, no streaks,
   immediate transition on response (JS skips the enter/exit awaits) ============ */
@media (prefers-reduced-motion: reduce) {
  .card-stage.enter, .card-stage.exit { animation: none; }
  .card-g, .speed-line { animation: none; }
  .speed-line { opacity: 0; }
  .loading-label { transition: none; }
  .wrap { transition: none; }
  .path { animation: none; stroke-dasharray: 0; }  /* solid strokes, no draw */
  .tick { transition: none; }
  .reveal { transition: none; }
  input, select, .btn, .field label, .field .ts-wrapper::after { transition: none; }
  #flip-clone, body.arriving.arrived .fadein { transition: none; }
}
