/* The sign-in page's own look (D-112, F-UI-14): a network that turns in depth behind the
   card, and the robot that looks over it. Loaded by the sign-in page only.

   The same rules as app.css (F-UI-02): no color value here, only tokens. Brand red is only
   ever a dot or a short thin streak (D-54: "a thin line or a dot"): the robot's antenna tip
   and microphone, and a few of the signals that run along the network's lines.

   Everything here works with no script. `signin.js` only adds the eyes' following and the
   Show / Hide button. */

/* --- The network (core/_signin_network.html): real depth, moved by CSS alone. ---

   Every dot and line is an element placed in three dimensions (its --x, --y, --z). The
   whole space turns about its upright axis, and the browser draws it with perspective: a
   near line sweeps past large, a far one drifts small. Because the *space* turns and not
   each dot, every line stays joined to its dots with no script to redraw it.

   From the outside in: .net (your eye) > .net-stage (the pointer's tilt) > .net-drift (the
   camera gliding in, then floating) > two .net-world (the web, and far dust turning the
   other way) > the dots, lines and specks. Each one keeps `preserve-3d`: without it the
   space below goes flat. */

.sign-in { position: relative; overflow: hidden; }
.sign-in-card { position: relative; z-index: 2; }

/* Where the pointer is, from -1 to 1 each way (signin-network.css sets them, cell by
   cell). Registered as numbers so that a change glides instead of jumping. */
@property --px { syntax: "<number>"; inherits: true; initial-value: 0; }
@property --py { syntax: "<number>"; inherits: true; initial-value: 0; }

.net { position: fixed; inset: 0; z-index: 0; overflow: hidden; perspective: 92vmax; --px: 0; --py: 0; transition: --px 900ms var(--motion-ease), --py 900ms var(--motion-ease), opacity 600ms var(--motion-ease); animation: net-appear 1800ms ease-out both; }
@keyframes net-appear { from { opacity: 0; } }
.net i, .net b { position: absolute; top: 0; left: 0; display: block; pointer-events: none; }

/* A pool of light that follows the pointer. */
.net::before { content: ""; position: absolute; inset: 0; background: radial-gradient(circle at calc(50% + var(--px) * 44%) calc(50% + var(--py) * 44%), var(--bg-surface), transparent 40%); }
/* And the network fades away toward the edges of the screen, into the page. */
.net::after { content: ""; position: absolute; inset: 0; background: radial-gradient(ellipse at center, transparent 62%, var(--bg-page) 108%); pointer-events: none; }

/* Looking around: the pointer tilts the whole space, as turning your head would. */
.net-stage { position: absolute; top: 50%; left: 50%; transform-style: preserve-3d; transform: rotateX(calc(var(--py) * -10deg)) rotateY(calc(var(--px) * 15deg)); }

/* The camera: it glides forward into the network as the page opens, then floats a little,
   for ever, as a camera held in the hand would. */
.net-drift { transform-style: preserve-3d; animation: net-arrive 2800ms var(--motion-ease) both, net-float 26s ease-in-out 2800ms infinite alternate; }
/* (No fading here: an element that is being faded is drawn flat, and the space below it
   would lose its depth. The whole network fades in on .net instead.) */
@keyframes net-arrive { from { transform: translateZ(-46vmax) rotateX(14deg); } }
@keyframes net-float { from { transform: translate3d(0, 0, 0) rotateX(-1.5deg); } to { transform: translate3d(1.2vmax, -.8vmax, 4vmax) rotateX(2deg) rotateZ(1.2deg); } }

/* The space turns, slowly, for ever. The dust behind it turns the other way, slower. */
.net-world { position: absolute; top: 0; left: 0; transform-style: preserve-3d; animation: net-spin 160s linear infinite; }
.net-dust { animation-duration: 340s; animation-direction: reverse; }
@keyframes net-spin { to { transform: rotateY(1turn); } }

/* Haze: two see-through sheets of the page's own color stand in the space, so what is
   behind them is fainter. That is the whole of the depth of field. */
.net-fog { pointer-events: none; position: absolute; top: -150vmax; left: -150vmax; width: 300vmax; height: 300vmax; background: var(--bg-page); opacity: .46; }
.net-fog-near { transform: translateZ(-2vmax); }
.net-fog-far { transform: translateZ(-24vmax); }

/* Dust: fine specks far out, which twinkle as they turn edge-on. */
.net .d { width: 3px; height: 3px; border-radius: 50%; background: var(--text-muted); transform: translate3d(var(--x), var(--y), var(--z)); animation: net-twinkle 5s ease-in-out infinite alternate; animation-delay: calc(var(--o) * -260ms); }
@keyframes net-twinkle { from { opacity: .15; } to { opacity: .75; } }

/* A line: a strip one pixel high, lying along its own x from the dot it starts at, swung
   round into depth (--ry) and tipped (--rz). It fades out at both ends, and draws itself
   outward when the page opens. */
.net .l { width: var(--l); height: 1px; background: linear-gradient(to right, transparent, var(--text-secondary) 10%, var(--text-secondary) 90%, transparent); opacity: .9; transform-origin: 0 50%; transform: translate3d(var(--x), var(--y), var(--z)) rotateY(var(--ry)) rotateZ(var(--rz)); transform-style: preserve-3d; animation: net-draw 1300ms var(--motion-ease) both; animation-delay: calc(var(--o) * 70ms + 400ms); }
@keyframes net-draw { from { scale: 0 1; opacity: 0; } }

/* A signal: a short streak with a fading tail that runs the length of its line, again and
   again. A few are brand red, and glow. */
.net .s::after { content: ""; position: absolute; top: -1px; left: 0; width: 2.4vmax; height: 3px; border-radius: 3px; background: linear-gradient(to right, transparent, var(--text-primary)); translate: -100% 0; animation: net-signal var(--pt) linear infinite; animation-delay: var(--pd); }
.net .s.hot::after { background: linear-gradient(to right, transparent, var(--brand-accent)); box-shadow: 0 0 8px var(--brand-accent); }
@keyframes net-signal { from { left: 0; opacity: 0; } 10%, 90% { opacity: 1; } to { left: calc(100% + 2.4vmax); opacity: 0; } }

/* A dot. The outer element only holds its place. The inner one is the dot itself, with a
   soft halo, and it turns exactly against the space (the spin, then the pointer's tilt,
   undone in reverse order), so it always faces you and stays round. */
.net .n { transform: translate3d(var(--x), var(--y), var(--z)); transform-style: preserve-3d; }
.net .n b { top: -4px; left: -4px; width: 8px; height: 8px; border-radius: 50%; background: var(--text-primary); box-shadow: 0 0 0 4px color-mix(in srgb, var(--text-primary) 12%, transparent); animation: net-face 160s linear infinite, net-pop 800ms var(--motion-ease) both; animation-delay: 0s, calc(var(--o) * 70ms + 700ms); }
.net .n.hub b { top: -6px; left: -6px; width: 12px; height: 12px; box-shadow: 0 0 0 6px color-mix(in srgb, var(--text-primary) 12%, transparent); }
@keyframes net-face {
  from { transform: rotateY(0turn) rotateY(calc(var(--px) * -15deg)) rotateX(calc(var(--py) * 10deg)); }
  to { transform: rotateY(-1turn) rotateY(calc(var(--px) * -15deg)) rotateX(calc(var(--py) * 10deg)); }
}
@keyframes net-pop { from { scale: 0; } 70% { scale: 1.6; } }

/* The main dots breathe: a thin ring opens out from each and fades, out of step. */
.net .n.hub b::before { content: ""; position: absolute; inset: -2px; border: 1px solid var(--text-primary); border-radius: 50%; animation: net-breathe 4.2s ease-out infinite; animation-delay: calc(var(--o) * 190ms); }
@keyframes net-breathe { from { scale: 1; opacity: .8; } 70%, to { scale: 3.4; opacity: 0; } }

/* A press sends a wave out through the dots: a ring from each, the middle ones first. */
.net .n b::after { content: ""; position: absolute; inset: -1px; border: 1px solid var(--text-primary); border-radius: 50%; opacity: 0; }
.net:has(.c:active) .n b::after { animation: net-wave 900ms ease-out both; animation-delay: calc(var(--o) * 28ms); }
@keyframes net-wave { from { scale: 1; opacity: .9; } to { scale: 8; opacity: 0; } }

/* The grid the pointer moves over: see-through cells that only feel the pointer. */
.net-cells { position: absolute; inset: 0; z-index: 1; display: grid; grid-template-columns: repeat(12, 1fr); grid-template-rows: repeat(8, 1fr); }
.net .net-cells .c { position: static; pointer-events: auto; }

/* While the password is typed the network steps back, as the robot looks away. (Not for
   Username: that box is in use from the moment the page opens.) */
.sign-in:has(#id_password:focus) .net { opacity: .45; }

/* --- The robot (ported from TOS ServiceDesk's sign-in form, b372809; D-112). --- */

.sign-in-card.has-bot { margin-top: 106px; padding-top: 48px; }
.bot { position: absolute; top: -106px; left: 50%; width: 166px; translate: -50% 0; --lx: 0; --ly: 0; }
.bot svg { display: block; width: 100%; height: auto; overflow: visible; }

.bot-shell { fill: var(--bg-surface); stroke: var(--border-control); stroke-width: 2; }
.bot-band { fill: none; stroke: var(--border-control); stroke-width: 8; }
.bot-cup { fill: var(--bg-hover); stroke: var(--border-control); stroke-width: 2; }
.bot-palm { fill: var(--border-control); stroke: var(--brand-steel); stroke-width: 2; }
.bot-fingers { fill: none; stroke: var(--brand-steel); stroke-width: 2; stroke-linecap: round; }
.bot-boom { fill: none; stroke: var(--border-control); stroke-width: 5; stroke-linecap: round; }
.bot-antenna { stroke: var(--border-control); stroke-width: 4; stroke-linecap: round; }
.bot-screen { fill: var(--brand-ink); }
.bot-eye { fill: var(--brand-white); }
.bot-pupil { fill: var(--brand-ink); }
.bot-closed { fill: none; stroke: var(--brand-white); stroke-width: 3; stroke-linecap: round; opacity: 0; }
/* D-54, D-112: the two red dots. */
.bot-antenna-tip, .bot-mic { fill: var(--brand-accent); }

.bot-antenna-tip { transform-box: fill-box; transform-origin: center; animation: bot-pulse 1.8s ease-in-out infinite; }
@keyframes bot-pulse { 0%, 100% { scale: 1; opacity: 1; } 50% { scale: .72; opacity: .55; } }

/* It blinks, now and then. */
.bot-eyes { transform-origin: 110px 94px; animation: bot-blink 5.5s infinite; }
@keyframes bot-blink { 0%, 95%, 100% { scale: 1 1; } 97.5% { scale: 1 .08; } }

/* Where it looks: --lx and --ly, a few units either way. With no script it looks down at
   the Username box while that is in use; signin.js follows the typing and the pointer. */
.bot-pupil { transform: translate(calc(var(--lx) * 1px), calc(var(--ly) * 1px)); transition: transform 140ms var(--motion-ease), opacity 200ms; }
.bot-eye, .bot-closed { transition: opacity 200ms; }
.sign-in-card:has(#id_username:focus) .bot { --ly: 5; }

/* Hands over its eyes while the Password box is in use. */
.bot-hand { transition: transform 300ms var(--motion-ease); }
.sign-in-card:has(#id_password:focus) .bot-eye,
.sign-in-card:has(#id_password:focus) .bot-pupil { opacity: 0; }
.sign-in-card:has(#id_password:focus) .bot-closed { opacity: 1; }
.sign-in-card:has(#id_password:focus) .bot-hand-l { transform: translate(22px, -58px); }
.sign-in-card:has(#id_password:focus) .bot-hand-r { transform: translate(-22px, -58px); }
.sign-in-card:has(#id_password:focus) .bot-eyes { animation: none; }

/* The password is shown (signin.js sets this): it looks through its fingers with one eye. */
/* `:has(#id_password)` is here for its weight alone: the rules above carry an id, and
   these must win over them. */
.sign-in-card:has(#id_password) .bot.is-peeking .bot-hand-l { transform: translate(-2px, -32px); }
.sign-in-card:has(#id_password) .bot.is-peeking .bot-eye-l,
.sign-in-card:has(#id_password) .bot.is-peeking .bot-pupil-l { opacity: 1; }
.sign-in-card:has(#id_password) .bot.is-peeking .bot-closed-l { opacity: 0; }
.sign-in-card:has(#id_password) .bot.is-peeking { --lx: -3; --ly: 4; }

/* A wrong username or password: it shakes its head, twice. */
.bot-head { transform-origin: 110px 144px; }
.sign-in-card:has(.alert-critical) .bot-head { animation: bot-no 460ms ease-in-out 2; }
@keyframes bot-no { 0%, 100% { rotate: 0deg; } 30% { rotate: -7deg; } 70% { rotate: 7deg; } }

/* A small wave when the pointer is on it. */
.bot:hover .bot-hand-r { transform: translate(8px, -20px); }

/* The Show / Hide button signin.js adds beside the password. */
.pw-wrap { position: relative; display: block; }
.pw-wrap input { width: 100%; padding-right: 64px; }
.pw-toggle { position: absolute; top: 50%; right: var(--space-2); translate: 0 -50%; padding: var(--space-1) var(--space-2); border: 0; border-radius: var(--radius-sm); background: none; color: var(--text-secondary); font: inherit; font-size: var(--text-caption-size); font-weight: 600; cursor: pointer; }
.pw-toggle:hover { background: var(--bg-hover); color: var(--text-primary); }

/* Someone who asks their computer for less motion gets a still network and a still robot:
   the space stops turning, nothing draws itself, runs or ripples, and the pointer moves
   nothing. The robot still covers its eyes: that is what it is for, and it is one short
   move. */
@media (prefers-reduced-motion: reduce) {
  .net, .net-drift, .net-world, .net .d, .net .l, .net .n b, .net .n.hub b::before, .net .n b::after, .net .s::after, .bot-antenna-tip, .bot-eyes, .bot-head { animation: none !important; }
  .net .s::after, .net .n.hub b::before { display: none; }
  .net-stage { transform: none; }
  .net::before { display: none; }
}
