/* ═══════════════════════════════════════════════════════════════════════════
   AUTH FORMS — states, validation, the moments between click and world.

   Loaded after wizloria.css / login-luxe.css / login-fx.css / create.css and
   BEFORE login-mobile.css on both index.php and create.php.  So: this sheet
   wins over the skins by source order, and the phone sheet still wins over
   this one.  Nothing in here needs !important, and nothing in here may use it.

   THE ONE RULE OF THIS FILE
       Nothing that appears may move anything that was already there.
       Every error line, every hint, every spinner is either absolutely
       positioned or lives in a box whose height was reserved from the start.
       A phone screen that jumps when you get your password wrong is a bug.
                                                                 (2026-08-16)
   ═════════════════════════════════════════════════════════════════════════ */

:root{
  --au-bad  :#f08a7e;              /* lifted off --bad for contrast on navy   */
  --au-bad-d:#96231d;              /* the same voice on parchment             */
  --au-ok   :#8fd082;
  --au-ok-d :#3d6b33;
  --au-dim  :#a9b8cf;
  --au-ring :#f2d896;
  --au-err-h:15px;                 /* one reserved line of inline error text  */
}

/* ══════════════════ 1. FOCUS — visible on a black castle ═════════════════
   A gold ring that survives the dark ground, plus a dark under-ring so it is
   still legible if a field ever sits on parchment.  Keyboard only: pointer
   users already know where they clicked.                                    */

.form :is(input,button,a,select,textarea):focus-visible,
.lux-signin :is(input,button):focus-visible,
.modal :is(input,button,a):focus-visible{
  outline:2px solid var(--au-ring);
  outline-offset:2px;
  border-radius:1px;
}
/* the field is the focus surface — the bare input inside it never draws its
   own ring, the groove around it lights up instead */
.field input:focus-visible{outline:0}
.field:has(input:focus-visible){
  box-shadow:
    inset 0 2px 8px rgba(0,0,0,.6),
    0 0 0 2px rgba(242,216,150,.85),
    0 0 22px rgba(216,160,60,.34);
}
/* ⚠ `:has()` on a bare tag is a document-wide subtree scan; `.field` is a
   class subject, so this stays cheap.  The fallback below covers engines
   without :has() and costs nothing. */
@supports not selector(:has(*)){
  .field:focus-within{box-shadow:
    inset 0 2px 8px rgba(0,0,0,.6),
    0 0 0 2px rgba(242,216,150,.7),
    0 0 22px rgba(216,160,60,.3)}
}

/* the little text links in the meta row are real controls — give them a hover
   surface, not just a colour change */
.lux-signin-meta .link,
.form .link{
  padding:2px 3px;margin:-2px -3px;
  border-radius:2px;
  transition:color .18s ease,background .18s ease,text-shadow .18s ease;
}
.lux-signin-meta .link:hover,
.form .link:hover{background:rgba(226,178,88,.10)}

/* ⚠ the hover surface above sets a negative side margin, which would eat the
   `margin:auto` that centres `.link-c`; and a block <button> is shrink-to-fit,
   so it needs an explicit fit-content width for auto margins to bite at all.
   Without both, "Back to login" sits hard against the owl modal's corner. */
.form .link-c{width:fit-content;margin:14px auto 0}

/* ══════════════════ 2. HOVER / ACTIVE on the fields ══════════════════════ */

.field:hover:not(:focus-within){border-color:rgba(214,170,96,.72)}
.field .f-ic{transition:color .18s ease}
.field:hover .f-ic{color:rgba(238,200,132,.95)}
/* the eye is a button: it needs a target, not just an icon */
.f-eye{border-radius:2px}
.f-eye:hover{background:rgba(226,178,88,.10)}
.f-eye:active{background:rgba(226,178,88,.18)}

/* ══════════════════ 3. THE SUBMIT BUTTON — working / done / dead ═════════
   The face text NEVER changes length while a request is in flight: the label
   fades back, a turning rune fades in over it.  Swapping the words would
   resize the button and shove the row it sits in.                           */

.btn{ --au-spin:rgba(26,17,6,.86); }
.btn .btn-face{transition:opacity .18s ease}

.btn.is-busy,.btn.is-done{pointer-events:none}
.btn.is-busy .btn-face{opacity:.22}
.btn.is-busy::after,          /* login-fx owns a sweep on ::after — hide it   */
.btn.is-done::after{opacity:0}

/* the turning rune, drawn in the middle of the plaque.  Both markers are
   appended once and then live there; only the button's state decides which
   one is on screen, so nothing is ever created mid-transition. */
.btn > .au-spin,
.btn > .au-seal{
  position:absolute;inset:0;z-index:3;
  display:none;place-items:center;
  pointer-events:none;
}
.btn.is-busy > .au-spin,
.btn.is-done > .au-seal{display:grid}
.au-spin i{
  width:15px;height:15px;display:block;border-radius:50%;
  border:2px solid rgba(26,17,6,.22);
  border-top-color:var(--au-spin);
  animation:auSpin .72s linear infinite;
}
.au-seal svg{width:17px;height:17px;color:var(--au-spin);
  stroke:currentColor;stroke-width:2.8;fill:none;
  stroke-linecap:round;stroke-linejoin:round;
  animation:auSeal .34s cubic-bezier(.2,.9,.3,1) both}
@keyframes auSpin{to{transform:rotate(360deg)}}
@keyframes auSeal{from{opacity:0;transform:scale(.55)}to{opacity:1;transform:scale(1)}}

/* the red register plaque and the dark modal buttons want a light rune */
.btn-red{--au-spin:rgba(255,238,208,.92)}
.btn.is-done .btn-face{opacity:.22}

/* a button that cannot be pressed must LOOK like it cannot be pressed */
.btn:disabled,.btn[aria-disabled="true"]{
  cursor:default;
  filter:saturate(.45) brightness(.72);
  box-shadow:0 2px 8px rgba(0,0,0,.45);
}
.btn:disabled:hover,.btn[aria-disabled="true"]:hover{filter:saturate(.45) brightness(.72)}
.btn:disabled:active{transform:none}
/* …except while it is working: that is not "dead", that is "casting" */
.btn.is-busy:disabled,.btn.is-done:disabled{filter:none;cursor:progress}
.btn.is-done:disabled{cursor:default}

.soc:disabled,.link:disabled,.f-eye:disabled{opacity:.45;cursor:default}

/* while a form is in flight the whole thing stops taking input, quietly */
.form.is-locked .field{opacity:.72}
.form.is-locked .field input{cursor:progress}

/* ══════════════════ 4. INLINE ERRORS — beside the field that caused them ══
   `.f-err` is absolutely positioned out of flow, so it can never reflow the
   page.  Where the fields stack (the sign-up card, the owl modal, reset) the
   space under each field is reserved once, up front, so the error drops into
   a gap that was always there.                                              */

.f-err{
  position:absolute;left:2px;right:2px;top:calc(100% + 3px);
  z-index:4;pointer-events:none;
  font-family:var(--ff-body),Georgia,serif;
  font-size:11.5px;line-height:var(--au-err-h);letter-spacing:.01em;
  color:var(--au-bad);
  opacity:0;transform:translateY(-2px);
  transition:opacity .18s ease,transform .18s ease;
  text-wrap:pretty;
}
.f-err::before{content:'▲ ';font-size:8px;vertical-align:1px;opacity:.85}
.field.is-bad  > .f-err,
.field.is-warn > .f-err,
.field.is-ok   > .f-err{opacity:1;transform:none}
.field.is-warn > .f-err{color:#e6c274}
.field.is-warn > .f-err::before{content:'· '}
.field.is-ok   > .f-err{color:var(--au-ok)}
.field.is-ok   > .f-err::before{content:'✓ '}
/* a field marked good with nothing to say keeps its mouth shut */
.field > .f-err:empty{opacity:0}

/* the warn state is a nudge, not a verdict: gold, not red */
.field.is-warn{border-color:rgba(214,170,96,.75)}
.lux-auth-card .field.is-warn{border-color:rgba(140,100,34,.65)}

/* reserve the gap ONCE, on every stacked form, error or not */
.lux-auth-card .form .field,
.modal .form .field,
.au-page .field{margin-bottom:calc(clamp(8px,.8vw,12px) + var(--au-err-h))}

/* THE SIGN-IN PLANK is a single ROW: there is no room under a field, so its
   errors rise into the empty air above the board as a small hung plaque.
   ⚠ it has to out-stack login-fx's glass sheen (a pseudo on .lux-signin-bar)
   and the board itself, or the second line disappears behind the gold band. */
body.luxe .lux-signin .field{z-index:3}
.lux-signin .field > .f-err{
  z-index:9;
  top:auto;bottom:calc(100% + 12px);
  left:50%;right:auto;transform:translate(-50%,-2px);
  width:max-content;max-width:min(370px,44vw);
  padding:5px 10px;text-align:center;
  color:#ffd7cd;
  background:linear-gradient(180deg,rgba(14,10,4,.96),rgba(4,3,1,.94));
  border:1px solid rgba(198,152,72,.5);
  box-shadow:0 8px 22px rgba(0,0,0,.7),inset 0 1px 0 rgba(255,232,182,.10);
}
.lux-signin .field > .f-err::before{content:none}
/* the pin that hangs it off the field */
.lux-signin .field > .f-err::after{
  content:'';position:absolute;left:50%;top:100%;
  width:7px;height:7px;margin:-4px 0 0 -3.5px;transform:rotate(45deg);
  background:rgba(6,4,2,.96);
  border-right:1px solid rgba(198,152,72,.5);
  border-bottom:1px solid rgba(198,152,72,.5);
}
.lux-signin .field.is-bad > .f-err,
.lux-signin .field.is-warn > .f-err{transform:translate(-50%,0)}

/* login-luxe re-declares the plank's borders at a higher specificity than
   wizloria's `.field.is-bad`, so the groove never turned red in there.
   Restate the verdict where it can actually win. */
body.luxe .lux-signin .field.is-bad{
  border-color:rgba(228,116,104,.9);
  border-top-color:rgba(196,86,76,.95);
  border-bottom-color:rgba(240,150,138,.75);
  box-shadow:
    inset 0 3px 7px rgba(0,0,0,.55),
    inset 0 -1px 0 rgba(255,190,178,.16),
    0 0 18px rgba(200,70,60,.28);
}
body.luxe .lux-signin .field.is-ok{border-color:rgba(127,191,114,.6)}
body.luxe .lux-signin .field.is-bad .f-ic{color:#f0a396}

/* parchment card: the same voice, darker ink */
.lux-auth-card .f-err{color:var(--au-bad-d)}
.lux-auth-card .field.is-warn > .f-err{color:#8a6a24}
.lux-auth-card .field.is-ok > .f-err{color:var(--au-ok-d)}

/* ══════════════════ 5. THE SUMMARY LINE — two lines, always reserved ══════
   login-luxe reserved ONE 12px line and clipped the rest with an ellipsis,
   which is how a genuinely useful sign-in error ("…if you have been playing
   as a guest, your wizard has no address yet") arrived on screen as
   "Those credentials were rejected by the war…".  Two lines are reserved now,
   the clip is gone, and the height never changes.                           */

/* ⚠ SPECIFICITY, not source order. login-fx.css owns this slot at
   `body.luxe .lux-signin-msg.form-msg` (0,3,1) and pins it to ONE 14px line —
   which is right for a one-line message and wrong the moment the message is
   worth reading. Two lines are reserved here, at (0,4,1), so the plank is
   exactly as tall with an error as without one. */
body.luxe .lux-signin .lux-signin-msg.form-msg,
body.luxe .lux-signin .lux-signin-msg.form-msg:empty{
  margin-top:3px;
  min-height:28px;line-height:14px;font-size:11.8px;
  white-space:normal;overflow:visible;text-overflow:clip;
  color:var(--au-bad);
  text-shadow:0 1px 6px rgba(2,4,10,.9);
  text-wrap:pretty;
}
body.luxe .lux-signin .lux-signin-msg.form-msg.is-ok{color:var(--au-ok)}

/* stacked forms: reserve two lines there too */
.form .form-msg,
.form .form-msg:empty{min-height:2.6em;margin:9px 0 0;text-wrap:pretty}
.form .form-msg{color:var(--au-bad)}
.form .form-msg.is-ok{color:var(--au-ok)}
.lux-auth-card .form .form-msg{color:var(--au-bad-d)}
.lux-auth-card .form .form-msg.is-ok{color:var(--au-ok-d)}

/* an error arriving is a fade, never a slide and never a height change */
.form-msg{animation:none}
.form-msg:not(:empty){animation:auFade .22s ease both}
@keyframes auFade{from{opacity:0}to{opacity:1}}

/* ══════════════════ 6. GENTLE NATIVE VALIDATION ══════════════════════════
   `:user-invalid`, not `:invalid` — the browser must not scold a field the
   player has not finished with yet.  Everything here is a hairline; the real
   message is ours.                                                          */

.field:has(input:user-invalid):not(:focus-within){border-color:rgba(224,104,95,.55)}
.field:has(input:user-valid) .f-ic{color:rgba(214,170,96,.95)}

/* ══════════════════ 7. SMALL PRINT that earns its keep ═══════════════════ */

/* a note the form injects (guest hint in the owl modal, caps-lock warning) */
.f-note{
  margin:-2px 0 10px;
  font-size:12px;line-height:1.45;color:var(--au-dim);
  text-wrap:pretty;
}
.lux-auth-card .f-note,.modal .f-note{color:var(--parch-dim,#6b5535)}
.modal .f-note{color:#9db0cb}
.f-note b{color:var(--g-1);font-weight:600}

/* CAPS LOCK — a floating tag on the password field, out of flow */
.f-caps{
  position:absolute;right:6px;bottom:calc(100% + 6px);z-index:5;
  padding:2px 7px;
  font-family:var(--ff-head),Georgia,serif;
  font-size:9.5px;letter-spacing:.16em;
  color:#1a1206;background:linear-gradient(180deg,#f2d896,#c9962f);
  box-shadow:0 3px 10px rgba(0,0,0,.55);
  opacity:0;transform:translateY(3px);pointer-events:none;
  transition:opacity .16s ease,transform .16s ease;
}
.field.is-caps > .f-caps{opacity:1;transform:none}

/* the password reveal reads its own state */
.f-eye[aria-pressed="true"]{color:var(--g-1)}

/* ══════════════════ 8. THE SUCCESS BEAT ══════════════════════════════════
   Between "it worked" and the world loading there is about half a second of
   nothing.  Fill it: the board takes a breath of gold.                      */

.lux-signin.is-open{animation:auOpen .62s ease both}
@keyframes auOpen{
  0%  {filter:none}
  45% {filter:brightness(1.14) drop-shadow(0 0 26px rgba(238,196,110,.5))}
  100%{filter:none}
}
.lux-auth-card.is-open{animation:auOpen .62s ease both}

/* ══════════════════ 9. AUTOFILL — the yellow slab must not win ═══════════ */

.field input:-webkit-autofill,
.field input:-webkit-autofill:hover,
.field input:-webkit-autofill:focus{
  transition:background-color 100000s ease 0s;
}

/* ══════════════════ 10. reset.php shares the vocabulary ══════════════════ */

.au-page .field{position:relative}
.au-page .f-err{font-size:12px}

/* ══════════════════ 11. RESPECT THE SETTING ═════════════════════════════ */

@media (prefers-reduced-motion:reduce){
  .au-spin i{animation:auSpin 1.6s linear infinite}
  .au-seal svg,.form-msg:not(:empty){animation:none}
  .lux-signin.is-open,.lux-auth-card.is-open{animation:none;filter:none}
  .f-err,.f-caps{transition:none}
}

/* a phone must not paint an error under the plank and then have the plank
   move; the bar's messages stay put and the fields never grow */
/* the same test auth.js makes (COARSE), so the hint is never printed twice */
@media (max-width:860px), (pointer:coarse){
  /* On a phone the plank is a COLUMN, so a hanging plaque would have to push
     the field under it out of the way — a reflow, on the one screen where a
     reflow is the reported bug.  The bubbles go away and auth.js folds the
     hint into the summary line, which reserves three lines up front and so
     is exactly as tall with an error as without one. */
  .lux-signin .field > .f-err{display:none}
  body.luxe .lux-signin .lux-signin-msg.form-msg,
  body.luxe .lux-signin .lux-signin-msg.form-msg:empty{
    min-height:42px;line-height:14px;font-size:11.5px}
}
