/* Boot loader for the MAIN APP hosts (WebApp.Client PWA / Telegram Mini App, and the MAUI
   Android app) — the splash shown until MainLayout calls window.hideBootLoader().

   Shared (SharedUI RCL) rather than inlined per host: this was ~200 lines of CSS and ~160 of
   JS living inside WebApp.Client/index.html, so the MAUI app booted to a bare "Loading..."
   instead — copying it a second time would have guaranteed drift.

   NOT used by Reading.Web: the Reader has its own splash (.lw-boot-loader, its own
   wwwroot/css/boot-loader.css) built by lwLoader.start() from lw-loader.js, so its boot orb
   and its in-app LoadingOverlay stay identical. Two designs on purpose; don't merge them
   without deciding which look wins.

   Pair with js/app-boot-loader.js and the markup block documented there. Tokens (--bg,
   --accent-rgb, --star-*) come from tokens.css; the literal fallbacks match the dark-theme
   defaults and cover the theme-flash window before tokens.css applies. */

.boot-loader {
    position: fixed;
    inset: 0;
    z-index: 10000;
    display: flex;
    align-items: center;
    justify-content: center;
    background:
        radial-gradient(circle at 30% 20%, rgba(var(--accent-rgb, 224, 122, 78), 0.18), transparent 55%),
        radial-gradient(circle at 70% 80%, rgba(var(--accent-rgb, 224, 122, 78), 0.12), transparent 50%),
        var(--bg, #000000);
    color: var(--text, #f4f1ec);
    font-family: var(--font, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif);
    overflow: hidden;
    opacity: 1;
    transition: opacity 0.25s ease-out;
}

.boot-loader.is-hiding {
    opacity: 0;
    pointer-events: none;
}

/* The loader is a dark starfield with a per-theme HUE regardless of the page theme:
   remap the page tokens the rules here read (--bg / --text / --accent*) to the cosmos
   palette (tokens.css) so it stays a dark sky even under the light cool/paper themes.
   Literal fallbacks are the night values for the pre-tokens.css flash window. */
.boot-loader {
    --bg:             var(--loader-bg, #0b0e15);
    --text:           var(--loader-text, #e6e8f0);
    --text-secondary: var(--loader-text-dim, #9298a8);
    --accent-rgb:     var(--loader-accent-rgb, 224, 122, 78);
    --accent:         rgb(var(--loader-accent-rgb, 224, 122, 78));
    --accent-strong:  rgb(var(--loader-accent-rgb, 224, 122, 78));
}

.boot-sky { position: absolute; inset: 0; overflow: hidden; pointer-events: none; }

/* Twinkling is animated PER GROUP, not per star. An animated `opacity` promotes its element
   to its own compositor layer, so the earlier one-animation-per-star version asked the
   compositor for ~90 extra layers (measured: 123 layers / 25 MB of layer memory for this
   splash alone). A desktop GPU absorbs that; a mid-range Android WebView does not — it hits
   its layer budget, folds the surplus back onto the main thread, and that thread is exactly
   where the MAUI host is booting .NET (measured: one 700 ms / 77-frame stall at launch).
   Result was a splash that stuttered precisely while it was on screen.
   Six groups with staggered durations keep the shimmer visually irregular at six layers.

   Each group is a horizontal BAND, not a full-bleed `inset: 0` box: a promoted layer costs
   width × height × 4 bytes, so six stacked full-screen groups traded 123 small layers for six
   huge ones (25 MB → 57 MB on a 1280×720 viewport — worse on a 1080×2400 phone, which is the
   device that was stuttering). Banding keeps the six layers but caps their COMBINED area at
   one screen. `left/right/height` are set here; JS only stamps each band's `top`. */
.boot-star-group {
    position: absolute;
    left: 0;
    right: 0;
    height: 16.667%;   /* 100% / 6 bands — keep in step with GROUP_COUNT in app-boot-loader.js */
    will-change: opacity;
    animation: boot-twinkle ease-in-out infinite;
}
.boot-star {
    position: absolute;
    border-radius: 50%;
    background: rgb(var(--star-rgb, 255, 255, 255));
}
.boot-star--alt    { background: rgb(var(--star-alt-rgb, 130, 185, 250)); }
.boot-star--bright { box-shadow: 0 0 5px var(--star-glow, rgba(180, 210, 255, 0.8)); }
/* opacity only — a `transform: scale()` here would pump the whole group, not single stars. */
@keyframes boot-twinkle {
    0%,100% { opacity: calc(var(--star-peak, 1) * .35); }
    50%     { opacity: var(--star-peak, 1); }
}
/* No filter: drop-shadow on the comet — an animated filter repaints on the main
   thread every frame and janks while Blazor WASM boots; transform+opacity only keeps
   it on the compositor thread. Streak takes the theme hue; the bright head glows. */
.boot-meteor {
    position: absolute;
    width: 88px; height: 2px;
    border-radius: 2px;
    background: linear-gradient(90deg,
        rgba(var(--loader-accent-rgb, 224,122,78), 0) 0%,
        rgba(var(--loader-accent-rgb, 224,122,78), 0.55) 65%,
        rgb(var(--meteor-tip-rgb, 255, 255, 255)) 100%);
    opacity: 0;
    will-change: transform, opacity;
    animation: boot-shoot linear infinite;
}
@keyframes boot-shoot {
    0%   { opacity: 0; transform: rotate(150deg) translateX(-140px); }
    7%   { opacity: 0; }
    10%  { opacity: 1; }
    20%  { opacity: 1; }
    26%  { opacity: 0; transform: rotate(150deg) translateX(560px); }
    100% { opacity: 0; transform: rotate(150deg) translateX(560px); }
}

.boot-stage {
    position: relative;
    z-index: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 26px;
}
.boot-orbits { position: relative; width: 300px; height: 300px; }
.boot-orbit {
    position: absolute;
    top: 0; left: 0;
    border-radius: 50%;
    border: 1px solid rgba(var(--accent-rgb, 224,122,78), 0.38);
    will-change: opacity;
    /* `backwards` fill-mode holds orbits at the 0% (dim) keyframe
       during the 3000ms pre-alignment window — avoids the abrupt
       opacity drop when the animation finally kicks in. */
    animation: boot-orbit-dim 12000ms ease-in-out 3000ms infinite backwards;
}
@keyframes boot-orbit-dim {
    0%, 100% { opacity: 0.23; }   /* dim while chips are aligned */
    50%      { opacity: 0.55; }   /* brighter while chips are spread */
}

.boot-core {
    position: absolute;
    top: 50%; left: 50%;
    transform: translate(-50%, -50%);
    text-align: center;
    line-height: 1.05;
    font-weight: 800;
    letter-spacing: 0.3px;
    z-index: 3;
    pointer-events: none;
}
.boot-core::before {
    content: "";
    position: absolute;
    top: 50%; left: 50%;
    width: 120px; height: 120px;
    transform: translate(-50%, -50%);
    background: radial-gradient(circle, rgba(var(--accent-rgb, 224,122,78), 0.32), transparent 65%);
    z-index: -1;
    animation: boot-pulse 2.4s ease-in-out infinite;
}
.boot-core .l1 { display: block; font-size: 22px; color: var(--text, #f4f1ec); }
.boot-core .l2 { display: block; font-size: 22px; color: var(--accent, #e07a4e); }
@keyframes boot-pulse {
    0%,100% { transform: translate(-50%,-50%) scale(0.92); opacity: 0.8; }
    50%     { transform: translate(-50%,-50%) scale(1.08); opacity: 1; }
}

/* Pivot is a 0×0 anchor at the orbit center (CX, CY). It rotates via
   CSS; the chip inside it sits at offset (--r, 0) and counter-rotates
   to stay upright. Both animations run on the compositor thread, so
   Blazor's main-thread WASM compile/startup can't starve them — the
   previous rAF-driven version visibly froze whenever Blazor was busy
   (Brave desktop was reproducing it). animation-duration and
   animation-delay are stamped per-chip by JS once after layout. */
.boot-pivot {
    position: absolute;
    top: 150px;
    left: 150px;
    width: 0;
    height: 0;
    will-change: transform;
    animation-name: boot-pivot-spin;
    animation-iteration-count: infinite;
    animation-timing-function: linear;
}
@keyframes boot-pivot-spin {
    from { transform: rotate(0deg); }
    to   { transform: rotate(360deg); }
}

.boot-word {
    position: absolute;
    left: 0; top: 0;
    padding: 5px 11px;
    border-radius: 999px;
    font-size: 14px;
    font-weight: 600;
    letter-spacing: 0.2px;
    white-space: nowrap;
    color: var(--accent-strong, #ec9069);
    background: rgba(var(--accent-rgb, 224,122,78), 0.12);
    border: 1px solid rgba(var(--accent-rgb, 224,122,78), 0.3);
    isolation: isolate;
    will-change: transform;
    z-index: 2;
    animation-name: boot-chip-spin;
    animation-iteration-count: infinite;
    animation-timing-function: linear;
}
@keyframes boot-chip-spin {
    from { transform: translate(var(--r, 0px), 0) translate(-50%, -50%) rotate(0deg); }
    to   { transform: translate(var(--r, 0px), 0) translate(-50%, -50%) rotate(-360deg); }
}
.boot-word::before {
    content: "";
    position: absolute;
    inset: -1px;
    border-radius: inherit;
    background: rgba(var(--accent-rgb, 224,122,78), 0.22);
    border: 1px solid rgba(var(--accent-rgb, 224,122,78), 0.55);
    opacity: 0;
    z-index: -1;
    pointer-events: none;
    will-change: opacity;
    /* Alignment glow flashes at every T-boundary (every 12000ms,
       starting from t = 3000ms — the moment chips first converge).
       Static opacity 0 above keeps glow off during the pre-alignment
       scatter; the 3000ms delay phases the flash with the chips. */
    animation: boot-orbit-glow 12000ms ease-in-out 3000ms infinite;
}
@keyframes boot-orbit-glow {
    0%, 100% { opacity: 1; }
    8%, 92%  { opacity: 0; }
}

.boot-text {
    font-size: 12.5px;
    font-weight: 500;
    letter-spacing: 0.4px;
    color: var(--text-secondary, #a4a09a);
    margin-top: 20px;
    animation: boot-soft 2.2s ease-in-out infinite;
}
@keyframes boot-soft  { 0%,100% { opacity: 0.55; } 50% { opacity: 0.85; } }

/* ── Still native-matching frame (html.lw-boot-static) ──────────────────────────
   Set by the MAUI host, which boots the whole SPA inside this WebView's own renderer.
   Measured there (Redmi Note 12 Pro, Android 14, CDP trace of the renderer): for the entire
   time this splash is up the main thread is 88% busy — 1358 ms of task time in a 1544 ms
   window, 436 ms of it EmbedderCallback (the .NET->WebView interop), with single blocks up to
   107 ms. The five seconds AFTER the splash measure 62 ms of task time: the load is not spread
   out, it is exactly under the splash.

   No animation survives that. A block of 107 ms at 60 Hz is six frames the compositor never
   gets, so the orbiting words teleport instead of moving — which reads as a broken app. And it
   is NOT the animation's own cost: the orbits are fully composited (verified over CDP:
   ActiveTransformAnimation, trace compositeFailed = 0) and the GPU idles at a 12 ms 90th
   percentile. Thinning the motion would not help; only removing it does, because a still frame
   cannot stutter.

   Motion was removed first (#1208) and the artwork kept — starfield and words laid out where
   the orbits would have converged them. That fixed the stutter but not the composition: with
   the animation gone the splash is only on screen ~1 s, which is not enough to read a starfield
   and a sentence, and it followed a native splash showing something else entirely. Two pictures
   in a row inside two seconds read as a flash.

   So this host now shows the NATIVE frame instead: the same comet on the same flat colour,
   dead centre, unchanged until Home replaces it. The markup carries no starfield or orbit
   nodes at all (see Mobile/wwwroot/index.html), so everything below is about the frame itself.
   The web hosts are untouched — their boot doesn't monopolise the renderer, their splash lives
   5-6 s, and the orbits have time to converge and be seen. */

/* Flat, and exactly the colour the native splash paints. The animated splash's two accent
   radial-gradients are NOT in the native frame, so leaving them on would put a visible wash
   over half the screen at the very moment the two frames are supposed to be identical.

   NOT var(--loader-bg): that token is per-theme (#0c1a35 under cool — which is the app's DEFAULT
   theme — #3a2c18 under sepia, #000000 under amoled), while MauiSplashScreen Color is one static
   value baked into each APK at build time. A themed loader background therefore disagrees with the
   native frame for most users, and the disagreement shows up as a full-screen colour jump at
   handoff. The native splash cannot follow the theme, so here the theme has to yield.

   --lw-native-splash is set by the HOST's index.html, because the two apps splash on different
   brand colours: the full app on the night sky (#0b0e15), LangOrbit Books on terracotta (#b8431f).
   The fallback keeps a host that forgets on the older of the two rather than on nothing.
   MobileShellRules compares each host's value against its own csproj so they can't drift apart. */
html.lw-boot-static .boot-loader {
    background: var(--lw-native-splash, #0b0e15);

    /* Land the comet on the SCREEN centre, which is where the native splash draws it. This
       WebView is not the screen: MainActivity pads the activity out of the system bars
       (SystemBarInsetPadding), so the viewport starts below the status bar and its own centre
       sits (top - bottom) / 2 lower. Measured on a Redmi Note 12 Pro: 35 CSS px of top inset,
       none at the bottom, and the comet landed exactly 48 physical px (= 17.5 CSS px at
       dpr 2.75) below the native one.

       Padding the OPPOSITE edge of each inset is what re-centres a flex box: padding-bottom
       equal to the top inset moves the content up by half of it. Written as both edges so a
       device with a bottom inset too (three-button navigation) gets (top - bottom) / 2, which
       is the real offset — halving the combined gap would over-correct there.

       --screen-inset-*, NOT --safe-*: this host zeroes the --safe-* tokens (the activity has
       already reserved the strips, so the page owes nothing), and zero correction here puts the
       comet back 48 px below the native one. What this rule needs is the screen's physical
       geometry, which is what --screen-inset-* is for. */
    padding-top: var(--screen-inset-bottom, 0px);
    padding-bottom: var(--screen-inset-top, 0px);
    box-sizing: border-box;
}

/* 108 px matches drawable-v31/maui_splash_image.xml, where MAUI boxes the splash icon at
   108x108 dp — and on this WebView (width=device-width) 1 CSS px is 1 dp. Sharing the source
   viewBox with Resources/Splash/splash.svg makes the two renderings the same picture, not
   merely a similar one. Android < 12 draws the icon at the 128 dp BaseSize instead, so there
   the handoff shifts the comet slightly; 12+ is the version this targets. */
.boot-comet {
    width: 108px;
    height: 108px;
    display: block;
}

/* The hint is for a boot that went WRONG. A normal one hands off in ~1 s, well before this
   fires, so the frame the user actually sees carries no text — like the native splash. If the
   boot hangs, the text arrives at 2.5 s and the frame stops looking dead. By then the main
   thread is idle (62 ms of task time across the five seconds after the splash), so a composited
   opacity fade costs nothing; running it any earlier would land in the busy window.

   Positioned off the centre line rather than in flow: the comet must not move when the text
   appears, or the one thing this frame promises — that nothing moves — breaks precisely when
   the user is already waiting. */
html.lw-boot-static .boot-text {
    position: absolute;
    left: 0;
    right: 0;
    /* 86 px below the comet, carrying the same inset correction the loader applies to it —
       an absolutely positioned box resolves 50% against the padding box, so it would otherwise
       stay on the viewport's centre line while the comet moved off it. Same reason as the
       loader for reading --screen-inset-* rather than --safe-*. */
    top: calc(50% + 86px + (var(--screen-inset-bottom, 0px) - var(--screen-inset-top, 0px)) / 2);
    margin-top: 0;
    text-align: center;
    opacity: 0;
    animation: boot-late-hint 240ms ease-in 2500ms forwards;
}
@keyframes boot-late-hint { to { opacity: 0.7; } }

/* Fading out over 250 ms while the thread is still busy produces a torn fade, which undoes the
   point of going static. Hand off in one frame instead. */
html.lw-boot-static .boot-loader { transition: none; }
