/* ============================================================
   OBI AUTH. The sign-in's room: the splash, the sign-in card (sign in, the 6-digit code, the notices, and the cards still to
   come: set your password, set up your authenticator app, check your email, an expired link), and the arrival in the app.

   One thread through both screens (William, 2026-10-14, round 5: "start over on the sign in page ... similar-but-on-par
   design"): a learner's growth through the seven CAPS levels, told in light, in Obi's own colours.
     the splash   the ball's seven landings each light the next CAPS level: a ripple of its colour where it lands and a wash of
                  its light on the stage, in an arc over the logo from 1 (clay) to 7 (green, as it lands in the full stop);
                  the bar fills through the CAPS ramp; the logo blooms.
     the sign-in  the same seven lights stay on, settled, behind two panes of glass: a product panel on the left (the front
                  page's closing line over an example on the website and an example on a phone, which animate in once) and
                  the card on the right. The panes are invisible glass: the pointer lights the pane it is over, and nothing
                  else. While signing in, a wave passes along the lights; when it fails, they flicker and dip, the card nudges,
                  and the line under the title becomes the warning in the same place, so the card never changes size.
   Obi's fields and button stay its own (cream Input, apricot Button).

   Markup and behaviour stay in ovation-auth.js (server mode: card(), showLogin, showCode, showNotice) and the splash in
   ovation-loginfx.js. This sheet styles what they draw; obi-auth.js builds the room (.oa-house, .oa-show) and adds the light,
   the busy and the error states. Every selector starts at #authOverlay (or html.obi-arrive): the modules inject their <style>
   after this sheet, so an id is what lets these rules win, and nothing here reaches the signed-in app.

   Movement: springs are Motion's own (spring() from the vendored motion 14.2.0, sampled into CSS linear() curves; nothing is
   loaded at run time). Every part's default is its final state; movement is only added on top, inside prefers-reduced-
   motion: no-preference, so reduced motion or the app's own Motion setting off (ovation-theme.js turns every animation off)
   leaves a complete, still room.
   ============================================================ */

:root {   /* on the root, not the overlay: the arrival (below) runs after the overlay has gone */
  /* spring(0.45, 0.12): a settle with a whisper of overshoot, for rows arriving. 550 ms. */
  --obi-ease-settle: linear(0, 0.0568, 0.1827, 0.3313, 0.4764, 0.6048, 0.7115, 0.7961, 0.8605, 0.908, 0.942, 0.9654, 0.981, 0.991, 0.9971, 1.0005, 1.0022, 1);
  /* spring(0.4, 0.45): a bouncy pop, for the warning, the tile, the chips. 850 ms. */
  --obi-ease-pop: linear(0, 0.0724, 0.2433, 0.4538, 0.6611, 0.8385, 0.9729, 1.062, 1.1103, 1.1261, 1.1192, 1.0988, 1.0725, 1.046, 1.023, 1.0053, 0.9933, 0.9866, 0.9841, 0.9847, 0.9871, 0.9904, 0.9938, 0.9968, 0.9991, 1.0007, 1.0016, 1);
  /* spring(0.5, 0.18): the card and the panel's scenes rising, the level bars growing. 900 ms. */
  --oa-spring-rise: linear(0, 0.0442, 0.1477, 0.2776, 0.4125, 0.5393, 0.6511, 0.745, 0.8209, 0.8799, 0.9243, 0.9565, 0.9789, 0.9938, 1.003, 1.0082, 1.0106, 1.0111, 1.0105, 1.0093, 1.0079, 1.0064, 1.005, 1.0037, 1.0027, 1.0019, 1.0013, 1.0008, 1, 1);
  --oa-house: #241E1A;                                   /* the dark: Obi's Ink, as the front page's stage */
  --oa-line: rgba(255, 253, 248, .16);                   /* a hairline on the dark (the front page's --st-line) */
  --oa-muted: #CFC6BA;                                   /* quiet text on the dark (the front page's --st-muted) */
  --oa-dim: #5B514A;                                     /* words before the light reaches them (the front page's --st-dim) */
  --oa-glass: rgba(30, 25, 22, .62);                     /* the glass: Ink's own warm dark at 62% (the front page's bar) */
  --oa-elev: 0 2px 4px rgba(20, 14, 10, .45), 0 18px 40px rgba(20, 14, 10, .42);
  --oa-wash: .3;                                         /* how strong each level's light is once settled */
}

/* ============================================================
   1. The room: the overlay goes dark; the seven CAPS lights hang in an arc, settled; a fine grain over them (not in front
   mode: the front page hides every other child of the overlay; not while the splash plays, which has its own lights)
   ============================================================ */
#authOverlay:not(.ov-front) { background: var(--oa-house); color-scheme: dark; }
#authOverlay .oa-house { position: fixed; inset: 0; z-index: 0; pointer-events: none; overflow: hidden;
  background: radial-gradient(120% 90% at 50% 50%, transparent 55%, rgba(0, 0, 0, .34)); }   /* the corners fall away */
/* a level's light: a bright bead of its colour (::before) and the wash it throws on the dark (::after), mixed like light */
#authOverlay :is(.oa-wash, .ovs-wash) { position: absolute; left: var(--x); top: var(--y); width: min(52vmin, 560px); aspect-ratio: 1; translate: -50% -50%;
  pointer-events: none; mix-blend-mode: screen; }
#authOverlay :is(.oa-wash, .ovs-wash)::after { content: ""; position: absolute; inset: 0; border-radius: 50%; opacity: var(--oa-wash);
  background: radial-gradient(closest-side, color-mix(in srgb, var(--c) 70%, #FFFFFF) 0, var(--c) 14%, color-mix(in srgb, var(--c) 32%, transparent) 46%, transparent); }
#authOverlay :is(.oa-wash, .ovs-wash)::before { content: ""; position: absolute; left: 50%; top: 50%; width: 9px; height: 9px; translate: -50% -50%; border-radius: 50%;
  background: color-mix(in srgb, var(--c) 45%, #FFFFFF); box-shadow: 0 0 10px 3px color-mix(in srgb, var(--c) 80%, transparent), 0 0 34px 10px color-mix(in srgb, var(--c) 30%, transparent); opacity: .85; }
/* the grain: a little film over the light, so the dark reads as a surface, not a void */
#authOverlay .oa-grain { position: absolute; inset: 0; opacity: .07; mix-blend-mode: overlay;
  background: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='160' height='160'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 1  0 0 0 0 1  0 0 0 0 1  0 0 0 .55 0'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>"); }
#authOverlay.ov-splashing :is(.oa-house, .oa-show) { visibility: hidden; }   /* the splash has its own lights */

/* the old carousel goes (the panel below tells two of Obi's best parts instead; loginfx still builds it, inert and hidden) */
#authOverlay#authOverlay .ov-showcase { display: none !important; }
#authOverlay#authOverlay.ov-revealed > .auth-card { margin-left: auto; margin-right: auto; }
/* wide windows: the panel on the left, the card on the right, as the sign-in was laid out first */
#authOverlay .oa-show { display: none; }
@media (min-width: 1040px) and (min-height: 600px) {
  #authOverlay.ov-revealed:not(.ov-front) > .oa-show { display: flex; }
  #authOverlay#authOverlay.ov-revealed > .oa-show + .auth-card { margin-left: 0; }
  #authOverlay#authOverlay.ov-revealed > .oa-show { margin: auto 0 auto auto; }
}

/* ============================================================
   2. The card: a pane of dark glass. One column, one left edge, cream words; Obi's own fields and button. The line under
   the title is a fixed slot, shared by the subtitle and the warning (#loginErr): when there is a warning, the slot becomes
   it, in the same place and at the same size, so the card never grows, and nothing hangs off it.
   ============================================================ */
#authOverlay .auth-card {
  --oa-slot: 3.5rem;                                      /* the slot: a two-line warning, with its padding */
  position: relative; z-index: 1; isolation: isolate;
  display: grid; grid-template-columns: minmax(0, 1fr); align-content: start;
  text-align: left; max-width: 420px;
  color: var(--cream);
  background: var(--oa-glass);
  -webkit-backdrop-filter: blur(14px) saturate(1.2); backdrop-filter: blur(14px) saturate(1.2);
  border: 1px solid var(--oa-line); border-radius: var(--radius-pill);
  box-shadow: var(--oa-elev);
}
#authOverlay .auth-card > * { grid-column: 1; min-width: 0; }
#authOverlay .auth-card > #authTitle { text-wrap: balance; }
#authOverlay .auth-card:has(> #loginErr) { grid-template-rows: auto auto minmax(var(--oa-slot), auto); } /* the slot, or the subtitle's own height when it is longer */
/* a setup step (brief c): the steps sit between the logo and the title, so the slot is the fourth row */
#authOverlay .auth-card:has(> .obi-steps):has(> #loginErr) { grid-template-rows: auto auto auto minmax(var(--oa-slot), auto); }
#authOverlay .auth-card:has(> .obi-steps):has(> #loginErr) > .sub, #authOverlay .auth-card:has(> .obi-steps) > #loginErr { grid-row: 4; }
#authOverlay .auth-card:has(> #loginErr) > .sub, #authOverlay .auth-card > #loginErr { grid-row: 3; }
#authOverlay .auth-card:has(> #loginErr) > .sub { align-self: center; margin: 0; }
#authOverlay .auth-card:has(> #loginErr) > .sub + * { margin-top: var(--space-4); } /* air between the slot and the first field, the same with or without a warning */
/* the glass, lit only where the pointer is on it (::before a glow from the pointer, ::after the edge catching it) */
/* the light lives in a leaf span obi-auth.js puts in each pane (.oa-glass): the pointer's position is written to it alone */
#authOverlay :is(.auth-card, .oa-show) > .oa-glass { position: absolute; inset: -1px; z-index: -1; pointer-events: none; border-radius: inherit;
  grid-column: auto; grid-row: auto; margin: 0; animation: none !important; }
#authOverlay .oa-glass::before, #authOverlay .oa-glass::after { content: ""; position: absolute; inset: 0; z-index: -1;
  pointer-events: none; border-radius: inherit; opacity: 0; transition: opacity .25s var(--ease-out); }
#authOverlay .oa-glass::before { background: radial-gradient(9rem circle at var(--gx, 50%) var(--gy, 50%), rgba(255, 220, 180, .13), rgba(232, 133, 58, .05) 50%, transparent 74%); }
#authOverlay .oa-glass::after { border: 1px solid rgba(255, 236, 214, .55);
  -webkit-mask-image: radial-gradient(8rem circle at var(--gx, 50%) var(--gy, 50%), #000, transparent 78%);
  mask-image: radial-gradient(8rem circle at var(--gx, 50%) var(--gy, 50%), #000, transparent 78%); }
#authOverlay :is(.auth-card, .oa-show).is-hot > .oa-glass::before, #authOverlay :is(.auth-card, .oa-show).is-hot > .oa-glass::after { opacity: 1; }

#authOverlay .auth-card > .brand { align-self: flex-start; justify-self: start; align-items: flex-start; margin: 0 0 var(--space-5); }
#authOverlay .auth-card > .brand .word { position: relative; color: var(--cream);
  font-size: min(40px/*the card's wordmark: one step above the 24px title*/, 14vw); line-height: 1; }
#authOverlay .auth-card #authTitle { text-align: left; color: var(--cream); }
#authOverlay .auth-card .sub { text-align: left; margin: 0 0 var(--space-5); color: var(--oa-muted); max-width: 34em; }
#authOverlay .auth-card label.fld { width: 100%; margin-bottom: var(--space-4); color: var(--oa-muted); }
#authOverlay .auth-card label.fld > input { width: 100%; min-height: 44px; }
#authOverlay .auth-card > .btn { min-height: 44px; }
#authOverlay .auth-card > .btn + .btn { justify-self: center; } /* the code card's "Sign out" under Continue */
#authOverlay .auth-card .btn.ghost { color: var(--cream); background: transparent; border-color: var(--oa-line); }
#authOverlay .auth-card .btn.ghost:hover { background: rgba(255, 253, 248, .08); }
#authOverlay .auth-card #obiAbout { justify-self: center; color: var(--oa-muted); }
#authOverlay .auth-card #obiAbout:hover { color: var(--cream); }

/* ---- the logo at the top of the card: the horizontal artwork, inline (obi-auth.js), its letters cream (the reversed artwork). The
   typed "Obi." stays for assistive technology, visually hidden. ---- */
#authOverlay .auth-card > .brand.oa-branded { display: block; }
#authOverlay .auth-card > .brand.oa-branded .oa-logo { display: block; height: min(34px/*the lockup: tile and letters at the card's scale*/, 9vw); width: auto; overflow: visible; }
#authOverlay .auth-card > .brand.oa-branded .oa-lword { fill: var(--cream); }
#authOverlay .auth-card > .brand.oa-branded .oa-stop { transform-box: fill-box; transform-origin: 50% 100%; }
#authOverlay .auth-card > .brand.oa-branded .word { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0; }

/* ---- the ball: the full stop of "Obi." ---- */
#authOverlay .auth-card > .brand .dot { display: inline-block; transform-origin: 50% 88%; } /* the glyph's foot: it squashes onto the baseline */
#authOverlay .auth-card > .brand .word::after { content: ""; position: absolute; right: 0.02em; bottom: -0.06em; width: 0.26em; height: 0.07em;
  border-radius: 50%; background: #000; opacity: 0; pointer-events: none; }

/* ---- the warning (#loginErr, role="alert") in the slot: Obi's concern red with white words (4.8:1), a white dot centred on
   them; empty, it is nothing and the subtitle shows. Any other error line (another card's) stays a band in the flow. ---- */
#authOverlay .auth-card .auth-err { min-height: 0; margin: 0; font-size: var(--text-caption); line-height: var(--lh-label, 1.4); color: var(--clay-ink); }
#authOverlay .auth-card .auth-err:not(:empty) { display: flex; align-items: center; gap: var(--space-2); margin: 0 0 var(--space-4);
  padding: var(--space-2) var(--space-3); background: var(--clay-tint); border-radius: var(--radius-control); }
#authOverlay .auth-card > #loginErr { align-self: start; min-height: var(--oa-slot); margin: 0; }
/* a card whose warning comes straight after the line under the title (Welcome): the air goes to what follows the slot, not the warning */
#authOverlay .auth-card:has(> #loginErr) > .sub + #loginErr { margin-top: 0; }
#authOverlay .auth-card:has(> #loginErr) > .sub + #loginErr + * { margin-top: var(--space-4); } /* the slot's height; under a long subtitle, at its top */
#authOverlay .auth-card > #loginErr:empty { visibility: hidden; }
#authOverlay .auth-card > #loginErr:not(:empty) { margin: 0; gap: var(--space-3); padding: var(--space-2) var(--space-4); overflow: hidden;
  background: var(--concern); color: #FFFFFF; font-size: var(--text-caption); font-weight: var(--fw-semibold); line-height: 1.3; text-wrap: balance;
  border-radius: var(--radius-control); }
#authOverlay .auth-card .auth-err:not(:empty)::before { content: ""; flex: none; width: 8px; height: 8px; border-radius: var(--radius-full); background: var(--clay); }
#authOverlay .auth-card > #loginErr:not(:empty)::before { background: #FFFFFF; box-shadow: 0 0 0 3px rgba(255, 255, 255, .28); } /* a drawn mark: Obi has no unicode-as-icon */
#authOverlay .auth-card:has(> #loginErr:not(:empty)) > .sub { visibility: hidden; }

/* ---- busy: after Sign in or Continue, until the answer (data-obi-busy, set by obi-auth.js) ---- */
#authOverlay .auth-card[data-obi-busy] > .btn:not(.ghost) { cursor: progress; background: #DA762C/*keep: the app's own .btn:hover in ovation.css (Obi has no hover token)*/; }
#authOverlay .auth-card[data-obi-busy] label.fld > input { color: var(--muted); }

/* ---- the 6-digit code ---- */
#authOverlay .auth-card #loginCode { font-size: var(--text-h3); font-weight: var(--fw-semibold); font-variant-numeric: tabular-nums;
  letter-spacing: 0.35em; padding-left: calc(var(--space-3) + 0.35em); /* letter-spacing trails the last digit: even it out */
  text-align: center; min-height: 56px; }
#authOverlay .auth-card #loginCode::placeholder { color: var(--line-strong); }

/* ---- notices (a card with no field): the reading text gets the room ---- */
#authOverlay .auth-card:not(:has(input)) .sub { font-size: var(--text-body); line-height: var(--lh-body, 1.5); color: var(--cream); margin-top: var(--space-2); }

/* ---- shared patterns for the cards still to come (names for the DB agent; see handoff/VISUAL-AGENT-REPLY) ----
   .obi-steps     an ordered list of steps (<ol class="obi-steps"><li class="is-done|is-now">…</li></ol>)
   .obi-qr        the frame around the authenticator QR code (<div class="obi-qr"><img|svg …></div>)
   .obi-key       the setup key printed under the QR code, to type by hand (<code class="obi-key">…</code>)
   .obi-note      a quiet line under the button (hints, "Didn't get it?") */
#authOverlay .obi-steps { list-style: none; counter-reset: obi-step; display: flex; gap: var(--space-2); margin: 0 0 var(--space-5); padding: 0; }
#authOverlay .obi-steps > li { counter-increment: obi-step; flex: 1 1 0; min-width: max-content; font-size: var(--text-caption); line-height: 1.35; color: var(--oa-muted);
  padding-top: var(--space-2); border-top: 3px solid var(--oa-line); white-space: nowrap; overflow-wrap: normal; } /* a name stays whole: the number has its own line, so even "Authenticator" fits at 320 */
#authOverlay .obi-steps > li::before { content: counter(obi-step); display: block; font-weight: var(--fw-semibold); font-variant-numeric: tabular-nums; }
#authOverlay .obi-steps > li.is-done { border-top-color: var(--sage); }
#authOverlay .obi-steps > li.is-now { border-top-color: var(--apricot); color: var(--cream); font-weight: var(--fw-semibold); }
#authOverlay .obi-qr { justify-self: center; display: grid; place-items: center; width: min(200px, 100%); aspect-ratio: 1; margin: 0 0 var(--space-4); padding: var(--space-3);
  background: #FFFFFF/*a QR code reads best on pure white*/; border-radius: var(--radius-card); }
#authOverlay .obi-qr > :is(img, svg, canvas) { display: block; width: 100%; height: auto; }
#authOverlay .obi-key { display: block; margin: 0 0 var(--space-4); padding: var(--space-2) var(--space-3);
  font-family: ui-monospace, "Cascadia Mono", Consolas, monospace/*Obi has no mono face: the system's, so 0/O and 1/l differ*/; font-size: var(--text-caption); letter-spacing: 0.08em;
  overflow-wrap: anywhere; text-align: center; background: rgba(255, 253, 248, .08); border: 1px solid var(--oa-line); border-radius: var(--radius-control); color: var(--cream); }
#authOverlay .obi-note { margin: var(--space-3) 0 0; font-size: var(--text-caption); color: var(--oa-muted); text-align: center; }
#authOverlay .auth-card .obi-note a { color: var(--oa-muted); text-decoration-color: rgba(207, 198, 186, .5); text-underline-offset: 2px; } /* "Forgot your password?": the app's clay link colour is 2.7:1 on the dark card */
#authOverlay .auth-card .obi-note a:hover { color: var(--cream); text-decoration-color: currentColor; }

/* ---- browser mode only: the demo accounts under the card, on the dark ---- */
#authOverlay .auth-card .auth-roles { border-top-color: var(--oa-line); }
#authOverlay .auth-card .auth-roles .hint { color: var(--oa-muted); }
#authOverlay .auth-card .role-chip { color: var(--cream); border-color: var(--oa-line); background: rgba(255, 253, 248, .06); }
#authOverlay .auth-card .role-chip:hover { background: rgba(255, 253, 248, .12); color: var(--cream); }

/* ---- Sign in with Google or Microsoft (brief b; ovation-auth.js draws .auth-or, .auth-sso, button.sso, .sso-mark[data-brand]) ----
   A small "or" between two hairlines, then two quiet glass buttons: each company's own mark, never redrawn (Google: the "G"
   of its published sign-in button artwork; Microsoft: the four squares of its sign-in symbol), and its approved label, kept as
   ovation-auth.js writes it. Dark, neutral buttons with the full-colour mark are both companies' dark-theme pattern. */
#authOverlay .auth-or { display: flex; align-items: center; gap: var(--space-3); margin: var(--space-5) 0 var(--space-4);
  font-size: var(--text-caption); color: var(--oa-muted); }
#authOverlay .auth-or::before, #authOverlay .auth-or::after { content: ""; flex: 1 1 0; height: 1px; background: var(--oa-line); }
#authOverlay .auth-sso { display: grid; gap: var(--space-2); }
#authOverlay .auth-sso > button.sso { margin-top: 0 !important/*the inline margin-top: one gap for both, from the grid*/; display: inline-flex; align-items: center; gap: var(--space-3);
  min-height: 44px; padding: var(--space-2) var(--space-4); border-radius: var(--radius-control);
  font-weight: var(--fw-semibold); color: var(--cream); background: rgba(255, 253, 248, .06); border: 1px solid var(--oa-line); }
#authOverlay .auth-sso > button.sso:hover { background: rgba(255, 253, 248, .12); color: var(--cream); border-color: rgba(255, 253, 248, .32); }
#authOverlay .auth-sso > button.sso:active { background: rgba(255, 253, 248, .16); }
@media (max-width: 360px) { #authOverlay .auth-sso > button.sso { font-size: var(--text-label); gap: var(--space-2); padding-inline: var(--space-3); } } /* "Sign in with Microsoft" stays on one line at 320 */
#authOverlay .sso-mark { flex: none; width: 18px; height: 18px; background: center / contain no-repeat; }
#authOverlay .sso-mark[data-brand="google"] { background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2048%2048%22%3E%3Cpath%20fill%3D%22%23EA4335%22%20d%3D%22M24%209.5c3.54%200%206.71%201.22%209.21%203.6l6.85-6.85C35.9%202.38%2030.47%200%2024%200%2014.62%200%206.51%205.38%202.56%2013.22l7.98%206.19C12.43%2013.72%2017.74%209.5%2024%209.5z%22%2F%3E%3Cpath%20fill%3D%22%234285F4%22%20d%3D%22M46.98%2024.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58%202.96-2.26%205.48-4.78%207.18l7.73%206c4.51-4.18%207.09-10.36%207.09-17.65z%22%2F%3E%3Cpath%20fill%3D%22%23FBBC05%22%20d%3D%22M10.53%2028.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92%2016.46%200%2020.12%200%2024c0%203.88.92%207.54%202.56%2010.78l7.97-6.19z%22%2F%3E%3Cpath%20fill%3D%22%2334A853%22%20d%3D%22M24%2048c6.48%200%2011.93-2.13%2015.89-5.81l-7.73-6c-2.15%201.45-4.92%202.3-8.16%202.3-6.26%200-11.57-4.22-13.47-9.91l-7.98%206.19C6.51%2042.62%2014.62%2048%2024%2048z%22%2F%3E%3C%2Fsvg%3E"); }
#authOverlay .sso-mark[data-brand="microsoft"] { background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2021%2021%22%3E%3Crect%20x%3D%221%22%20y%3D%221%22%20width%3D%229%22%20height%3D%229%22%20fill%3D%22%23f25022%22%2F%3E%3Crect%20x%3D%221%22%20y%3D%2211%22%20width%3D%229%22%20height%3D%229%22%20fill%3D%22%2300a4ef%22%2F%3E%3Crect%20x%3D%2211%22%20y%3D%221%22%20width%3D%229%22%20height%3D%229%22%20fill%3D%22%237fba00%22%2F%3E%3Crect%20x%3D%2211%22%20y%3D%2211%22%20width%3D%229%22%20height%3D%229%22%20fill%3D%22%23ffb900%22%2F%3E%3C%2Fsvg%3E"); }

/* ---- keyboard focus: Obi's sky ring on the buttons and the link (the fields keep Obi Input's own sky border and ring) ---- */
#authOverlay .auth-card :is(.btn, a):focus-visible { outline: 2px solid var(--sky); outline-offset: 2px; }

/* ============================================================
   3. The panel: a pane of glass on the left (wide windows). The front page's closing line, then two examples composed as one
   picture: Sipho's record on the website, and in front of it a phone with Lerato's note. Every word is the front page's
   (plans/FRONT-PAGE.md's copy deck); each scene is marked "Example". The scenes are Obi's own components, cream on the dark.
   ============================================================ */
#authOverlay .oa-show { position: relative; z-index: 1; isolation: isolate; flex: 0 1 600px; min-width: 0; flex-direction: column; gap: var(--space-6);
  padding: var(--space-6) var(--space-6) var(--space-7); border-radius: 28px/*the panel: a large pane, rounder than a card*/; }
#authOverlay .oa-show-h { margin: 0; max-width: 15em; color: var(--cream); text-shadow: 0 0 22px rgba(20, 14, 10, .85), 0 1px 6px rgba(20, 14, 10, .7); /* a halo of the dark: the lights behind never thin the words */ font-size: clamp(1.75rem, 1rem + 1.5vw, 2.5rem); line-height: 1.08; letter-spacing: -.03em; font-weight: var(--fw-bold); }
/* the full stops are apricot (the front page's signature), typed, so the sentence reads whole to assistive technology */
#authOverlay .oa-hstop { color: var(--apricot); }
#authOverlay .oa-stage { position: relative; height: 22.5rem; }
@media (max-width: 1239px) { #authOverlay .oa-stage { height: 26rem; } } /* a narrower panel wraps the record taller: room so it never meets the phone's caption */
#authOverlay .oa-device { position: absolute; margin: 0; display: flex; flex-direction: column; gap: var(--space-2); }
#authOverlay .oa-cue { font-family: Georgia, "Times New Roman", serif; font-style: italic; font-size: .875rem; color: var(--oa-muted); text-shadow: 0 0 12px rgba(20, 14, 10, .9); }
#authOverlay .oa-screen { position: relative; display: flex; flex-direction: column; gap: var(--space-2); color: var(--ink); background: var(--cream); font-size: var(--text-caption);
  box-shadow: 0 1px 0 rgba(255, 255, 255, .5) inset, 0 24px 48px rgba(10, 6, 4, .5), 0 4px 10px rgba(10, 6, 4, .35); }
#authOverlay .oa-ex { position: absolute; top: calc(-1 * var(--space-3)); right: var(--space-3); padding: 2px var(--space-2); border-radius: var(--radius-full);
  background: var(--sand); color: var(--ink-70); font-size: var(--text-caption); font-weight: var(--fw-semibold); box-shadow: var(--elev-1); }
/* the website: a browser window with Sipho's record */
#authOverlay .oa-web { left: 0; top: 0; width: 63%; }
#authOverlay .oa-web .oa-screen { padding: 0 var(--space-5) var(--space-5); border-radius: var(--radius-card); overflow: visible; }
#authOverlay .oa-bar3 { display: flex; gap: 6px; margin: 0 calc(-1 * var(--space-5)) var(--space-2); padding: var(--space-3) var(--space-4); border-bottom: 1px solid var(--line);
  background: var(--bone); border-radius: var(--radius-card) var(--radius-card) 0 0; }
#authOverlay .oa-bar3 > i { width: 9px; height: 9px; border-radius: 50%; background: var(--line-strong); }
#authOverlay .oa-who { display: flex; align-items: center; gap: var(--space-3); }
#authOverlay .oa-who > span { display: flex; flex-direction: column; }
#authOverlay .oa-who b { font-size: var(--text-label); } #authOverlay .oa-who small { color: var(--ink-70); }
#authOverlay .oa-av { display: grid; place-items: center; width: 36px; height: 36px; border-radius: 50%; background: var(--sage-tint); color: var(--sage-ink); font-size: var(--text-caption); }
#authOverlay .oa-sec { margin-top: var(--space-2); font-size: var(--text-caption); font-weight: var(--fw-semibold); letter-spacing: .04em; color: var(--ink-70); }
#authOverlay .oa-lv { display: grid; grid-template-columns: 1fr auto; align-items: baseline; gap: 4px var(--space-3); }
#authOverlay .oa-lv > b { font-weight: var(--fw-medium); color: var(--ink-70); }
#authOverlay .oa-lv > em { font-style: normal; font-weight: var(--fw-semibold); color: var(--ink); }
#authOverlay .oa-lv > i { grid-column: 1 / -1; position: relative; height: 6px; border-radius: var(--radius-full); background: var(--sand); overflow: hidden; }
#authOverlay .oa-lv > i::before { content: ""; position: absolute; inset: 0; width: calc(var(--lv) / 7 * 100%); border-radius: inherit; background: var(--c); transform-origin: 0 50%; }
#authOverlay .oa-obs { display: flex; flex-direction: column; gap: 2px; margin-top: var(--space-2); padding: var(--space-2) var(--space-3); border-radius: var(--radius-control); background: var(--sand); color: var(--ink); }
#authOverlay .oa-obs small { font-size: var(--text-caption); font-weight: var(--fw-semibold); color: var(--ink-70); }
/* the phone: in front, lower right, Lerato's note */
#authOverlay .oa-phone { right: 0; bottom: 0; width: 38%; max-width: 230px; z-index: 1; } /* narrow enough to leave the record's levels in view */
#authOverlay .oa-phone .oa-cue { order: 2; text-align: right; text-wrap: balance; }
@media (min-width: 1240px) { #authOverlay .oa-phone .oa-cue { white-space: nowrap; } }
#authOverlay .oa-phone .oa-ex { right: calc(var(--space-3) - 6px); top: calc(-1 * var(--space-3) - 6px); left: auto; } /* the web card's corner, measured from the rim's outer edge */
#authOverlay .oa-phone .oa-screen { padding: var(--space-5) var(--space-3) var(--space-5); border-radius: 26px; border: 6px solid #191310/*the phone's dark rim*/; }
#authOverlay .oa-notch { position: absolute; left: 50%; bottom: 7px; width: 42px; height: 4px; opacity: .55; translate: -50% 0; border-radius: var(--radius-full); background: #191310; } /* a home bar at the foot, so the top corner is free for the Example tag */
#authOverlay .oa-app { font-size: var(--text-label); } #authOverlay .oa-dot { color: var(--apricot); }
#authOverlay .oa-jot { display: block; padding: var(--space-2); border: 1px solid var(--warm-grey); border-radius: var(--radius-control); background: var(--cream); line-height: 1.35; }
#authOverlay .oa-chips { display: flex; flex-wrap: wrap; gap: var(--space-1); }
#authOverlay .oa-chips > span { padding: 2px var(--space-2); border-radius: var(--radius-full); background: var(--sand); color: var(--ink-70); }
#authOverlay .oa-chips > span + span { background: var(--sky-tint); color: var(--sky-ink); }

/* ============================================================
   4. The splash (#ovSplash, built by ovation-loginfx.js; behaviour: tools/SPLASH-CONTRACT.md). Positions come from the logo's
   own geometry: its viewBox is 2673.36 x 750.98 units, the full stop a circle of r 95 at (2578.36, -95), the baseline at
   y 0; --u is one unit on screen. By default (no movement) the logo is whole and lit, the seven lights on, the ball hidden,
   the bar steps.
   ============================================================ */
#authOverlay #ovSplash {
  --w: min(440px/*the logo's width: the splash's one big thing*/, 78vw, 120vh);
  --u: calc(var(--w) / 2673.36);
  --d: calc(190 * var(--u));                  /* the full stop's diameter */
  --gap: calc(var(--w) * 0.16);               /* logo baseline to bar: room for the hops to stay clear of the logo */
  --leap: calc(-11.72 * var(--u) - var(--gap)); /* from resting on the bar to sitting in the full stop's place */
  --oa-letters: var(--cream);
  gap: 0; background: transparent;            /* the dark overlay is the splash's background */
}
#authOverlay #ovSplash .ovs-stage { position: relative; width: var(--w); }
#authOverlay #ovSplash .ovs-logo { position: relative; z-index: 1; display: block; width: 100%; height: auto; overflow: visible; } /* the ball passes behind it */
#authOverlay #ovSplash .ovs-word { fill: var(--oa-letters); } /* the reversed artwork (brand/obi-logo/svg/obi-logo-horizontal-reversed.svg): cream letters */
#authOverlay #ovSplash #ovSplashBar { position: relative; z-index: 1; width: 100%; max-width: none; margin-top: var(--gap); }
@media (forced-colors: none) {
  #authOverlay #ovSplash #ovSplashBar { background: rgba(255, 253, 248, .12); }   /* the stage's edge, unlit */
  /* the fill runs through the CAPS ramp as the ball climbs (the gradient is the bar's whole width; the fill uncovers it) */
  #authOverlay #ovSplash #ovSplashBar > span { background: linear-gradient(90deg, var(--caps-1), var(--caps-2), var(--caps-3), var(--caps-4), var(--caps-5), var(--caps-6), var(--caps-7)) 0 0 / var(--w) 100% no-repeat;
    filter: drop-shadow(0 0 6px rgba(255, 220, 180, .45)); }
}
#authOverlay #ovSplash .ovs-ball-x { position: absolute; z-index: 0; left: 0; top: calc(750.98 * var(--u) + var(--gap) - var(--d)); width: var(--d); height: var(--d); pointer-events: none; }
#authOverlay #ovSplash .ovs-ball { display: block; width: 100%; height: 100%; border-radius: var(--radius-full); background: var(--apricot); opacity: 0; transform-origin: 50% 100%;
  box-shadow: 0 0 calc(var(--d) * .6) rgba(232, 133, 58, .45); }
#authOverlay #ovSplash .ovs-tag { position: relative; z-index: 1; margin: var(--space-6) 0 0; max-width: var(--w);
  font-family: Georgia, "Times New Roman", serif; font-style: italic; letter-spacing: .01em;
  font-size: clamp(var(--text-label), calc(var(--w) * 0.045), var(--text-h4, 20px)); color: var(--oa-muted); text-align: center; }
#authOverlay #ovSplash .ovs-tile { transform-box: fill-box; transform-origin: 50% 50%; }
#authOverlay #ovSplash .ovs-word { transform-box: fill-box; transform-origin: 0 100%; }
/* the ripples: where the ball lands, a ring of that level's colour spreads along the bar (the last round the full stop) */
#authOverlay #ovSplash .ovs-ripples { position: absolute; inset: 0; z-index: 2; pointer-events: none; }
#authOverlay #ovSplash .ovs-ripples > i { position: absolute; width: calc(var(--d) * 1.6); height: calc(var(--d) * .7); translate: -50% -50%; border-radius: 50%;
  border: 2px solid var(--c); opacity: 0;
  left: calc((var(--w) - var(--d)) * var(--k) / 5 + var(--d) / 2); top: calc(750.98 * var(--u) + var(--gap)); }
#authOverlay #ovSplash .ovs-ripples > i:last-child { left: calc(2578.36 * var(--u)); top: calc(644.26 * var(--u)); width: calc(var(--d) * 1.8); height: calc(var(--d) * 1.8); }
/* the set behind everything: the seven lights (on, by default) and the bloom (only a flash) */
#authOverlay #ovSplash .ovs-set { position: fixed; inset: 0; z-index: -1; pointer-events: none; overflow: hidden;
  background: radial-gradient(120% 90% at 50% 50%, transparent 55%, rgba(0, 0, 0, .34)); }
#authOverlay #ovSplash .ovs-bloom { position: absolute; left: 50%; top: 46%; width: min(90vmin, 900px); aspect-ratio: 1; translate: -50% -50%; border-radius: 50%; opacity: 0;
  background: radial-gradient(closest-side, rgba(255, 236, 214, .32), rgba(232, 133, 58, .12) 40%, transparent); }
@media (max-height: 160px) { #authOverlay #ovSplash .ovs-tag { display: none; } } /* 400% zoom: logo and bar only */
@media (forced-colors: active) { #authOverlay #ovSplash .ovs-logo { forced-color-adjust: none; } #authOverlay #ovSplash { --oa-letters: CanvasText; } } /* the letters take the theme's text colour */

/* ============================================================
   5. Movement. Only when the person has not asked for less of it. Every part's default above is its final state.
   ============================================================ */
@media (prefers-reduced-motion: no-preference) {
  /* the splash: the stage starts dark; at each landing the next level's light comes up, flares and settles, and its ripple
     spreads; the fill follows the ball (a sliver until it lands at 10%, never 0, or loginfx's watch takes the fill for stopped
     and steps the bar; full at 86%); at the full stop the logo blooms */
  #authOverlay #ovSplash .ovs-wash { animation: oaLevelOn 1.1s var(--ease-out) calc(var(--ov-splash-ms, 2000ms) * var(--t)) both; }
  #authOverlay #ovSplash .ovs-ripples > i { animation: oaRipple .8s cubic-bezier(.2, .7, .3, 1) calc(var(--ov-splash-ms, 2000ms) * var(--t)) both; }
  #authOverlay #ovSplash .ovs-bloom { animation: oaBloom 1.1s var(--ease-out) calc(var(--ov-splash-ms, 2000ms) * .96) both; }
  #authOverlay #ovSplash #ovSplashBar > span { animation: obiSplashFill var(--ov-splash-ms, 2000ms) linear forwards; }
  #authOverlay #ovSplash .ovs-tile { animation: obiTilePop 850ms var(--obi-ease-pop) both; }
  #authOverlay #ovSplash .ovs-word { animation: oaWordLit 1000ms var(--ease-out) 120ms both; }
  #authOverlay #ovSplash .ovs-stop { animation: obiStopShow var(--ov-splash-ms, 2000ms) linear both; }
  #authOverlay #ovSplash .ovs-ball-x { animation: obiBallX var(--ov-splash-ms, 2000ms) linear both; }
  #authOverlay #ovSplash .ovs-ball { animation: obiBallY var(--ov-splash-ms, 2000ms) both; }
  #authOverlay #ovSplash .ovs-tag { animation: oaTagIn 900ms var(--ease-out) 450ms both; }
  /* the splash fades a little slower than Obi's 200ms, so the bloom finishes over the card rising (loginfx removes it after its
     own computed transition) */
  #authOverlay.ov-revealed #ovSplash { transition: opacity 450ms var(--ease-out); }

  /* the room comes up as the splash goes: the lights (already where the splash left them), the card, then the panel's scenes */
  #authOverlay.ov-revealed .oa-house { animation: oaUp .6s var(--ease-out) both; }
  #authOverlay.ov-revealed > .auth-card { animation: oaCardIn 900ms var(--oa-spring-rise) both; }
  #authOverlay.ov-revealed > .auth-card > * { animation: obiRowIn 550ms var(--obi-ease-settle) both; }
  #authOverlay.ov-revealed > .auth-card > :nth-child(2) { animation-delay: 60ms; }
  #authOverlay.ov-revealed > .auth-card > :nth-child(3) { animation-delay: 110ms; }
  #authOverlay.ov-revealed > .auth-card > :nth-child(4) { animation-delay: 160ms; }
  #authOverlay.ov-revealed > .auth-card > :nth-child(5) { animation-delay: 210ms; }
  #authOverlay.ov-revealed > .auth-card > :nth-child(n+6) { animation-delay: 260ms; }
  #authOverlay.ov-revealed > .auth-card > .brand .dot { animation: obiDotDrop 1000ms 260ms both; }
  #authOverlay.ov-revealed > .auth-card > .brand .word::after { animation: obiShadowDrop 1000ms 260ms both; }
  #authOverlay.ov-revealed > .auth-card > .brand .oa-stop { animation: oaStopDrop 1000ms 260ms both; }
  #authOverlay.ov-revealed .oa-show-h { animation: obiRowIn 700ms var(--obi-ease-settle) .2s both; }
  #authOverlay.ov-revealed .oa-web { animation: oaSceneIn 900ms var(--oa-spring-rise) .45s both; }
  #authOverlay.ov-revealed .oa-phone { animation: oaPhoneIn 900ms var(--oa-spring-rise) .8s both; }
  #authOverlay.ov-revealed .oa-lv > i::before { animation: oaGrow 900ms var(--oa-spring-rise) calc(1.15s + var(--i) * 160ms) both; }
  #authOverlay.ov-revealed .oa-obs { animation: obiRowIn 600ms var(--obi-ease-settle) 1.5s both; }
  #authOverlay.ov-revealed .oa-jot { animation: oaJot 1.3s steps(36, end) 1.45s both; }            /* the note writes itself */
  #authOverlay.ov-revealed .oa-chips > span { animation: oaChip 850ms var(--obi-ease-pop) 2.7s both; }
  #authOverlay.ov-revealed .oa-chips > span + span { animation-delay: 2.82s; }

  /* busy: the ball hops in place, and a wave of light passes along the seven levels, until the answer comes back (obi-auth.js
     lets the last hop land first) */
  #authOverlay .auth-card[data-obi-busy] > .brand .dot { animation: obiDotHop 600ms infinite; }
  #authOverlay .auth-card[data-obi-busy] > .brand .word::after { animation: obiShadowHop 600ms infinite; }
  #authOverlay .auth-card[data-obi-busy] > .brand .oa-stop { animation: oaStopHop 600ms infinite; }
  /* keyed to aria-busy, which obi-auth.js removes the moment the answer comes (data-obi-busy waits for the last hop to land), so
     the wave never runs into the error's flicker */
  #authOverlay:has(> .auth-card[aria-busy="true"]) .oa-wash { animation: oaWave 1.4s ease-in-out calc(var(--k) * 120ms) infinite; }
  /* after a hop, or right after the splash landed one, the ball stays down (obi-auth.js sets the mark, and takes it off when
     card() draws a new step, whose ball then drops in) */
  #authOverlay .auth-card[data-obi-landed]:not([data-obi-busy]) > .brand .dot,
  #authOverlay .auth-card[data-obi-landed]:not([data-obi-busy]) > .brand .word::after,
  #authOverlay .auth-card[data-obi-landed]:not([data-obi-busy]) > .brand .oa-stop { animation: none; }
  /* the warning arrives in its slot: the red wipes across from the left, the words follow */
  #authOverlay .auth-card > #loginErr:not(:empty) { animation: oaWipe 420ms cubic-bezier(.2, .7, .3, 1) both; }

  #authOverlay .auth-card > .btn { transition: background-color var(--motion-fast) var(--ease-out), transform var(--motion-fast) var(--ease-out); }

  /* the arrival: sign-in finished and the card left (obi-auth.js puts .obi-arrive on <html>, on this page or, after
     obi-db.js's reload, on the next). The house lights come up: a veil of the dark fades off the app (its default is
     invisible, so with every animation off it never covers anything), while the header comes down and the navigation and
     the page rise in. */
  html.obi-arrive body::after { content: ""; position: fixed; inset: 0; z-index: 2147483000; pointer-events: none; background: var(--oa-house); opacity: 0;
    animation: oaLightsUp 700ms var(--ease-out) both; }
  html.obi-arrive header:not(#view header) { animation: obiArriveDown 550ms var(--obi-ease-settle) 120ms both; }
  html.obi-arrive #tabs { animation: obiRowIn 550ms var(--obi-ease-settle) 190ms both; }
  html.obi-arrive #view { animation: obiRowIn 550ms var(--obi-ease-settle) 260ms both; }
}

@keyframes oaUp { from { opacity: 0; } to { opacity: 1; } }
@keyframes oaLevelOn { 0% { opacity: 0; scale: .5; } 20% { opacity: 1; scale: 1.08; filter: brightness(2); } 100% { opacity: 1; scale: 1; filter: brightness(1); } } /* a level's light comes up, flares, settles */
@keyframes oaRipple { 0% { opacity: .95; scale: .3; } 100% { opacity: 0; scale: 2.2; } }
@keyframes oaBloom { 0% { opacity: 0; scale: .7; } 30% { opacity: 1; scale: 1; } 100% { opacity: .35; scale: 1.08; } }
@keyframes oaWave { 0%, 100% { filter: brightness(1); } 40% { filter: brightness(2); } } /* each light flares in turn; when the answer comes, obi-auth.js eases it back (no snap) */
@keyframes oaWordLit { from { fill: var(--oa-dim); } to { fill: var(--oa-letters); } } /* to the letters' colour: cream, or the theme's text in forced colours */
@keyframes oaTagIn { from { opacity: 0; translate: 0 10px; } to { opacity: 1; translate: 0 0; } }
@keyframes oaCardIn { from { opacity: 0; transform: translateY(28px) scale(.97); } to { opacity: 1; transform: none; } }
@keyframes oaSceneIn { from { opacity: 0; transform: translateY(36px) scale(.96); } to { opacity: 1; transform: none; } }
@keyframes oaPhoneIn { from { opacity: 0; transform: translate(40px, 56px) rotate(6deg); } to { opacity: 1; transform: none; } }
@keyframes oaGrow { from { transform: scaleX(0); } to { transform: scaleX(1); } }
@keyframes oaJot { from { clip-path: inset(0 100% 0 0); } to { clip-path: inset(0 0 0 0); } }
@keyframes oaChip { from { opacity: 0; transform: scale(.6); } to { opacity: 1; transform: none; } }
@keyframes oaWipe { from { clip-path: inset(0 100% 0 0 round var(--radius-control)); } to { clip-path: inset(0 0 0 0 round var(--radius-control)); } }
@keyframes oaLightsUp { from { opacity: 1; } to { opacity: 0; } }
@keyframes obiRowIn  { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
@keyframes obiArriveDown { from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; transform: none; } }

/* A ball dropped from above the word: it falls (ease-in), squashes on the baseline, bounces to a third of the drop,
   lands, bounces once more, small, and settles. Heights in em, so it scales with the wordmark. */
@keyframes obiDotDrop {
  0%   { transform: translateY(-1.7em) scale(0.92, 1.08); opacity: 0; animation-timing-function: cubic-bezier(0.5, 0, 0.9, 0.5); }
  6%   { opacity: 1; }
  31%  { transform: translateY(-0.04em) scale(0.86, 1.16); animation-timing-function: linear; }   /* stretched by the fall, a hair above the line */
  34%  { transform: translateY(0) scale(1.35, 0.68); animation-timing-function: cubic-bezier(0.2, 0.6, 0.4, 1); }   /* contact: squash */
  42%  { transform: translateY(-0.12em) scale(0.88, 1.14); animation-timing-function: cubic-bezier(0.2, 0.6, 0.4, 1); }
  58%  { transform: translateY(-0.55em) scale(1, 1); animation-timing-function: cubic-bezier(0.5, 0, 0.9, 0.5); }
  72%  { transform: translateY(-0.03em) scale(0.94, 1.07); animation-timing-function: linear; }
  74%  { transform: translateY(0) scale(1.18, 0.84); animation-timing-function: cubic-bezier(0.2, 0.6, 0.4, 1); }
  84%  { transform: translateY(-0.14em) scale(1, 1); animation-timing-function: cubic-bezier(0.5, 0, 0.9, 0.5); }
  92%  { transform: translateY(0) scale(1.06, 0.95); }
  100% { transform: none; opacity: 1; }
}
@keyframes obiShadowDrop {
  0%   { opacity: 0;    transform: scale(0.2); animation-timing-function: cubic-bezier(0.5, 0, 0.9, 0.5); }
  34%  { opacity: 0.35; transform: scale(1.3); animation-timing-function: cubic-bezier(0.2, 0.6, 0.4, 1); }
  58%  { opacity: 0.12; transform: scale(0.6); animation-timing-function: cubic-bezier(0.5, 0, 0.9, 0.5); }
  74%  { opacity: 0.3;  transform: scale(1.15); animation-timing-function: cubic-bezier(0.2, 0.6, 0.4, 1); }
  84%  { opacity: 0.2;  transform: scale(0.9); }
  100% { opacity: 0;    transform: scale(1); }
}
/* The same drop and hop for the logo's circle, in its own size (100% = the full stop's diameter, about 0.2 of the type) */
@keyframes oaStopDrop {
  0%   { transform: translateY(-850%) scale(0.92, 1.08); opacity: 0; animation-timing-function: cubic-bezier(0.5, 0, 0.9, 0.5); }
  6%   { opacity: 1; }
  31%  { transform: translateY(-20%) scale(0.86, 1.16); animation-timing-function: linear; }
  34%  { transform: translateY(0) scale(1.35, 0.68); animation-timing-function: cubic-bezier(0.2, 0.6, 0.4, 1); }
  42%  { transform: translateY(-60%) scale(0.88, 1.14); animation-timing-function: cubic-bezier(0.2, 0.6, 0.4, 1); }
  58%  { transform: translateY(-275%) scale(1, 1); animation-timing-function: cubic-bezier(0.5, 0, 0.9, 0.5); }
  72%  { transform: translateY(-15%) scale(0.94, 1.07); animation-timing-function: linear; }
  74%  { transform: translateY(0) scale(1.18, 0.84); animation-timing-function: cubic-bezier(0.2, 0.6, 0.4, 1); }
  84%  { transform: translateY(-70%) scale(1, 1); animation-timing-function: cubic-bezier(0.5, 0, 0.9, 0.5); }
  92%  { transform: translateY(0) scale(1.06, 0.95); }
  100% { transform: none; opacity: 1; }
}
@keyframes oaStopHop {
  0%   { transform: none; }
  8%   { transform: translateY(0) scale(1.14, 0.86); animation-timing-function: cubic-bezier(0.2, 0.6, 0.4, 1); }
  20%  { transform: translateY(-50%) scale(0.92, 1.1); animation-timing-function: cubic-bezier(0.2, 0.6, 0.4, 1); }
  52%  { transform: translateY(-250%) scale(1, 1); animation-timing-function: cubic-bezier(0.5, 0, 0.9, 0.5); }
  84%  { transform: translateY(0) scale(1.14, 0.86); animation-timing-function: cubic-bezier(0.2, 0.6, 0.4, 1); }
  100% { transform: none; }
}
/* each hop starts and ends at rest (scale 1), so when the last one lands and the animation stops there is no jump */
@keyframes obiDotHop {
  0%   { transform: none; }
  8%   { transform: translateY(0) scale(1.14, 0.86); animation-timing-function: cubic-bezier(0.2, 0.6, 0.4, 1); }   /* crouch */
  20%  { transform: translateY(-0.1em) scale(0.92, 1.1); animation-timing-function: cubic-bezier(0.2, 0.6, 0.4, 1); }
  52%  { transform: translateY(-0.5em) scale(1, 1); animation-timing-function: cubic-bezier(0.5, 0, 0.9, 0.5); }
  84%  { transform: translateY(0) scale(1.14, 0.86); animation-timing-function: cubic-bezier(0.2, 0.6, 0.4, 1); }   /* land */
  100% { transform: none; }
}
@keyframes obiShadowHop {
  0%, 100% { opacity: 0;    transform: scale(1); }
  8%, 84%  { opacity: 0.3;  transform: scale(1.2); }
  52%      { opacity: 0.08; transform: scale(0.55); }
}
@keyframes obiSplashFill { 0% { width: 0; } 10% { width: 3%; } 86%, 100% { width: 100%; } }
@keyframes obiTilePop { from { opacity: 0; transform: scale(0.55) rotate(-8deg); } to { opacity: 1; transform: none; } }
@keyframes obiStopShow { 0%, 97.4% { opacity: 0; } 97.5%, 100% { opacity: 1; } }
@keyframes obiBallX { 0%, 10% { transform: translateX(0); } 86%, 100% { transform: translateX(calc(var(--w) - var(--d))); } }
/* Five hops, each lower, landing at 10, 25.2, 40.4, 55.6, 70.8 and 86%; a crouch; one leap; it lands in the full stop's
   place at 96%, squashes, rounds, and the logo's own circle takes over at 97.5%. Up eases out, down eases in. */
@keyframes obiBallY {
  0%    { opacity: 0; transform: translateY(calc(var(--w) * -0.22)) scale(0.9, 1.1); animation-timing-function: cubic-bezier(0.5, 0, 0.9, 0.5); }
  6%    { opacity: 0; }   /* it shows only once it is below the tile, which is still fading in */
  8%    { opacity: 1; }
  10%   { opacity: 1; transform: translateY(0) scale(1.3, 0.72); animation-timing-function: linear; }
  11.5% { transform: translateY(0) scale(0.9, 1.12); animation-timing-function: cubic-bezier(0.2, 0.6, 0.4, 1); }
  17.6% { transform: translateY(calc(var(--w) * -0.075)) scale(1, 1); animation-timing-function: cubic-bezier(0.5, 0, 0.9, 0.5); }
  25.2% { transform: translateY(0) scale(1.22, 0.8); animation-timing-function: linear; }
  26.5% { transform: translateY(0) scale(0.92, 1.1); animation-timing-function: cubic-bezier(0.2, 0.6, 0.4, 1); }
  32.8% { transform: translateY(calc(var(--w) * -0.06)) scale(1, 1); animation-timing-function: cubic-bezier(0.5, 0, 0.9, 0.5); }
  40.4% { transform: translateY(0) scale(1.18, 0.84); animation-timing-function: linear; }
  41.7% { transform: translateY(0) scale(0.94, 1.08); animation-timing-function: cubic-bezier(0.2, 0.6, 0.4, 1); }
  48%   { transform: translateY(calc(var(--w) * -0.045)) scale(1, 1); animation-timing-function: cubic-bezier(0.5, 0, 0.9, 0.5); }
  55.6% { transform: translateY(0) scale(1.14, 0.88); animation-timing-function: linear; }
  56.9% { transform: translateY(0) scale(0.95, 1.06); animation-timing-function: cubic-bezier(0.2, 0.6, 0.4, 1); }
  63.2% { transform: translateY(calc(var(--w) * -0.035)) scale(1, 1); animation-timing-function: cubic-bezier(0.5, 0, 0.9, 0.5); }
  70.8% { transform: translateY(0) scale(1.1, 0.9); animation-timing-function: linear; }
  72.1% { transform: translateY(0) scale(0.96, 1.05); animation-timing-function: cubic-bezier(0.2, 0.6, 0.4, 1); }
  78.4% { transform: translateY(calc(var(--w) * -0.025)) scale(1, 1); animation-timing-function: cubic-bezier(0.5, 0, 0.9, 0.5); }
  86%   { transform: translateY(0) scale(1.34, 0.68); animation-timing-function: linear; }                     /* crouch */
  88%   { transform: translateY(0) scale(0.84, 1.2); animation-timing-function: cubic-bezier(0.2, 0.6, 0.4, 1); } /* spring */
  92.5% { transform: translateY(calc(var(--leap) - var(--w) * 0.14)) scale(1, 1); animation-timing-function: cubic-bezier(0.5, 0, 0.9, 0.5); }
  96%   { transform: translateY(var(--leap)) scale(1.24, 0.8); animation-timing-function: cubic-bezier(0.2, 0.6, 0.4, 1); }
  97.4% { opacity: 1; transform: translateY(var(--leap)) scale(1, 1); }
  97.5%, 100% { opacity: 0; transform: translateY(var(--leap)) scale(1, 1); }
}

/* ============================================================
   6. Windows high contrast: the light goes (it is decoration); the card and the panel are plain framed boxes; the warning
   keeps a real border
   ============================================================ */
@media (forced-colors: active) {
  #authOverlay .oa-house, #authOverlay #ovSplash .ovs-set, #authOverlay #ovSplash .ovs-ripples { display: none; }
  #authOverlay:not(.ov-front) { background: Canvas; }
  #authOverlay :is(.auth-card, .oa-show) { background: Canvas; color: CanvasText; border: 1px solid CanvasText; box-shadow: none; -webkit-backdrop-filter: none; backdrop-filter: none; }
  #authOverlay .oa-glass, #authOverlay .auth-card > .brand .word::after { display: none; }
  #authOverlay .auth-card :is(#authTitle, .sub, label.fld, .brand .word), #authOverlay .oa-show :is(.oa-show-h, .oa-cue) { color: CanvasText; }
  #authOverlay .auth-card .auth-err:not(:empty) { border: 1px solid CanvasText; }
  #authOverlay .auth-card .auth-err:not(:empty)::before { forced-color-adjust: none; background: CanvasText; box-shadow: none; }
  #authOverlay .oa-screen { border: 1px solid CanvasText; }
  #authOverlay .oa-lv > i { border: 1px solid CanvasText; }
  #authOverlay .oa-lv > i::before { forced-color-adjust: none; background: CanvasText; }
  #authOverlay .auth-card > .brand.oa-branded .oa-logo { forced-color-adjust: none; }
  #authOverlay .auth-card > .brand.oa-branded .oa-lword { fill: CanvasText; }
}

/* a phone: the card fills the width; the slot takes three lines */
@media (max-width: 480px) {
  #authOverlay .auth-card { --oa-slot: 4.25rem; }
  #authOverlay .auth-card > .brand { margin-bottom: var(--space-4); }
}

/* print: never the room */
@media print { #authOverlay .oa-house { display: none; } }
