/* splash.css — full-screen loading screen.
 *
 * Loaded by BOTH the site (js/utils/splash.js) and the Splash Editor
 * (splash-poc.html), so what the editor shows is literally what ships.
 *
 * ── Theming ───────────────────────────────────────────────────────
 * Nothing here is a hardcoded colour. Every surface derives from the
 * active theme's chrome pair (--header-bg / --header-text) plus
 * --color-accent, so all eight themes work with no per-theme block.
 * The chrome pair is used rather than --color-bg/--color-text because
 * it is already a designed dark-surface / light-ink couple in every
 * theme — exactly what a full-bleed splash needs.
 *
 * ── Geometry ──────────────────────────────────────────────────────
 * The squares are flush by construction: for n squares centred on a
 * circle of diameter ⌀, the chord between neighbours is ⌀·sin(π/n), so
 * that IS the square's side. Change n and the side follows; the logo
 * takes whatever diameter is left inside the ring. CSS does the
 * trigonometry itself (sin() + pi), so it stays exact at every
 * viewport size with no JS resize handling.
 *
 * ── Where the numbers come from ───────────────────────────────────
 * Every tunable below reads `var(--sp-<name>-cfg, <default>)`. The
 * -cfg half is set on :root by assets/splash/splash-vars.css, which is
 * GENERATED from splash-config.json (scripts/build-splash-css.js, and
 * by the editor's Save). That file is a plain stylesheet in <head>, so
 * the saved design is in effect on the FIRST PAINT.
 *
 * This exists because the config also arrives over the network as JSON,
 * and that fetch measured ~330ms — a third of a second of the splash
 * wearing these fallbacks before restyling itself in place. A loading
 * screen that visibly changes design while you look at it is the exact
 * flicker the whole rewrite set out to remove. The JSON path still runs
 * (it carries non-CSS settings, and it is the source of truth if the
 * generated file is stale) — it just no longer has anything to change.
 *
 * The fallbacks are therefore a safety net for a missing/failed
 * stylesheet, not the design. Edit the design in the editor.
 */

/* ── Surfaces ─────────────────────────────────────────────────── */
.splash {
    /* Ink & canvas — swapped to the page palette when data-bg="page" */
    --sp-canvas: var(--header-bg, #1e293b);
    --sp-ink:    var(--header-text, #f8fafc);

    /* Lit-square accent, lifted toward the ink so it stays visible in
       themes whose accent sits close to their own header background
       (Nature: green on green; X22: orange on near-black). */
    --sp-lift:   var(--sp-lift-cfg, 35%);
    --sp-accent: color-mix(in srgb,
                    var(--color-accent, #2563eb) calc(100% - var(--sp-lift)),
                    var(--sp-ink) var(--sp-lift));

    /* The second (non-accent) square colour. Themes whose accent IS their
       ink — Vegas and Casino are both gold-on-gold — would otherwise show a
       single flat ring instead of an alternation. Shading this one toward
       the canvas restores a readable two-tone without leaving the palette. */
    --sp-alt:  var(--sp-alt-cfg, 0%);
    --sp-ink2: color-mix(in srgb,
                  var(--sp-ink) calc(100% - var(--sp-alt)),
                  var(--sp-canvas) var(--sp-alt));

    --sp-canvas-2: color-mix(in srgb, var(--sp-canvas) 92%, var(--sp-ink));
    /* Mixed toward the CANVAS, not toward transparent. These sit on a
       translucent surface, so an alpha-based "muted" compounds with the
       veil and collapses — measured at 1.74:1 against the scrim, i.e.
       unreadable. Blending against the canvas keeps them opaque and
       predictable while still reading as secondary. */
    --sp-muted:    color-mix(in srgb, var(--sp-ink) 78%, var(--sp-canvas));
    --sp-hair:     color-mix(in srgb, var(--sp-ink) 22%, var(--sp-canvas));
    --sp-off:      color-mix(in srgb, var(--sp-ink) 11%, transparent);
    --sp-glow:     color-mix(in srgb, var(--sp-accent) 34%, transparent);
    /* Lifted toward the ink for the same reason as the accent: a plain
       green "done" tick vanishes on Nature's green and Beige's brown. */
    --sp-done:     color-mix(in srgb, var(--color-success, #047857) 55%, var(--sp-ink) 45%);
}
.splash[data-bg="page"] {
    --sp-canvas: var(--color-bg, #e0e3ea);
    --sp-ink:    var(--color-text, #1a1a2e);
}

.splash {
    position: fixed;
    inset: 0;
    z-index: 9999;
    display: grid;
    place-items: center;
    overflow: hidden;
    color: var(--sp-ink);
    font-family: var(--font-main);
    /* Translucent, not opaque. On an internal navigation the View Transition
       is still holding a snapshot of the PREVIOUS page, so blurring through
       to it keeps the user anchored — they can see where they came from
       instead of staring at a slab. On a direct hit the page's own canvas
       (--color-bg) sits behind instead, so it never reads as hollow.
       --sp-veil is how much of the splash's own colour is laid over it.
       Deliberately low: the point is to read the page through it, so the
       tint only has to carry enough weight for the copy to stay legible
       (verified across all 8 themes — see the contrast note below). */
    --sp-veil: var(--sp-veil-cfg, 66%);
    background:
        radial-gradient(120% 90% at 50% 8%, var(--sp-glow) 0%, transparent 58%),
        linear-gradient(168deg,
            color-mix(in srgb, var(--sp-canvas)   var(--sp-veil), transparent) 0%,
            color-mix(in srgb, var(--sp-canvas-2) var(--sp-veil), transparent) 100%);
    /* The blur does the legibility work the tint no longer does: it destroys
       the high-frequency detail (text, table rules) that would otherwise
       fight the copy, while keeping the colour and shape of what's behind. */
    --sp-blur: var(--sp-blur-cfg, 22px);
    backdrop-filter: blur(var(--sp-blur)) saturate(125%);
    -webkit-backdrop-filter: blur(var(--sp-blur)) saturate(125%);
    transition: opacity .22s ease;
}

/* No backdrop-filter (older Safari, some Android): fall back to the fully
   opaque surface rather than letting content show through unblurred. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
    .splash { --sp-veil: 100%; }
}

/* Deferred reveal — the other half of the "don't flash a loader" rule.
 *
 * A loading screen that appears and disappears inside ~200ms reads as a
 * glitch, not as feedback. The convention is a pair of thresholds: a DELAY
 * before showing (if the work finishes first, the loader never appears at
 * all) and a MINIMUM display once it has (see MIN_VISIBLE_MS in splash.js).
 * We previously had only the minimum, which is exactly why fast navigations
 * flickered.
 *
 * The delay is applied only when arriving from our own site: the browser
 * keeps the previous page on screen until this one has something to paint,
 * so there is already something to look at and a loader would only interrupt
 * it. On a direct hit or a reload there is nothing behind the splash, so it
 * shows immediately — hiding it would just mean a blank screen.
 * The class is set by the inline script in each page's <head>.
 *
 * The head script ALSO holds this class for the duration of a cross-document
 * view transition, if one is ever running. There is no such transition today
 * (see the note at the top of layout.css for why it was removed), but that
 * guard is what stops the splash being captured in the transition's still
 * photograph — the bug that made one splash look like two. Keep it. */
:root.sp-defer .splash {
    opacity: 0;
    pointer-events: none;
}
/* No fade on the way in. The splash arrives already animating (the ring runs
   on CSS from the first paint), so a 0.22s fade-in would add a second, softer
   "rising" step on top of the transition that just ended — the very beat this
   is meant to remove. Fading OUT is still right; that is `.is-leaving`. */
:root.sp-defer .splash { transition: none; }

/* One loading message at a time.
   The canonical pattern is that a page ships an EMPTY shell and the splash is
   the only thing narrating the wait — index.html even hides its own header
   explicitly. Pages that predate the splash (analytics, admin) still INJECT a
   "Loading…" line from their renderer mid-flight, and with a transparent
   splash that line reads straight through it: two different progress messages
   stacked on top of each other. Rather than hunt every injection site, the
   placeholders are simply not painted while the splash owns the screen.
   `visibility`, not `display`, so nothing reflows when it comes back. */
:root.sp-active .loading { visibility: hidden; }

/* Grain — keeps the gradient from reading as flat CSS. */
.splash::after {
    content: '';
    position: absolute;
    inset: -20%;
    pointer-events: none;
    opacity: .45;
    mix-blend-mode: overlay;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23n)' opacity='.42'/%3E%3C/svg%3E");
}
/* Vignette — tinted with the theme's own ink, so it darkens light
   palettes and lifts dark ones instead of always smearing black. */
.splash::before {
    content: '';
    position: absolute;
    inset: 0;
    pointer-events: none;
    background: radial-gradient(70% 60% at 50% 45%,
                transparent 42%,
                color-mix(in srgb, var(--sp-ink) 13%, transparent) 100%);
}

.splash-core {
    position: relative;
    z-index: 2;
    /* With a translucent backdrop the copy no longer sits on a known solid
       colour. A shadow in the CANVAS colour (not black) keeps every theme's
       text anchored without introducing a tint that isn't in the palette. */
    text-shadow: 0 1px 3px color-mix(in srgb, var(--sp-canvas) 70%, transparent);
    isolation: isolate;
    display: grid;
    justify-items: center;
    gap: clamp(18px, 3.4vmin, 34px);
    padding: 5vmin 24px;
    text-align: center;
    animation: sp-enter .7s cubic-bezier(.2,.8,.25,1) both;
}
@keyframes sp-enter {
    from { opacity: 0; transform: translateY(14px) scale(.985); }
    to   { opacity: 1; transform: none; }
}

/* Local scrim. A low --sp-veil is what lets the page read through, but it
   also means the copy sits on an unpredictable mid-tone and stops being
   legible. Rather than raising the veil everywhere — which would undo the
   transparency — the tint is concentrated behind the content and falls off
   to nothing well before the edges. The page stays visible where there is
   nothing to read; the text keeps a known backdrop. */
.splash-core::before {
    content: '';
    position: absolute;
    /* Generous, and held near full strength out to ~70%: every piece of copy
       — including the actions and the folded "Technical details" at the
       bottom — has to sit inside the strong part. Measured: at a tighter
       falloff that summary dropped to 1.6:1 in Nature. */
    inset: -55% -120%;
    z-index: -1;
    pointer-events: none;
    border-radius: 50%;
    /* Wide and gently stepped. A tighter gradient was legible but read as a
       distinct dark ellipse sitting on the page; spread this far it reads as
       lighting rather than a shape, while still carrying the copy. */
    background: radial-gradient(ellipse 42% 48% at center,
        color-mix(in srgb, var(--sp-canvas) 84%, transparent)  0%,
        color-mix(in srgb, var(--sp-canvas) 78%, transparent) 38%,
        color-mix(in srgb, var(--sp-canvas) 58%, transparent) 58%,
        color-mix(in srgb, var(--sp-canvas) 26%, transparent) 76%,
        transparent 100%);
    filter: blur(26px);
}

/* ── Copy above the mark ──────────────────────────────────────── */
.sp-copy { display: grid; gap: 9px; max-width: 46ch; }

.sp-eyebrow {
    margin: 0;
    font-size: .66rem;
    font-weight: 700;
    font-style: normal;
    letter-spacing: .34em;
    text-transform: uppercase;
    color: var(--sp-muted);
}
.sp-eyebrow em { font-style: normal; }
.sp-eyebrow > span {
    display: inline-block;
    width: 5px; height: 5px;
    margin-inline-end: .8em;
    border-radius: 50%;
    background: var(--sp-accent);
    vertical-align: middle;
    animation: sp-blink 1.6s ease-in-out infinite;
}
@keyframes sp-blink { 0%,100% { opacity: .25 } 50% { opacity: 1 } }

/* The line that answers "what am I waiting for?".
   Two lines are reserved so a longer stage label never nudges the ring
   downward mid-load — nothing reads as broken faster than a splash that
   jumps while you watch it. */
.sp-stage {
    margin: 0;
    font-size: clamp(1.3rem, 3.2vmin, 1.85rem);
    font-weight: 600;
    letter-spacing: -.018em;
    line-height: 1.2;
    min-height: 2.4em;
    display: grid;
    align-content: center;
}
.sp-stage.is-swapping { animation: sp-swap .34s ease both; }
@keyframes sp-swap {
    0%   { opacity: 1; transform: none;               filter: blur(0); }
    45%  { opacity: 0; transform: translateY(-7px);   filter: blur(3px); }
    46%  { opacity: 0; transform: translateY(7px); }
    100% { opacity: 1; transform: none;               filter: blur(0); }
}
.sp-detail {
    margin: 0;
    font-size: .82rem;
    color: var(--sp-muted);
    font-variant-numeric: tabular-nums;
}
.sp-detail[hidden], .sp-steps[hidden] { display: none; }

/* ── The clock ring ───────────────────────────────────────────── */
.sp-ring {
    --n:      var(--sp-n-cfg,      24);   /* square count             */
    --tight:  var(--sp-tight-cfg,  1);    /* 1 = perfectly flush      */
    --scale:  var(--sp-scale-cfg,  1);    /* ring size multiplier     */
    --logo:   var(--sp-logo-cfg,   1);    /* logo size multiplier     */
    --corner: var(--sp-corner-cfg, 14);   /* corner radius, % of side */
    --pop:    var(--sp-pop-cfg,    1.1);  /* lit-square overshoot     */
    --cycle:  var(--sp-cycle-cfg,  3s);

    /* ⌀ of the circle the square CENTRES sit on */
    --ring: calc(clamp(175px, 36vmin, 265px) * var(--scale));
    /* Flush side length — the chord between neighbouring centres */
    --sq:   calc(var(--ring) * sin(pi / var(--n)) * var(--tight));
    /* Free diameter inside the ring (squares eat half a side per edge) */
    --hole: calc(var(--ring) - var(--ring) * sin(pi / var(--n)));
    /* One full revolution must finish inside --cycle, leaving a hold */
    --step: calc(var(--cycle) / (var(--n) + 10));

    /* The squares are CENTRED on --ring, so they stick out half a side
       past it. The box has to be the outer diameter or a low square count
       (big squares) overflows and collides with the chips below. The
       centre — which every square's transform is measured from — is
       unaffected, so the geometry above still holds. */
    --outer: calc(var(--ring) + var(--sq));

    position: relative;
    width:  var(--outer);
    height: var(--outer);
    display: grid;
    place-items: center;
}

.sq {
    position: absolute;
    top: 50%; left: 50%;
    width: var(--sq); height: var(--sq);
    margin: calc(var(--sq) / -2);
    border-radius: calc(var(--sq) * var(--corner) / 100);
    background: var(--sp-off);
    transform: rotate(var(--a)) translateY(calc(var(--ring) / -2));
    transform-origin: 50% 50%;
    /* Alternating accent / ink, applied only once lit */
    --lit: var(--sp-accent);
}
.sq:nth-child(even) { --lit: var(--sp-ink2); }

/* Indeterminate — each square lights on its own delay, all reset together */
.sp-ring[data-mode="indeterminate"] .sq {
    animation: sq-fill var(--cycle) linear infinite;
    animation-delay: calc(var(--i) * var(--step));
}
@keyframes sq-fill {
    0%   { background: var(--sp-off); box-shadow: none;
           transform: rotate(var(--a)) translateY(calc(var(--ring) / -2)) scale(.82); }
    3%   { background: var(--lit); box-shadow: 0 0 12px -4px var(--lit);
           transform: rotate(var(--a)) translateY(calc(var(--ring) / -2)) scale(var(--pop)); }
    9%   { background: var(--lit); box-shadow: 0 0 8px -4px var(--lit);
           transform: rotate(var(--a)) translateY(calc(var(--ring) / -2)) scale(1); }
    88%  { background: var(--lit); box-shadow: 0 0 8px -4px var(--lit);
           transform: rotate(var(--a)) translateY(calc(var(--ring) / -2)) scale(1); }
    100% { background: var(--sp-off); box-shadow: none;
           transform: rotate(var(--a)) translateY(calc(var(--ring) / -2)) scale(.82); }
}

/* Determinate — .is-lit is toggled to mirror real progress */
.sp-ring[data-mode="determinate"] .sq {
    transition: background .28s ease, box-shadow .28s ease, transform .28s cubic-bezier(.2,1.5,.4,1);
}
.sp-ring[data-mode="determinate"] .sq.is-lit {
    background: var(--lit);
    box-shadow: 0 0 8px -4px var(--lit);
    transform: rotate(var(--a)) translateY(calc(var(--ring) / -2)) scale(var(--pop));
}

/* Closing the ring on the way out.
 *
 * The last thing the user should see is a COMPLETE circle, not the arbitrary
 * arc the progress happened to reach. The obvious way — add .is-lit to every
 * square at once — lights up to twenty of them on a single frame, each also
 * scaling up, so the ring detonates and the exit animation starts on top of
 * the flash. Measured on a real navigation: 4 lit, then 24 on the next frame.
 *
 * Doing it from JS one square at a time does not work either: the page is busy
 * rendering at exactly this moment (a dashboard blocks the main thread for
 * ~2s), so a per-frame loop is starved and the ring leaves half full.
 *
 * So the stagger is declarative. JS adds one class and lights everything; the
 * per-square transition-delay makes them arrive in sequence, and CSS keeps that
 * promise whether or not the main thread is free.
 *
 * THE LAST SQUARE MUST FINISH BEFORE THE SPLASH IS GONE. The ring closing is a
 * promise that the work completed; a ring removed at 19/24 says the opposite.
 * The budget is easy to get wrong, and was: the note here used to read "~10ms
 * apart = the full ring closes in ~240ms", which counted the delay span (23 ×
 * 10ms) and forgot that the last square then still has its own transition to
 * run. The real figure was 230 + 280 = 510ms — longer than the whole exit, so
 * the ring was carried off screen before it ever reached 100%.
 *
 * Both halves count:
 *     last square done = (n - 1) × step + duration
 *                      = 23 × 6ms + 200ms ≈ 340ms   (n = 24)
 * against CLOSE_MS = 240ms in splash.js, which is when the fade starts. So the
 * ring completes ~100ms into a 300ms fade — visibly, at roughly two-thirds
 * opacity, which is the intent: seen at 100%, just slightly faded. Change
 * either number and re-check that sum. */
.splash.is-closing .sp-ring[data-mode="determinate"] .sq {
    /* --sp-close-scale is 1 everywhere except the editor's slow-motion
       preview, which stretches it so a ~250ms event can actually be watched. */
    transition-delay: calc(var(--i) * 6ms * var(--sp-close-scale, 1));
    transition-duration: calc(.2s * var(--sp-close-scale, 1));
}

/* ── Logo at the centre ───────────────────────────────────────── */
.sp-logo {
    position: relative;
    width: calc(var(--hole) * .88 * var(--logo));
    aspect-ratio: 1;
    display: grid;
    place-items: center;
}
.sp-logo img {
    width: 100%; height: 100%;
    object-fit: contain;
    border-radius: 50%;
    animation: sp-breathe 3.4s ease-in-out infinite;
}
@keyframes sp-breathe {
    0%,100% { transform: scale(1);     filter: drop-shadow(0 6px 20px rgba(0,0,0,.35)); }
    50%     { transform: scale(1.035); filter: drop-shadow(0 10px 30px rgba(0,0,0,.45)); }
}
.sp-logo::before {
    content: '';
    position: absolute;
    inset: 4%;
    border-radius: 50%;
    background: radial-gradient(circle, var(--sp-glow) 0%, transparent 68%);
    animation: sp-halo 3.4s ease-in-out infinite;
}
@keyframes sp-halo {
    0%,100% { opacity: .5; transform: scale(1); }
    50%     { opacity: .9; transform: scale(1.16); }
}

/* ── Step checklist ───────────────────────────────────────────── */
.sp-steps {
    list-style: none;
    margin: 0; padding: 0;
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 6px 8px;
    /* Wide enough for all six chips on one line at the default stage list —
       a lone "Render" wrapping to a second row reads as a layout bug. */
    max-width: 66ch;
}
.sp-steps li {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 5px 11px 5px 8px;
    border: 1px solid var(--sp-hair);
    border-radius: var(--radius-full, 9999px);
    font-size: .72rem;
    color: var(--sp-muted);
    opacity: .55;
    transition: color .3s ease, border-color .3s ease, background .3s ease, opacity .3s ease;
}
.sp-steps li .dot {
    width: 6px; height: 6px;
    border-radius: 50%;
    background: currentColor;
    flex: none;
    transition: background .3s ease, box-shadow .3s ease;
}
.sp-steps li.is-active {
    opacity: 1;
    color: var(--sp-ink);
    border-color: color-mix(in srgb, var(--sp-accent) 55%, transparent);
    background: color-mix(in srgb, var(--sp-accent) 14%, transparent);
}
.sp-steps li.is-active .dot { background: var(--sp-accent); box-shadow: 0 0 8px var(--sp-accent); }
.sp-steps li.is-done { opacity: 1; color: var(--sp-done); border-color: color-mix(in srgb, var(--sp-done) 40%, transparent); }
.sp-steps li.is-done .dot { background: var(--sp-done); }

/* ── Slow-network escape hatch ────────────────────────────────── */
.sp-slow {
    margin: 0;
    font-size: .78rem;
    color: var(--sp-muted);
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    opacity: 0;
    transform: translateY(6px);
    pointer-events: none;
    transition: opacity .45s ease, transform .45s ease;
}
.sp-slow.is-shown { opacity: 1; transform: none; pointer-events: auto; }
.sp-slow button {
    font: inherit;
    color: var(--sp-ink);
    background: transparent;
    border: 1px solid var(--sp-hair);
    border-radius: var(--radius-full, 9999px);
    padding: 4px 12px;
    cursor: pointer;
    transition: border-color .2s ease, background .2s ease;
}
.sp-slow button:hover {
    border-color: var(--sp-accent);
    background: color-mix(in srgb, var(--sp-accent) 16%, transparent);
}

/* ── Exit ─────────────────────────────────────────────────────────
   .3s, not the .55s this started at. The splash is no longer only a
   page-load screen: restartSplash() also runs it for in-page refetches
   (analytics' filters, the admin's section switches), and there the exit
   is a tail on an action the user took a moment ago rather than the end
   of a page load. Measured end-to-end, the old pair (.55s here plus the
   260ms ring-close wait in splash.js) left the screen covered for ~810ms
   after the data was already in. This halves that to ~480ms.

   Not taken to zero on purpose: an animation that is cut off mid-way
   reads as broken, which is the failure this whole mechanism exists to
   avoid. Shorten it, don't remove it. */
.splash.is-leaving { animation: sp-leave .3s cubic-bezier(.6,0,.35,1) forwards; }
@keyframes sp-leave {
    to { opacity: 0; transform: scale(1.045); filter: blur(6px); }
}

/* ══ Error screen ═══════════════════════════════════════════════
   Same surface, typography and theme derivation as the splash, so a
   failure reads as the same product rather than a raw stack trace. Two
   deliberate differences:

   · NO animation. The splash animates because you are waiting; here
     nothing is coming, so movement would be a lie.
   · The ring stays UNLIT. That is the message, visually: the circle
     never filled. The centre carries a no-entry mark instead of the logo.

   Built by js/utils/errorScreen.js. Unlike the splash this needs no
   static markup — by the time an error exists the page has long since
   painted, so there is no first-frame constraint. */
/* Fills the whole content area — everything except the site chrome. An error
   is a dead end, so the navigation has to stay reachable; but boxing the
   message into a card left a band of page background around it, which reads
   as an unrelated panel rather than the state of the page.
   The insets are measured from the live sidebar/top bar by errorScreen.js,
   not hardcoded, so it stays correct when the sidebar collapses to a rail or
   to the mobile top bar. */
.splash.is-error {
    position: fixed;
    inset: var(--sp-chrome-top, 0) 0 0 var(--sp-chrome-start, 0);
    inset-inline-start: var(--sp-chrome-start, 0);
    inset-inline-end: 0;
    /* Below the sidebar (z 50) and top bar (z 110) so both stay clickable. */
    z-index: 40;
    border-radius: 0;
}

.splash.is-error .sp-ring { --pop: 1; }
.splash.is-error .sq {
    animation: none !important;
    background: var(--sp-off);
    box-shadow: none;
}

.sp-mark {
    position: relative;
    width: calc(var(--hole) * .62);
    aspect-ratio: 1;
    display: grid;
    place-items: center;
    color: var(--sp-error);
}
.sp-mark svg { width: 100%; height: 100%; display: block; }

.splash.is-error {
    /* Lifted toward the ink for the same reason as the accent — a plain red
       disappears on Vegas's and Casino's dark reds. */
    --sp-error: color-mix(in srgb, var(--color-danger, #b91c1c) 62%, var(--sp-ink) 38%);
    /* Cool the backdrop's accent glow into the error hue so the whole
       screen agrees with the mark at its centre. */
    --sp-glow: color-mix(in srgb, var(--sp-error) 22%, transparent);
}
.splash.is-error .splash-core { animation: none; }
.splash.is-error .sp-eyebrow > span {
    animation: none;
    background: var(--sp-error);
}
.splash.is-error .sp-stage { min-height: 0; }

/* Technical detail — present for debugging, folded away so the headline
   stays the message. */
.sp-tech {
    max-width: 62ch;
    font-size: .74rem;
    color: var(--sp-muted);
    text-align: start;
}
.sp-tech summary {
    cursor: pointer;
    text-align: center;
    list-style: none;
    padding: 3px 10px;
    border-radius: var(--radius-full, 9999px);
    border: 1px solid var(--sp-hair);
    /* Its own backdrop rather than relying on the scrim: it sits at the
       bottom of the content where the scrim has fallen off, and measured
       3.4:1 against it in Nature. This pins it to a known surface. */
    background: color-mix(in srgb, var(--sp-canvas) 62%, transparent);
    display: inline-block;
    transition: border-color .2s ease;
}
.sp-tech summary::-webkit-details-marker { display: none; }
.sp-tech summary:hover { border-color: var(--sp-error); }
.sp-tech code {
    display: block;
    margin-top: 10px;
    padding: 10px 12px;
    border-radius: var(--radius-sm, 6px);
    background: color-mix(in srgb, var(--sp-ink) 7%, transparent);
    font-family: var(--font-mono, monospace);
    font-size: .72rem;
    line-height: 1.5;
    color: var(--sp-ink);
    overflow-wrap: anywhere;
}

.sp-actions { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; }
.sp-actions > * {
    font: inherit;
    font-size: .82rem;
    padding: 8px 18px;
    border-radius: var(--radius-full, 9999px);
    border: 1px solid var(--sp-hair);
    /* Same reasoning as .sp-tech summary — a secondary action must not
       depend on where the scrim happens to land. */
    background: color-mix(in srgb, var(--sp-canvas) 62%, transparent);
    color: var(--sp-ink);
    cursor: pointer;
    text-decoration: none;
    transition: border-color .2s ease, background .2s ease;
}
.sp-actions > *:hover {
    border-color: var(--sp-accent);
    background: color-mix(in srgb, var(--sp-accent) 14%, transparent);
}
.sp-actions .is-primary {
    background: var(--sp-accent);
    border-color: transparent;
    color: var(--sp-canvas);
    font-weight: 600;
}
.sp-actions .is-primary:hover {
    background: color-mix(in srgb, var(--sp-accent) 85%, var(--sp-ink));
}

/* ── Inline (one section failed, page is fine) ────────────────── */
.sp-inline-error {
    display: flex;
    align-items: flex-start;
    gap: 12px;
    padding: 14px 16px;
    margin: var(--space-sm, 8px) 0;
    border: 1px solid color-mix(in srgb, var(--color-danger, #b91c1c) 35%, transparent);
    border-radius: var(--radius-md, 10px);
    background: color-mix(in srgb, var(--color-danger, #b91c1c) 7%, transparent);
    color: var(--color-text, #1a1a2e);
    /* Standalone: unlike .splash it has no --sp-* context of its own, so the
       folded <details> below borrows the page palette instead. */
    --sp-muted: var(--color-text-muted, #6b6d84);
    --sp-hair:  var(--color-border, #d4d7df);
    --sp-ink:   var(--color-text, #1a1a2e);
    --sp-error: var(--color-danger, #b91c1c);
}
.sp-inline-mark {
    flex: none;
    width: 22px; height: 22px;
    color: var(--color-danger, #b91c1c);
    margin-top: 1px;
}
.sp-inline-mark svg { width: 100%; height: 100%; display: block; }
.sp-inline-title { margin: 0 0 6px; font-size: .9rem; font-weight: 600; }
.sp-inline-error .sp-tech { max-width: none; }
.sp-inline-error .sp-tech summary { text-align: start; }

/* ── Miniature (theme gallery in the editor) ──────────────────── */
.splash.is-mini {
    position: absolute;
    inset: 0;
    /* Drop out of the 9999 layer so the editor's tile label stays on top */
    z-index: 1;
}
.splash.is-mini .sp-ring   { --ring: calc(96px * var(--scale)); }
.splash.is-mini .splash-core { gap: 9px; padding: 10px; }
.splash.is-mini .sp-stage  { font-size: .78rem; min-height: 2.4em; }
/* Force the chips onto two tidy rows instead of one long wrapped run */
.splash.is-mini .sp-steps  { max-width: 25ch; }
.splash.is-mini .sp-detail { font-size: .62rem; }
.splash.is-mini .sp-eyebrow { font-size: .5rem; letter-spacing: .26em; }
.splash.is-mini .sp-steps li { font-size: .55rem; padding: 2px 7px 2px 5px; gap: 4px; }
.splash.is-mini .sp-steps li .dot { width: 4px; height: 4px; }
.splash.is-mini .sp-slow   { display: none; }

@media (prefers-reduced-motion: reduce) {
    .sp-ring[data-mode="indeterminate"] .sq { animation: none; background: var(--lit); box-shadow: none; }
    .sp-logo img, .sp-logo::before, .sp-eyebrow > span { animation: none; }
    .splash-core, .sp-stage.is-swapping { animation: none; }
    .splash.is-leaving { animation: none; opacity: 0; }
}
