/* loader.css
   The full-screen route loader and the café mark that animates inside it.

   Split out of shared.css so the admin pages can have it too. Those pages are
   deliberately self-contained — they carry their own tokens, buttons and toast
   rather than importing the student design system — so pulling in the whole of
   shared.css to get a loading animation would have meant its body rules and
   button styles landing on top of theirs. This is the part they actually need,
   and nothing else.

   Every custom property is written with a fallback for the same reason: on the
   admin side there is no guarantee the token it wants has been declared.

   The geometry is injected by logo-mark.js; the timeline is entirely here.
*/

/* Full-screen loader shown while a page checks session/auth state, hidden
   (faded out) the moment the real UI is ready to be revealed. */
.route-loader {
    position: fixed; inset: 0; z-index: 2000;
    background: var(--bg, #f3f4f6);
    display: flex; align-items: center; justify-content: center;
    opacity: 1; transition: opacity 0.25s ease;
}
.route-loader.hide { opacity: 0; pointer-events: none; }
/* Stand-in for the café mark, shown only if logo-mark.js hasn't run — it
   replaces this element in place (see mount() there). Its keyframes are
   deliberately repeated from shared.css: this file has to stand on its own. */
.route-spinner {
    width: 38px; height: 38px; border-radius: 50%;
    border: 3px solid var(--border, #e2e8f0); border-top-color: var(--primary, #e07b3c);
    animation: spin 0.7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* ---------- Café mark: the loading animation ----------
   The self-drawing cup index.html opens on, reused as the indicator for every
   wait long enough to notice. Same size, same centre, same stroke as the
   splash, because one page hands this animation straight to the next
   mid-stroke and the seam has to be invisible — logo-mark.js sets --resume to
   however far in the sequence already was. */
.logo-mark { width: min(200px, 50vw); height: min(200px, 50vw); }
.logo-mark-art { width: 100%; height: 100%; }
.logo-mark svg { display: block; width: 100%; height: 100%; }

/* Fades up and settles once, matching the splash's own conductor so a handoff
   lands on the same frame. */
.logo-mark-art {
    animation: logo-rise 2200ms linear both;
    animation-delay: calc(0ms - var(--resume, 0ms));
}
@keyframes logo-rise {
    0%    { opacity: 0; transform: scale(.92); animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1); }
    10.9% { opacity: 1; transform: scale(1);   animation-timing-function: linear; }        /*  240ms */
    90.9% { opacity: 1; transform: scale(1);   animation-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1); } /* 2000ms */
    95.5% { opacity: 1; transform: scale(1.045); }                                          /* 2100ms */
    100%  { opacity: 1; transform: scale(1); }                                              /* 2200ms */
}

/* Two animations, in order: the strokes draw themselves on, then — only if the
   page is still waiting at 2300ms — they keep going on a loop. Listing them
   together means the loop simply takes over the property when it starts, with
   no gap to fall through and nothing for JS to schedule. */
.logo-mark path {
    fill: none; stroke: var(--primary, #e07b3c); stroke-width: 11;
    stroke-linecap: round; stroke-linejoin: round;
    /* The fallback keeps the strokes hidden rather than solid in the frame
       before logo-mark.js has measured them. */
    stroke-dasharray: var(--len, 999); stroke-dashoffset: var(--len, 999);
    animation-name: logo-draw, logo-flow;
    animation-duration: var(--t), 2400ms;
    /* expo-out on the draw, not a bounce: overshoot past 1 would drive
       stroke-dashoffset negative and visibly over-draw the line. The loop is
       linear here because its own keyframes carry the easing per phase. */
    animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1), linear;
    /* The loop keeps the draw's stagger, scaled down: at full spread the cup
       would never be whole, and the beat where it is whole is the point the
       animation is making. */
    animation-delay: calc(600ms + var(--d) - var(--resume, 0ms)),
                     calc(2300ms + var(--d) * 0.42 - var(--resume, 0ms));
    animation-fill-mode: forwards, none;
    animation-iteration-count: 1, infinite;
}
@keyframes logo-draw { to { stroke-dashoffset: 0; } }
/* The loop: hold drawn, un-draw, hold empty, draw back on.

   The un-draw is the draw played backwards — the stroke retracts the way it
   came (offset 0 back to +len) on expo-*in*, which is the exact time-mirror of
   the expo-out the draw uses. That matters: this used to slide the dash off
   the far end instead, on a symmetrical ease and a single duration shared by
   all five strokes, so the cup formed with the careful stagger it was drawn
   with and then came apart as one flat undifferentiated motion. Mirroring it
   means the way it leaves is as considered as the way it arrives — and it also
   drops the seam, since retracting ends empty exactly where drawing starts. */
@keyframes logo-flow {
    0%   { stroke-dashoffset: 0; }
    10%  { stroke-dashoffset: 0;               animation-timing-function: cubic-bezier(0.7, 0, 0.84, 0); }
    27%  { stroke-dashoffset: var(--len, 999); }
    40%  { stroke-dashoffset: var(--len, 999); animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1); }
    57%  { stroke-dashoffset: 0; }
    100% { stroke-dashoffset: 0; }
}

/* Held back briefly so a page that was only ever going to take 80ms doesn't
   flash a loading animation — the overlay underneath is the same colour as the
   page, so a genuinely instant load shows nothing at all. A mark handed over
   from the page before is already on screen as far as the reader is concerned,
   so it skips the wait. */
.route-loader .logo-mark { opacity: 0; animation: logo-mark-in 300ms ease 140ms forwards; }
.route-loader .logo-mark.is-continuing { opacity: 1; animation: none; }
@keyframes logo-mark-in { to { opacity: 1; } }
/* Coming up over the page being left, rather than fading away from the page
   that's ready — quick enough to cover before the navigation commits. */
.route-loader.is-bridging { transition: opacity 140ms ease; }
/* Nothing to animate behind a loader that's been faded out. */
.route-loader.hide .logo-mark, .route-loader.hide .logo-mark * { animation-play-state: paused; }
/* Under the splash the loader sits below an opaque overlay, so its usual fade
   would still be running when the overlay lifts — showing the mark on top of
   the login card. Hide it instantly instead. */
.route-loader.no-fade { transition: none; }
