/* ── Phase 0.9.0 type system (see DESIGN.md "Typography") ──
   Three typographic registers, kept distinct so each one signals its
   own role:
     * --io-font-display — Syne Mono. The brand voice. Used for the
       wordmark and for headlines / sub-headlines (h1–h6).
     * --io-font-body — Josefin Sans. The reading voice. Used for body
       copy, lede paragraphs and lesson explanations.
     * --io-font-var — EB Garamond italic. The math voice. Used for
       single-letter variables (e, a, v, r, …) via the `.io-var` class
       and for the Kepler legend / HUD parameter glyphs that already
       reached for an italic-serif treatment. */
:root {
    --io-font-display: 'Syne Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    --io-font-body: 'Josefin Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
    --io-font-var: 'EB Garamond', 'Georgia', 'Times New Roman', serif;
}

/* ── Theme colours: Hearth (v1.0.0, see wwwroot/design/theme-concepts/) ──
   Hearth warms the dark itself: a plum-umber base a few steps lighter
   than the old blue-black #030308, with warm glass and warm text.
   Every colour that belongs to the "room" rather than to the physics
   lives here, so the theme can change in one place. The `-rgb` tokens
   are bare triplets for use with an alpha, e.g.
   `rgba(var(--io-line-rgb), 0.12)`. The TypeScript side reads the same
   values from `src/IntuitiveOrbit.Simulations/src/core/palette.ts`; a
   Vitest test keeps the two in step.

   Accents are not theme colours and stay literal: cyan `#00ccff` /
   `rgba(0,200,255,…)` marks what you can change or what is selected,
   and solar orange `rgba(255,200,80,…)` marks the Sun. Hearth keeps the
   cyan subtitles and the cyan button glass too. */
:root {
    --io-bg: #17111a;                    /* page, canvas and 3D scene base */
    --io-bg-rgb: 23, 17, 26;             /* panel glass, text halos */
    --io-surface-hi-rgb: 40, 27, 38;     /* lit corner of the info panel */
    --io-line-rgb: 255, 195, 160;        /* rims, hairlines, dividers, tracks */
    --io-line-bright-rgb: 255, 210, 175; /* emphasised rims */
    --io-text-strong: #fff7ee;           /* titles */
    --io-text-rgb: 235, 215, 200;        /* labels and descriptions */
    --io-text-soft-rgb: 240, 225, 210;   /* quotes, button text */
    --io-text-bright-rgb: 250, 240, 230; /* guide text, legend names */
    --io-text-dim-rgb: 230, 205, 185;    /* HUD read-outs */
    --io-subtitle-rgb: 0, 200, 255;      /* lesson subtitles, panel titles */
    --io-glass-rgb: 140, 215, 255;       /* button glass, top of gradient */
    --io-glass-lo-rgb: 100, 190, 250;    /* button glass, bottom of gradient */
    --io-glass-hi-rgb: 220, 240, 255;    /* button inner highlight */
    --io-glass-ring-rgb: 150, 215, 255;  /* button inner ring */
    --io-glow-amber-rgb: 255, 150, 80;   /* backdrop glow, low on the right */
    --io-glow-rose-rgb: 200, 90, 130;    /* backdrop glow, high on the left */
    --io-glow-haze-rgb: 255, 200, 150;   /* backdrop haze, middle; stage glows */
    /* The room behind every scene: the base and three soft glows. The 2D
       and 3D renderers paint the same backdrop (`paintBackdrop` in
       palette.ts), so a page without a canvas matches one with. */
    --io-backdrop:
        radial-gradient(circle 62vw at 86% 92%, rgba(var(--io-glow-amber-rgb), 0.1), rgba(var(--io-glow-amber-rgb), 0.045) 50%, rgba(var(--io-glow-amber-rgb), 0)),
        radial-gradient(circle 50vw at 10% 8%, rgba(var(--io-glow-rose-rgb), 0.08), rgba(var(--io-glow-rose-rgb), 0.035) 50%, rgba(var(--io-glow-rose-rgb), 0)),
        radial-gradient(circle 55vw at 50% 45%, rgba(var(--io-glow-haze-rgb), 0.035), rgba(var(--io-glow-haze-rgb), 0)),
        var(--io-bg);
}

html, body {
    margin: 0;
    padding: 0;
    font-family: var(--io-font-body);
}

/* All headlines and sub-headlines render in Syne Mono — the same face
   as the wordmark, so headline weight unifies the brand voice across
   pages. Per-page rules can still override `font-size`, `font-weight`
   and `letter-spacing`; only the family is locked here. */
h1, h2, h3, h4, h5, h6 {
    font-family: var(--io-font-display);
}

/* Math-style variable glyph. Use as `<span class="io-var">e</span>` in
   prose, or apply to existing labels (legend symbols, HUD parameter
   values) so a single letter reads as "this is a variable" instead of
   a stray character. Italic is intentional — it matches the textbook
   convention for scalars. */
.io-var {
    font-family: var(--io-font-var);
    font-style: italic;
    font-feature-settings: "kern" 1, "liga" 0;
}

/* ── The frontpage and Discover sky ──────────────────────────────────
   One Milky Way behind both pages and the frontpage's three bodies over
   them, drawn by `src/hero/heroSky.ts` in the simulations bundle on two
   canvases in MainLayout. They only show on a page whose root has
   `data-sky`. The bodies sit over the page and under the breadcrumb and
   the other chrome (z-index 900 and up). */
.io-sky,
.io-bodies {
    position: fixed;
    inset: 0;
    width: 100%;
    height: 100%;
    display: none;
    pointer-events: none;
}
.io-sky { z-index: 0; background: var(--io-bg); }
.io-bodies { z-index: 5; }
body:has([data-sky]) .io-sky,
body:has([data-sky]) .io-bodies { display: block; }
/* heroSky.ts sets this for one style pass while it puts `data-arriving`
   back after a page swap, so Discover's parts start hidden instead of
   fading out. */
.io-bodies[data-arriving="instant"] ~ main *,
.io-bodies[data-arriving="instant"] ~ main *::before,
.io-bodies[data-arriving="instant"] ~ main *::after { transition: none !important; }

/* ── Scrollbars (viewport + any scrollable pane) ───────────────────
   Default OS scrollbars look out of place against the deep-space
   palette — a bright rectangle on the right, a chunky bar along the
   bottom. We style them to be thin, dim, and tinted with the site's
   cyan accent so they recede into the background and only surface
   subtly when the user actually needs to scroll. */
html {
    /* Firefox */
    scrollbar-width: thin;
    scrollbar-color: rgba(var(--io-line-rgb), 0.22) transparent;
}
/* Chromium / WebKit */
::-webkit-scrollbar {
    width: 8px;
    height: 8px;
}
::-webkit-scrollbar-track {
    background: transparent;
}
::-webkit-scrollbar-thumb {
    background: rgba(var(--io-line-rgb), 0.18);
    border-radius: 4px;
    border: 2px solid transparent;
    background-clip: padding-box;
}
::-webkit-scrollbar-thumb:hover {
    background: rgba(var(--io-line-rgb), 0.32);
    background-clip: padding-box;
}
::-webkit-scrollbar-corner {
    background: transparent;
}

a, .btn-link {
    color: #006bb7;
}

.btn-primary {
    color: #fff;
    background-color: #1b6ec2;
    border-color: #1861ac;
}

.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
  box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
}

.content {
    padding-top: 1.1rem;
}

/* A lesson page fills the window edge to edge, so its article drops all
   padding, including the sides that MainLayout.razor.css adds from 641 px
   up (hence !important). */
.content:has(.lesson-page) {
    padding: 0 !important;
}

h1:focus {
    outline: none;
}

.valid.modified:not([type=checkbox]) {
    outline: 1px solid #26b050;
}

.invalid {
    outline: 1px solid #e50000;
}

.validation-message {
    color: #e50000;
}

.blazor-error-boundary {
    background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
    padding: 1rem 1rem 1rem 3.7rem;
    color: white;
}

    .blazor-error-boundary::after {
        content: "An error has occurred."
    }

.darker-border-checkbox.form-check-input {
    border-color: #929292;
}

.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder {
    color: var(--bs-secondary-color);
    text-align: end;
}

.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder {
    text-align: start;
}

/* ═══════════════════════════════════════════════════════════════
   Shared styles for Kepler lesson pages (.kl-*)
   See Web.Client Pages/Lessons/KeplerFirstLaw.razor and KeplerSecondLaw.razor
   ═══════════════════════════════════════════════════════════════ */

/* ── Top-left info panel ──
   The `.kl-info` container has two visual states, toggled via the
   `.kl-info-intro-mode` class:
     * Resting (default)    — narrow box at top-left, holds the title +
       Law of Ellipses quote + legend.
     * Intro mode           — wide box in the bottom-left, sitting to the
       left of the centred e/a/b/v/r controls panel. Holds only the
       current step's explanation text (content swaps per step).
   The morph between the two states plays as the intro's closing beat:
   when the guide finishes, `.kl-info-intro-mode` is removed and the CSS
   transitions below glide the panel from the bottom-left where the
   explanations lived, to the top-left corner where it lives at rest. */
.kl-info {
    /* `top: 38px` clears the discrete breadcrumb bar (Components/Shared/
       Breadcrumb.razor) which is fixed at top: 12px + 18px logo height.
       Before the breadcrumb existed, this panel sat at top: 14px and
       opened with a small `← Discover` link (`.kl-back`) above the
       title. The breadcrumb now owns that ancestor nav, so the link is
       gone and the panel slides down by ~24px to give the breadcrumb
       its own strip across the very top of the viewport.
       It also ends 16px above the bottom bar's reach (`--kl-bottom-reach`,
       see `.kl-info-intro-mode`), and scrolls, where the bar is tall and
       the window short, as Kepler 1's on a tablet: 38px above it, 32px of
       padding and border round the box max-height sets, and 16px below. */
    position: absolute; top: 38px; left: 14px; z-index: 10;
    width: 288px; overflow-y: auto;
    max-height: min(calc(100% - 124px), calc(100% - 86px - var(--kl-bottom-reach, 0px)));
    padding: 14px 16px 16px;
    background: linear-gradient(160deg, rgba(var(--io-surface-hi-rgb), 0.88) 0%, rgba(var(--io-bg-rgb), 0.88) 100%);
    border: 1px solid rgba(var(--io-line-rgb), 0.12);
    border-radius: 10px;
    -webkit-backdrop-filter: blur(14px);
    backdrop-filter: blur(14px);
    box-shadow: 0 4px 24px rgba(0,0,0,0.4), inset 0 1px 0 rgba(255,255,255,0.04);
    transform: translate(0, 0);
    transition:
        transform 1100ms cubic-bezier(0.65, 0.02, 0.25, 1),
        width    1100ms cubic-bezier(0.65, 0.02, 0.25, 1),
        padding  1100ms cubic-bezier(0.65, 0.02, 0.25, 1),
        border-color 700ms ease-out;
    will-change: transform, width;
}

/* While the panel is morphing between intro and resting positions, drop the
   most expensive composited effects (backdrop blur, layered + inset shadow)
   so the transform/width/padding transition stays in the GPU compositor.
   The blur and full shadow snap back when `.kl-info-morphing` is removed.
   `contain: layout paint` is scoped here (rather than the base rule) so the
   width+padding transition can't dirty the rest of the document each frame,
   while avoiding mobile browsers misclipping the panel when it is static. */
.kl-info.kl-info-morphing {
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    box-shadow: 0 4px 12px rgba(0,0,0,0.4);
    overflow: hidden;
    contain: layout paint;
}

/* Intro-mode position/size. We keep `top:38/left:14` from the base rule
   and morph via `transform` + `width` so the transition back to resting
   is purely interpolated transform/width (no layout thrash, no
   top/left `auto` pitfalls).

   translate's `100%` refers to the element's own height, so the
   vertical expression lands the panel's bottom 200px above the viewport
   bottom, which clears the centred `.kl-bottom` (20px up, up to ~160px
   tall). Where the bar reaches higher (Kepler 1's planet grid, a touch
   screen's 44px buttons, a HUD moved above the controls), the panel sits
   16px above it instead: `--kl-bottom-reach` is measured by
   `src/interop/bottomBarFit.ts`, so the Next button is never under the
   controls. Horizontally the box sits flush against `left: 20px` (translateX
   compensates for the base 14px) and its width clamps between 380px and
   720px, scaling with the viewport so it always leaves room for the
   centred controls panel. */
.kl-info.kl-info-intro-mode {
    width: clamp(380px, calc(50vw - 260px), 720px);
    padding: 20px 24px 22px;
    transform: translate(6px, calc(100vh - 38px - max(200px, var(--kl-bottom-reach, 0px) + 16px) - 100%));
    border-color: rgba(var(--io-line-bright-rgb), 0.22);
    overflow: visible;
    max-height: none;
}

/* The single caption text shown inside `.kl-info` during the intro.
   Larger than the resting panel's body copy so it reads well from the
   bottom area. Each new step gets a fresh `@key`, so Blazor replaces
   this element on advance and the fade-in animation plays again. */
.kl-info-intro-text {
    font-size: 16px;
    line-height: 1.6;
    color: rgba(var(--io-text-bright-rgb), 0.96);
    margin: 0;
}
.kl-info-intro-text em { color: rgba(var(--io-text-soft-rgb), 0.9); font-style: italic; }
.kl-info-intro-text strong { color: var(--io-text-strong); font-weight: 600; }
.kl-info-intro-fade {
    animation: kl-info-intro-fade 520ms cubic-bezier(0.2, 0.7, 0.2, 1) both;
}
@keyframes kl-info-intro-fade {
    0%   { opacity: 0; transform: translateY(6px); filter: blur(3px); }
    60%  { opacity: 1; filter: blur(0); }
    100% { opacity: 1; transform: translateY(0); filter: blur(0); }
}

/* Lagrange intro step: the Sun, Earth's orbit and the five points at a
   glance, before the storm. Orange = unstable points (L1–L3), cyan =
   stable (L4/L5), as in the 3D system view. */
.lp-intro-diagram {
    display: block;
    width: 100%;
    max-width: 260px;
    margin: 10px auto 2px;
    animation: kl-info-intro-fade 520ms cubic-bezier(0.2, 0.7, 0.2, 1) both;
}
.lp-intro-diagram .lp-orbit { fill: none; stroke: rgba(79,195,247,0.4); stroke-width: 1; stroke-dasharray: 3 3; }
.lp-intro-diagram .lp-sun { fill: #ffaa00; filter: drop-shadow(0 0 6px rgba(255,170,0,0.65)); }
.lp-intro-diagram .lp-earth { fill: #4fc3f7; }
.lp-intro-diagram .lp-unstable { fill: #ffaa00; }
.lp-intro-diagram .lp-stable { fill: #4fc3f7; }
.lp-intro-diagram text { fill: rgba(var(--io-text-soft-rgb), 0.8); font: 10px var(--io-font-body); }
.lp-intro-diagram text.lp-key { fill: #ffcc66; font-weight: 600; }

/* Flash-of-inspiration reveal for the lesson title — a proper bright-idea
   pop: blinding white flash, a scale punch, then a long luminous settle to
   a bright cyan-tinted resting glow. Applied on first post-intro render
   via the .kl-title-flash class. */
.kl-title-flash {
    animation: kl-title-flash 2200ms cubic-bezier(0.1, 0.7, 0.2, 1) both;
    will-change: transform, opacity;
}
/* Text-shadow blur radii > ~80px and `filter: brightness()` are CPU-paint
   heavy and were previously animating in lock-step with the panel morph,
   compounding jank. Brightness is dropped (the text-shadow stack already
   carries the bloom) and the largest blur radii are trimmed; the visible
   peak still reads as a flash. */
@keyframes kl-title-flash {
    0%   { color: #fff; opacity: 0; transform: scale(0.92); letter-spacing: 0.2px;
           text-shadow: 0 0 0 rgba(255,255,255,0); }
    8%   { color: #fff; opacity: 1; transform: scale(1.12); letter-spacing: 2.5px;
           text-shadow:
               0 0 12px rgba(255,255,255,1),
               0 0 36px rgba(255,255,255,0.95),
               0 0 80px rgba(160,215,255,0.85); }
    22%  { color: #fff; transform: scale(1.06); letter-spacing: 1.4px;
           text-shadow:
               0 0 8px  rgba(255,255,255,0.95),
               0 0 24px rgba(220,240,255,0.8),
               0 0 70px rgba(0,180,255,0.6); }
    55%  { color: #fff; transform: scale(1.01); letter-spacing: 0.5px;
           text-shadow:
               0 0 6px  rgba(255,255,255,0.7),
               0 0 18px rgba(180,220,255,0.55),
               0 0 40px rgba(0,200,255,0.4); }
    100% { color: #fff; transform: scale(1); letter-spacing: 0.2px;
           text-shadow:
               0 0 4px  rgba(255,255,255,0.55),
               0 0 10px rgba(180,220,255,0.4),
               0 0 20px rgba(0,200,255,0.25); }
}
.kl-info::-webkit-scrollbar { width: 4px; }
.kl-info::-webkit-scrollbar-thumb { background: rgba(var(--io-line-rgb), 0.15); border-radius: 2px; }

/* `.kl-back` was the per-page "← Discover" link in the top-left of every
   law's info panel. The discrete breadcrumb bar (Components/Shared/
   Breadcrumb.razor) now owns ancestor navigation site-wide, so the link
   was removed from each lesson's markup and the rule deleted with it.
   The kl-title sits at the top of the info panel directly. */

.kl-title {
    color: var(--io-text-strong); font-size: 18px; font-weight: 600;
    margin: 0; line-height: 1.2;
    letter-spacing: 0.2px;
}
.kl-subtitle {
    color: rgba(var(--io-subtitle-rgb), 0.7); font-size: 11px; font-weight: 500;
    margin: 3px 0 14px;
    letter-spacing: 1.4px; text-transform: uppercase;
    font-family: var(--io-font-display);
}

.kl-quote-box {
    border-left: 2px solid rgba(var(--io-line-rgb), 0.35);
    padding: 8px 10px; margin: 0 0 16px;
    background: linear-gradient(90deg, rgba(var(--io-line-rgb), 0.06), rgba(var(--io-line-rgb), 0));
    border-radius: 0 4px 4px 0;
}
.kl-quote {
    font-size: 12px; font-style: italic;
    color: rgba(var(--io-text-soft-rgb), 0.7);
    margin: 0; line-height: 1.6;
}

/* Legend — the per-law fact list. Each entry has a small italic
   cyan/gold variable glyph (matching .io-var) on the left and a
   name + description on the right. Items are separated by a soft
   horizontal cyan-fade hairline (echoing .kl-quote-box's gradient
   accent) instead of a flat one-pixel rule, so the divider reads
   as part of the same panel language rather than a hard boundary. */
.kl-legend { list-style: none; padding: 0; margin: 0; }
.kl-legend-item {
    display: grid;
    grid-template-columns: 22px 1fr;
    column-gap: 12px;
    align-items: baseline;
    padding: 10px 2px;
    /* Hairline divider as a top border-image gradient — fades in from
       the cyan accent at the centre and out to fully transparent at
       both ends. Replaces the old solid rgba(0,180,255,0.06) line. */
    border-top: 1px solid transparent;
    border-image: linear-gradient(90deg,
        rgba(var(--io-line-rgb), 0) 0%,
        rgba(var(--io-line-rgb), 0.18) 50%,
        rgba(var(--io-line-rgb), 0) 100%) 1;
}
.kl-legend-item:first-child { border-top: 0; padding-top: 4px; }
.kl-legend-sym {
    font-family: var(--io-font-var); font-style: italic;
    font-size: 17px; font-weight: 500;
    text-align: center; line-height: 1;
    color: rgba(0,200,255,0.92);
    text-shadow: 0 0 8px rgba(0,200,255,0.28);
}
.kl-legend-sym-gold {
    color: rgba(255,200,90,0.92);
    text-shadow: 0 0 8px rgba(255,200,90,0.28);
}
.kl-legend-name {
    display: block;
    color: rgba(var(--io-text-bright-rgb), 0.95);
    font-family: var(--io-font-display);
    font-size: 11px; font-weight: 400;
    letter-spacing: 0.6px;
    margin-bottom: 3px;
}
.kl-legend-desc {
    display: block;
    color: rgba(var(--io-text-rgb), 0.6);
    font-size: 10.5px; line-height: 1.5;
}

/* Glowing highlight on the legend row when a tip references it */
.kl-legend-item.kl-legend-highlight {
    background: linear-gradient(90deg, rgba(0,200,255,0.08), rgba(0,200,255,0));
    border-radius: 4px;
}
.kl-legend-item.kl-legend-highlight .kl-legend-sym {
    animation: kl-glow-wave 1.8s ease-in-out infinite;
}
.kl-legend-item.kl-legend-highlight .kl-legend-name {
    color: #fff;
}
@keyframes kl-glow-wave {
    0%, 100% { text-shadow: 0 0 6px rgba(0,200,255,0.3); }
    50%      { text-shadow: 0 0 14px rgba(0,200,255,0.75), 0 0 24px rgba(0,200,255,0.35); color: #fff; }
}

/* ── Contextual captions ───────────────────────────────────────
   Captions float free over the sim — no card background — so the text
   itself points at what it's describing. Each anchored variant drops a
   thin line + node in the direction of the subject.
     .kl-caption-planet   → floats above the orbit ring, line drops onto the planet
     .kl-caption-sun      → sits just right of the Sun, line points left to it
     .kl-caption-sectors  → upper-right over the swept sectors
     .kl-caption-orbit    → generic overlay (legacy fallback)
     .kl-caption-slider   → above the slider panel, line drops to the panel
     .kl-caption-center   → centered near the top (final "your turn" moment)
   All captions share .kl-caption styling. */
.kl-caption {
    z-index: 13;
    max-width: 320px;
    padding: 0;
    background: transparent;
    border: none;
    border-radius: 0;
    box-shadow: none;
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    pointer-events: none;
}
.kl-caption p {
    margin: 0;
    font-size: 13.5px;
    line-height: 1.6;
    color: rgba(var(--io-text-bright-rgb), 0.96);
    text-align: left;
    /* legibility replacement for the removed card background */
    text-shadow:
        0 0 1px rgba(0,0,0,0.95),
        0 1px 6px rgba(0,0,0,0.85),
        0 0 18px rgba(var(--io-bg-rgb), 0.9);
}
.kl-caption-fade-in { animation: kl-caption-in 620ms cubic-bezier(0.2, 0.7, 0.2, 1) both; }
.kl-caption-fade-out { animation: kl-caption-out 400ms ease-in forwards; }
@keyframes kl-caption-in {
    0%   { opacity: 0; transform: translateY(8px) scale(0.97); filter: blur(3px); }
    60%  { opacity: 1; filter: blur(0); }
    100% { opacity: 1; transform: translateY(0) scale(1); filter: blur(0); }
}
@keyframes kl-caption-out {
    0%   { opacity: 1; }
    100% { opacity: 0; transform: translateY(-4px); }
}

/* Orbit-anchored captions share absolute positioning over the sim. Each
   variant draws its own thin pointer line + small node so the subject of
   the caption is visually implicated without any surrounding box. */
.kl-caption-orbit,
.kl-caption-planet,
.kl-caption-sun,
.kl-caption-sectors {
    position: absolute;
}
/* Thin pointer line (::before) + node dot (::after) drawn in per-variant
   positions below. Colours: cyan for orbital/geometry subjects, gold for
   anything anchored to the Sun. */
.kl-caption-orbit::before,
.kl-caption-planet::before,
.kl-caption-sun::before,
.kl-caption-sectors::before,
.kl-caption-slider::before {
    content: '';
    position: absolute;
    background: linear-gradient(var(--kl-tail-dir, to bottom),
                rgba(0,200,255,0.85) 0%,
                rgba(0,200,255,0.15) 100%);
    box-shadow: 0 0 6px rgba(0,200,255,0.45);
    pointer-events: none;
}
.kl-caption-orbit::after,
.kl-caption-planet::after,
.kl-caption-sun::after,
.kl-caption-sectors::after,
.kl-caption-slider::after {
    content: '';
    position: absolute;
    width: 6px; height: 6px;
    border-radius: 50%;
    background: rgba(0,220,255,0.95);
    box-shadow: 0 0 8px rgba(0,200,255,0.8), 0 0 16px rgba(0,200,255,0.45);
    pointer-events: none;
}

/* Generic 'orbit' fallback — top-right of sim (legacy placement). */
.kl-caption-orbit { top: 64px; right: 20px; }
.kl-caption-orbit::before {
    top: -36px; right: 22px;
    width: 1.5px; height: 32px;
    background: linear-gradient(to top, rgba(0,200,255,0.85), rgba(0,200,255,0.1));
}
.kl-caption-orbit::after { top: -42px; right: 19px; }

/* 'planet' — first intro caption. World (0,0) = Sun = canvas centre
   (50%, 50%), and for Neptune's e≈0.009 the orbit is nearly circular and
   centred on the Sun, so placing the caption at 50%,50% sits it literally
   inside the orbit loop. No pointer — the text is describing the orbit it
   sits inside. */
.kl-caption-planet {
    top: 50%; left: 50%;
    transform: translate(-50%, -50%);
    max-width: 440px;
    text-align: center;
}
.kl-caption-planet p { text-align: center; }
.kl-caption-planet::before,
.kl-caption-planet::after { display: none; }

/* 'sun' — text sits just right of centre; a soft quarter-arc (built from
   border-left + border-bottom + border-bottom-left-radius) curves down-left
   from the caption, and the glowing node dot lands right on the Sun at
   canvas centre. Avoids the stick-straight horizontal pointer. */
.kl-caption-sun {
    top: calc(50% - 10px);
    left: calc(50% + 96px);
    max-width: 300px;
}
.kl-caption-sun::before {
    background: transparent !important;
    box-shadow: none !important;
    top: -8px;
    left: -92px;
    width: 80px;
    height: 22px;
    border-left: 1.5px solid rgba(255,200,80,0.9);
    border-bottom: 1.5px solid rgba(255,200,80,0.9);
    border-bottom-left-radius: 60px;
    filter: drop-shadow(0 0 6px rgba(255,200,80,0.45));
}
.kl-caption-sun::after {
    top: 11px;
    left: -95px;
    background: rgba(255,220,140,0.98);
    box-shadow: 0 0 10px rgba(255,200,80,0.9), 0 0 22px rgba(255,180,40,0.55);
}

/* 'sectors' — upper-right over the coloured wedges; the line drops further
   down-left to actually overlap the wedge band. */
.kl-caption-sectors {
    top: 22%; right: clamp(20px, 10%, 140px);
    max-width: 300px;
}
.kl-caption-sectors::before {
    top: 100%; left: 28px;
    width: 1.5px;
    height: clamp(60px, 12vh, 130px);
    margin-top: 6px;
    background: linear-gradient(to bottom, rgba(0,200,255,0.85), rgba(0,200,255,0.15));
}
.kl-caption-sectors::after {
    top: 100%; left: 25px;
    margin-top: calc(clamp(60px, 12vh, 130px) + 3px);
}

/* Slider caption: lifted OUT of `.kl-bottom` (see Razor) so it doesn't
   widen the centred flex row. Anchored in the top-level absolute context,
   centred horizontally, floating above the panel in the empty sky. A short
   soft-J pointer drops from the bottom of the caption onto the panel;
   the `.kl-slider-pulse` glow on the active slider (already applied) draws
   attention to the specific slider being discussed. */
.kl-caption-slider {
    position: absolute;
    bottom: 190px;
    left: 50%;
    transform: translateX(-50%);
    max-width: 460px;
    text-align: center;
}
.kl-caption-slider p { text-align: center; }
.kl-caption-slider::before {
    background: transparent !important;
    box-shadow: none !important;
    top: calc(100% + 4px);
    left: calc(50% - 22px);
    width: 22px;
    height: 36px;
    border-right: 1.5px solid rgba(0,200,255,0.85);
    border-bottom: 1.5px solid rgba(0,200,255,0.85);
    border-bottom-right-radius: 28px;
    filter: drop-shadow(0 0 6px rgba(0,200,255,0.4));
}
.kl-caption-slider::after {
    top: calc(100% + 4px + 36px - 3px);
    left: calc(50% - 25px);
}

/* Neutral center caption for the final "your turn" moment — no pointer.
   Centred on the orbit (same anchor as `.kl-caption-planet`) so the
   intro's first and last beats bookend in the same spot. */
.kl-caption-center {
    position: absolute;
    top: 50%; left: 50%;
    transform: translate(-50%, -50%);
    text-align: center;
    max-width: 440px;
}
.kl-caption-center p { text-align: center; }

/* Kepler 2's Moon rocket: what Artemis II is doing, over the flight, and
   on the trip how much of it a second shows. Free over the scene like the
   captions above, centred over the flight under the breadcrumb (or under
   the info banner on a narrow view); `artemisCaption.ts` writes and places
   it. The burn for the Moon and time changing pace are in Solar Orange;
   the pace is a value, so it is cyan, in the HUD's monospace. */
.kl-artemis-caption {
    position: absolute;
    z-index: 1;
    pointer-events: none;
    text-align: center;
    font-family: var(--io-font-body);
    text-shadow:
        0 0 1px rgba(0,0,0,0.95),
        0 1px 6px rgba(0,0,0,0.85),
        0 0 18px rgba(var(--io-bg-rgb), 0.9);
}
.kl-artemis-caption p { margin: 0; }
.kl-artemis-what {
    font-size: 15px;
    line-height: 1.45;
    color: rgba(var(--io-text-bright-rgb), 0.96);
}
.kl-artemis-what.kl-artemis-key { color: rgba(255, 200, 80, 0.95); }
.kl-artemis-time {
    margin-top: 3px;
    font-family: monospace;
    font-size: 11px;
    color: #00ccff;
}
.kl-artemis-time.kl-artemis-note {
    font-family: inherit;
    font-size: 12px;
    color: rgba(var(--io-text-dim-rgb), 0.85);
}
@media (max-width: 560px) {
    .kl-artemis-what { font-size: 13.5px; }
}

/* "Skip intro" button pinned to the left edge, just beneath the wide
   intro-mode info panel. The intro panel's bottom edge sits at
   `100vh - 200px` (see `.kl-info-intro-mode`), so placing the button at
   `bottom: 170px` puts it ~30px below the panel and ~10px above the
   controls bar. Shown only while the intro is running. */
/* ── Previous / Next / Ready navigation row inside the intro panel ──
   Concept C "sunlit-edge" button language (see
   wwwroot/design/button-concepts/README.md). Firm capsule with a
   `border-radius: 4px` cabin-panel silhouette, glassy cyan fill, no rim
   line — the silhouette is implied by layered inner-highlight + outer
   drop-shadows. On hover, a warm radial whose centre sits *past* the
   right edge bleeds back in from the right (the "sunlit edge"). The
   button itself never moves. The same language ports to .kl-skip-global,
   .sb (control bar) and .pb (preset grid) further down — all chrome
   speaks one button vocabulary. */
.kl-nav-buttons {
    display: flex;
    gap: 10px;
    margin-top: 20px;
    align-items: center;
}

.kl-nav-btn {
    position: relative;
    isolation: isolate;
    overflow: hidden;
    font-family: inherit;
    font-size: 11px;
    letter-spacing: 1.4px;
    text-transform: uppercase;
    color: rgba(var(--io-text-soft-rgb), 0.82);
    background: linear-gradient(180deg, rgba(var(--io-glass-rgb), 0.07) 0%, rgba(var(--io-glass-lo-rgb), 0.025) 100%);
    border: 0;
    border-radius: 4px;
    padding: 8px 20px;
    cursor: pointer;
    box-shadow:
        inset 0 1px 0 rgba(var(--io-glass-hi-rgb), 0.05),
        inset 0 0 0 1px rgba(var(--io-glass-ring-rgb), 0.08),
        0 1px 2px rgba(0,0,0,0.55),
        0 2px 6px rgba(0,0,0,0.35);
    transition: color 160ms ease, box-shadow 220ms ease;
}
.kl-nav-btn::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;
    pointer-events: none;
    border-radius: inherit;
    background: radial-gradient(circle at 110% 50%,
        rgba(255,240,210,0.32) 0%,
        rgba(255,230,195,0.18) 18%,
        rgba(255,225,185,0.08) 38%,
        rgba(255,220,175,0)    62%);
    opacity: 0;
    transition: opacity 280ms ease;
}
.kl-nav-btn:hover,
.kl-nav-btn:focus-visible {
    color: #eaf6ff;
    box-shadow:
        inset 0 1px 0 rgba(255,255,255,0.09),
        inset 0 0 0 1px rgba(var(--io-glass-ring-rgb), 0.14),
        0 1px 2px rgba(0,0,0,0.55),
        0 2px 8px rgba(0,0,0,0.40);
    outline: none;
}
.kl-nav-btn:hover::before,
.kl-nav-btn:focus-visible::before { opacity: 1; }

/* Ready: the primary variant of the same Concept C button. Slightly
   warmer fill at rest so it reads as the next-step affordance even
   before hover; the sunlit-edge wash still arrives on hover. */
.kl-ready-btn {
    color: #fff;
    background: linear-gradient(180deg, rgba(140,225,255,0.16) 0%, rgba(100,200,250,0.06) 100%);
    box-shadow:
        inset 0 1px 0 rgba(220,245,255,0.10),
        inset 0 0 0 1px rgba(160,225,250,0.18),
        0 1px 2px rgba(0,0,0,0.55),
        0 2px 8px rgba(0,0,0,0.40);
}
.kl-ready-btn:hover,
.kl-ready-btn:focus-visible {
    color: #fff;
    box-shadow:
        inset 0 1px 0 rgba(255,255,255,0.14),
        inset 0 0 0 1px rgba(160,230,250,0.30),
        0 1px 2px rgba(0,0,0,0.55),
        0 2px 10px rgba(0,0,0,0.45);
}

/* ── Invite-mode panel ───────────────────────────────────────────────────────
   When a lesson's first guide step is the sole "enter the tour" gate, the
   entire .kl-info panel becomes the CTA. .kl-info-invite is added to the
   outer div alongside .kl-info-intro-mode; role="button" + tabindex are
   set in Razor. Same Concept C sunlit-edge hover — the panel never moves. */
.kl-info.kl-info-invite {
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 64px;
    isolation: isolate;
    transition:
        transform 1100ms cubic-bezier(0.65, 0.02, 0.25, 1),
        width    1100ms cubic-bezier(0.65, 0.02, 0.25, 1),
        padding  1100ms cubic-bezier(0.65, 0.02, 0.25, 1),
        border-color 280ms ease,
        box-shadow   280ms ease;
}
.kl-info.kl-info-invite::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;
    pointer-events: none;
    border-radius: inherit;
    background: radial-gradient(circle at 110% 50%,
        rgba(255,240,210,0.16) 0%,
        rgba(255,230,195,0.08) 20%,
        rgba(255,225,185,0.03) 40%,
        rgba(255,220,175,0)    65%);
    opacity: 0;
    transition: opacity 280ms ease;
}
.kl-info.kl-info-invite:hover,
.kl-info.kl-info-invite:focus-visible {
    border-color: rgba(0,200,255,0.42);
    box-shadow:
        0 4px 24px rgba(0,0,0,0.4),
        inset 0 1px 0 rgba(255,255,255,0.04),
        0 0 28px rgba(0,200,255,0.13);
    outline: none;
}
.kl-info.kl-info-invite:hover::before,
.kl-info.kl-info-invite:focus-visible::before { opacity: 1; }
.kl-info-invite-text {
    font-family: var(--io-font-display);
    font-size: 13px;
    letter-spacing: 0.9px;
    text-transform: uppercase;
    color: rgba(var(--io-text-bright-rgb), 0.88);
    line-height: 1.55;
    text-align: center;
    pointer-events: none;
}

/* Skip intro — same Concept C language as the nav row, just a touch
   smaller so it reads as a secondary, push-it-aside action. */
.kl-skip-global {
    position: relative;
    isolation: isolate;
    overflow: hidden;
    margin-right: auto;
    font-family: inherit;
    font-size: 10.5px;
    letter-spacing: 0.6px;
    text-transform: uppercase;
    color: rgba(var(--io-text-soft-rgb), 0.7);
    background: linear-gradient(180deg, rgba(var(--io-glass-rgb), 0.06) 0%, rgba(var(--io-glass-lo-rgb), 0.02) 100%);
    border: 0;
    border-radius: 4px;
    padding: 6px 12px;
    cursor: pointer;
    box-shadow:
        inset 0 1px 0 rgba(var(--io-glass-hi-rgb), 0.04),
        inset 0 0 0 1px rgba(var(--io-glass-ring-rgb), 0.08),
        0 1px 2px rgba(0,0,0,0.55),
        0 2px 6px rgba(0,0,0,0.35);
    transition: color 160ms ease, box-shadow 220ms ease;
}
.kl-skip-global::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;
    pointer-events: none;
    border-radius: inherit;
    background: radial-gradient(circle at 110% 50%,
        rgba(255,240,210,0.32) 0%,
        rgba(255,230,195,0.18) 18%,
        rgba(255,225,185,0.08) 38%,
        rgba(255,220,175,0)    62%);
    opacity: 0;
    transition: opacity 280ms ease;
}
.kl-skip-global:hover,
.kl-skip-global:focus-visible {
    color: #eaf6ff;
    box-shadow:
        inset 0 1px 0 rgba(255,255,255,0.09),
        inset 0 0 0 1px rgba(var(--io-glass-ring-rgb), 0.14),
        0 1px 2px rgba(0,0,0,0.55),
        0 2px 8px rgba(0,0,0,0.40);
    outline: none;
}
.kl-skip-global:hover::before,
.kl-skip-global:focus-visible::before { opacity: 1; }

@media (prefers-reduced-motion: reduce) {
    .kl-nav-btn::before,
    .kl-skip-global::before { transition: none; }
}

/* ── Astronaut-hand cursor that drives a slider automatically ──
   Positioned absolutely inside .kl-param (which must be position:relative).
   `left` is set by JS each animation frame to track the slider thumb.
   After flipping the SVG vertically so fingertips point DOWN, the index
   fingertip sits at x = 20/32 = 62.5% of the element's width (it was x=12
   before the flip), so we pull the hand back by that amount — not 50% —
   to align the fingertip itself with the thumb centre. */
.kl-hand {
    position: absolute;
    width: 30px; height: 34px;
    pointer-events: none;
    z-index: 5;
    transform: translate(-62.5%, 0);
    animation: kl-hand-press 1.6s ease-in-out infinite;
    filter: drop-shadow(0 0 6px rgba(0,200,255,0.45));
}
.kl-hand svg { width: 100%; height: 100%; display: block; }
@keyframes kl-hand-press {
    0%, 100% { transform: translate(-62.5%, 0); }
    50%      { transform: translate(-62.5%, 3px); }
}

/* .kl-param must be position:relative so the hand anchors correctly. */
.kl-param { position: relative; }

/* ── Bottom bar: HUD + controls ── */
/* `bottom: 20px` keeps the bar, and the HUD beside it, clear of the
   fixed version badge (bottom-left, 6–16 px up). */
.kl-bottom {
    position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%); z-index: 10;
    display: flex; align-items: flex-end; gap: 8px;
}
/* HUD is absolutely positioned to the LEFT of the centred play panel
   so its variable-width content never shifts the panel horizontally.
   Common sources of width drift this guards against:
     * decimal-width changes in any numeric readout (e.g. `t = 1.23`
       becomes `t = 12.3` and the string grows by a character),
     * the three-body ghost note swapping between `ghost off` (9 chars)
       and `ghost drift X.XXX` (17+ chars) when toggled,
     * Kepler First Law switching between planets — `a = 9.54 AU` vs
       `a = 19.19 AU` differ in width.
   With the HUD outside the flex flow, `.kl-bottom`'s width depends only
   on fixed-width children (the play panel, plus the 210 px chart panel
   on Third Law), so the centred bar stays put. */
.kl-hud {
    position: absolute;
    right: 100%;
    bottom: 0;
    margin-right: 8px;
    padding: 10px 12px;
    background: rgba(var(--io-bg-rgb), 0.88);
    border: 1px solid rgba(var(--io-line-rgb), 0.1);
    border-radius: 6px;
    -webkit-backdrop-filter: blur(10px);
    backdrop-filter: blur(10px);
    font-family: monospace; font-size: 11px;
    color: rgba(var(--io-text-dim-rgb), 0.75);
    white-space: nowrap;
}
.kl-hud:empty { display: none; }
/* Where the HUD would run off the window's left edge (about 821–1270 px
   wide, depending on the lesson), `src/interop/bottomBarFit.ts` adds
   `.kl-hud-above` and the HUD sits above the controls at their right end,
   clear of the info panel on the left. Still out of the flex flow, so the
   bar stays put. */
.kl-bottom.kl-hud-above .kl-hud {
    right: 0;
    bottom: calc(100% + 8px);
    margin-right: 0;
}

.kl-panel {
    width: 420px; padding: 10px 14px;
    background: rgba(var(--io-bg-rgb), 0.88);
    border: 1px solid rgba(var(--io-line-rgb), 0.1);
    border-radius: 6px;
    -webkit-backdrop-filter: blur(10px);
    backdrop-filter: blur(10px);
}

/* Third Law only: with just two sliders (Planet 1, Planet 2) and three
   speed buttons, the panel can be much narrower than the shared default.
   Scoped to the Third Law bottom bar via `.kl-bottom-third` so First /
   Second Law layouts are untouched. */
.kl-bottom-third .kl-panel {
    width: 280px;
}
.kl-panel-controls {
    display: flex; align-items: center; gap: 5px;
    justify-content: center; flex-wrap: nowrap;
}
.kl-panel-divider { height: 1px; background: rgba(var(--io-line-rgb), 0.08); margin: 8px 0; }
.kl-sep { width: 1px; height: 18px; background: rgba(var(--io-line-rgb), 0.12); margin: 0 2px; }

.kl-param { margin-bottom: 8px; }
.kl-param:last-of-type { margin-bottom: 0; }
.kl-param-row { display: flex; justify-content: space-between; margin-bottom: 2px; }
.kl-param-row span { font-size: 11px; color: rgba(var(--io-text-rgb), 0.6); }
.kl-param-val { color: #00ccff !important; font-family: monospace; }

/* ── Third-Law side chart (sits to the left of the play panel in
     `.kl-bottom`). Mirrors the play panel's dark translucent card look
     so the two read as part of the same instrument strip. ── */
.kl-chart-panel {
    width: 210px; height: 160px;
    background: rgba(var(--io-surface-hi-rgb), 0.78);
    border: 1px solid rgba(var(--io-line-rgb), 0.25);
    border-radius: 6px;
    -webkit-backdrop-filter: blur(10px);
    backdrop-filter: blur(10px);
    display: block;
}

/* ── Top-right button row ── */
.kl-topright {
    position: absolute; top: 10px; right: 10px; z-index: 11;
    display: flex; gap: 4px;
}
/* Focus mode on a page that keeps its panels mounted (Gravity Assists),
   so the read-out and the controls the simulation fills survive it. */
.kl.kl-focus .kl-info,
.kl.kl-focus .kl-bottom { display: none; }

/* ── Buttons (shared) — Concept C "sunlit-edge" language ──
   Same vocabulary as .kl-nav-btn / .kl-skip-global above, scaled
   down for the control-bar row (Play / Step / Reset / 1× / 2× / 5×
   etc.). Compact padding so a full row fits the .kl-panel width.
   `.sb.on` is the persistent-active variant — used by speed
   buttons, focus toggle, etc. — and brightens the cyan fill rather
   than warming the right edge, so "currently selected" reads as a
   different signal from the transient sunlit-edge hover. */
.sb {
    position: relative;
    isolation: isolate;
    overflow: hidden;
    background: linear-gradient(180deg, rgba(var(--io-glass-rgb), 0.06) 0%, rgba(var(--io-glass-lo-rgb), 0.025) 100%);
    border: 0;
    border-radius: 4px;
    padding: 5px 12px;
    color: rgba(var(--io-text-soft-rgb), 0.75);
    font-size: 11px; cursor: pointer; font-family: inherit;
    box-shadow:
        inset 0 1px 0 rgba(var(--io-glass-hi-rgb), 0.06),
        inset 0 0 0 1px rgba(var(--io-glass-ring-rgb), 0.10),
        0 1px 2px rgba(0,0,0,0.55),
        0 2px 6px rgba(0,0,0,0.35);
    transition: color 160ms ease, box-shadow 220ms ease;
}
.sb::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;
    pointer-events: none;
    border-radius: inherit;
    background: radial-gradient(circle at 110% 50%,
        rgba(160,220,255,0.20) 0%,
        rgba(120,200,255,0.10) 18%,
        rgba(80,180,255,0.04) 38%,
        rgba(0,160,255,0)    62%);
    opacity: 0;
    transition: opacity 280ms ease;
}
.sb:hover,
.sb:focus-visible {
    color: #eaf6ff;
    box-shadow:
        inset 0 1px 0 rgba(var(--io-glass-hi-rgb), 0.12),
        inset 0 0 0 1px rgba(var(--io-glass-ring-rgb), 0.22),
        0 1px 2px rgba(0,0,0,0.55),
        0 2px 8px rgba(0,0,0,0.40);
    outline: none;
}
.sb:hover::before,
.sb:focus-visible::before { opacity: 1; }
.sb.on {
    color: #00ccff;
    background: linear-gradient(180deg, rgba(130,225,255,0.14) 0%, rgba(90,200,250,0.05) 100%);
    box-shadow:
        inset 0 1px 0 rgba(220,245,255,0.08),
        inset 0 0 0 1px rgba(160,230,250,0.18),
        0 1px 2px rgba(0,0,0,0.55),
        0 2px 8px rgba(0,0,0,0.40);
}
.sb.sp { padding: 5px 10px; text-align: center; white-space: nowrap; box-sizing: border-box; }
.sb:disabled { opacity: 0.25; cursor: not-allowed; }
.sb:disabled::before { opacity: 0; }

/* ── Preset grid ── */
.kl-presets {
    display: grid; grid-template-columns: repeat(5, 1fr); gap: 3px; margin-top: 8px;
}
.kl-presets-second { grid-template-columns: repeat(5, 1fr); }
/* Two choices side by side: Gravity Assists' side of Jupiter, Kepler 2's Earth and comet. */
.kl-presets-pair { grid-template-columns: repeat(2, 1fr); }

/* ── Preset grid — same Concept C language, smaller still. The grid
   is dense (5 columns) so padding stays minimal and font-size shrinks
   one notch from .sb. `.pb-active` is the persistent selection (cyan
   fill) and `.pb-solar` is the warm-amber "Solar System" override that
   spans the full row, however many columns it has; both keep their
   existing colour identity but inherit the layered shadow + sunlit-edge
   hover from the base. */
.pb {
    position: relative;
    isolation: isolate;
    overflow: hidden;
    background: linear-gradient(180deg, rgba(var(--io-glass-rgb), 0.04) 0%, rgba(var(--io-glass-lo-rgb), 0.015) 100%);
    border: 0;
    color: rgba(var(--io-text-rgb), 0.6); padding: 5px 0; border-radius: 3px;
    font-size: 10px; cursor: pointer; font-family: inherit;
    text-align: center; letter-spacing: 0.2px;
    box-shadow:
        inset 0 1px 0 rgba(var(--io-glass-hi-rgb), 0.03),
        inset 0 0 0 1px rgba(var(--io-glass-ring-rgb), 0.06),
        0 1px 2px rgba(0,0,0,0.45);
    transition: color 160ms ease, box-shadow 220ms ease;
}
.pb::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;
    pointer-events: none;
    border-radius: inherit;
    background: radial-gradient(circle at 110% 50%,
        rgba(255,240,210,0.28) 0%,
        rgba(255,230,195,0.14) 22%,
        rgba(255,220,175,0)    62%);
    opacity: 0;
    transition: opacity 240ms ease;
}
.pb:hover,
.pb:focus-visible {
    color: #fff;
    box-shadow:
        inset 0 1px 0 rgba(255,255,255,0.07),
        inset 0 0 0 1px rgba(var(--io-glass-ring-rgb), 0.13),
        0 1px 2px rgba(0,0,0,0.5);
    outline: none;
}
.pb:hover::before,
.pb:focus-visible::before { opacity: 1; }
.pb.pb-active {
    color: #00ccff;
    background: linear-gradient(180deg, rgba(130,225,255,0.12) 0%, rgba(90,200,250,0.04) 100%);
    box-shadow:
        inset 0 1px 0 rgba(220,245,255,0.06),
        inset 0 0 0 1px rgba(160,230,250,0.16),
        0 1px 2px rgba(0,0,0,0.5);
}
.pb.pb-solar {
    grid-column: 1 / -1;
    color: rgba(255,200,80,0.65);
    background: linear-gradient(180deg, rgba(255,200,80,0.06) 0%, rgba(255,170,40,0.02) 100%);
    box-shadow:
        inset 0 1px 0 rgba(255,255,255,0.04),
        inset 0 0 0 1px rgba(255,200,90,0.14),
        0 1px 2px rgba(0,0,0,0.45);
    letter-spacing: 1px; text-transform: uppercase; font-size: 9px; padding: 6px 0;
}
.pb.pb-solar:hover,
.pb.pb-solar:focus-visible {
    color: rgba(255,220,100,0.95);
    box-shadow:
        inset 0 1px 0 rgba(255,255,255,0.07),
        inset 0 0 0 1px rgba(255,210,110,0.22),
        0 1px 2px rgba(0,0,0,0.5);
}
/* Kepler 2's Artemis II button while its scene is showing. */
.pb.pb-solar.pb-active {
    color: rgba(255,220,100,0.95);
    background: linear-gradient(180deg, rgba(255,200,80,0.14) 0%, rgba(255,170,40,0.05) 100%);
    box-shadow:
        inset 0 1px 0 rgba(255,255,255,0.07),
        inset 0 0 0 1px rgba(255,210,110,0.3),
        0 1px 2px rgba(0,0,0,0.5);
}

@media (prefers-reduced-motion: reduce) {
    .sb::before,
    .pb::before { transition: none; }
}

/* ── Sliders ── */
.sr {
    width: 100%; height: 3px;
    -webkit-appearance: none; appearance: none;
    background: rgba(var(--io-line-rgb), 0.1); border-radius: 2px; outline: none;
    transition: background 0.2s, box-shadow 0.2s;
}
.sr::-webkit-slider-thumb {
    -webkit-appearance: none; appearance: none;
    width: 12px; height: 12px; border-radius: 50%;
    background: #00ccff; cursor: pointer; box-shadow: 0 0 6px rgba(0,200,255,0.3);
    transition: box-shadow 0.2s, transform 0.2s;
}
.sr::-moz-range-thumb {
    width: 12px; height: 12px; border-radius: 50%;
    background: #00ccff; cursor: pointer; border: none; box-shadow: 0 0 6px rgba(0,200,255,0.3);
    transition: box-shadow 0.2s, transform 0.2s;
}

/* ── Aggressive slider glow when a tip points at it ── */
.kl-slider-pulse .kl-param-row span { color: rgba(0,220,255,0.95); }
.kl-slider-pulse .kl-param-val { color: #7df3ff !important; }
.kl-slider-pulse .sr {
    background: rgba(0,200,255,0.35);
    animation: kl-slider-track-pulse 1.2s ease-in-out infinite;
}
.kl-slider-pulse .sr::-webkit-slider-thumb {
    animation: kl-slider-thumb-pulse 1.2s ease-in-out infinite;
    transform: scale(1.15);
}
.kl-slider-pulse .sr::-moz-range-thumb {
    animation: kl-slider-thumb-pulse 1.2s ease-in-out infinite;
    transform: scale(1.15);
}
@keyframes kl-slider-track-pulse {
    0%, 100% { box-shadow: 0 0 0 0 rgba(0,220,255,0), inset 0 0 4px rgba(0,220,255,0.4); }
    50%      { box-shadow: 0 0 12px 2px rgba(0,220,255,0.55), inset 0 0 6px rgba(0,220,255,0.6); }
}
@keyframes kl-slider-thumb-pulse {
    0%, 100% { box-shadow: 0 0 6px rgba(0,220,255,0.6), 0 0 0 0 rgba(0,220,255,0); }
    50%      { box-shadow: 0 0 14px rgba(0,220,255,0.95), 0 0 0 6px rgba(0,220,255,0.18); }
}

/* Canvas host — stretch to container */
.simulation-container, .simulation-canvas {
    width: 100% !important; height: 100% !important; background: transparent !important;
}

/* ══════════════════════════════════════════════════════════════════
   Responsive breakpoints for interactive lesson pages
   ══════════════════════════════════════════════════════════════════

   Breakpoint convention (see STYLE_GUIDE.md § Responsive layout):
     * 820 px — tablet / phone landscape threshold. Lesson pages stop
       trying to fit info panel + canvas + controls side-by-side and
       stack the overlay cards full-width instead.
     * 560 px — phone portrait threshold. Further typographic and
       padding compression.
     * (pointer: coarse) — touch-target sweep. Independent of viewport
       width because phones in landscape are still touch devices.

   The Kepler / Hohmann / Three-Body pages share the same `.kl-*`
   layout scaffolding, so one rule set covers all five at once. Pages
   with their own page-level styles (EscapeVelocity, NewtonsGravitation,
   Brachistochrone) already carry their own media queries.                */

@media (max-width: 820px) {
    /* Resting info panel: full-width banner across the top instead of
       a 288 px column on the left. Max-height clamped so the tour text
       never fills more than ~45 % of the viewport, leaving the canvas
       and the bottom controls reachable in one thumb swipe.
       `top: 58px` clears both the fixed breadcrumb bar (bottom ≈ 30 px)
       and the Focus button (bottom ≈ 50 px on a touch screen, where it
       is 44 px tall) with room to spare on devices where font metrics
       differ. */
    .kl-info {
        left: 8px; right: 8px; top: 58px;
        width: auto;
        max-width: none;
        max-height: 45vh;
        padding: 10px 12px 12px;
        font-size: 13px;
    }

    /* Intro-mode: same full-width banner, pinned at the top rather than
       floating bottom-left next to a centred control panel (which no
       longer exists at this width). Dropping the fancy translate keeps
       the transition from intro → resting a no-op position-wise, which
       is fine — the fade-in of the resting content carries the beat. */
    .kl-info.kl-info-intro-mode {
        left: 8px; right: 8px; top: 58px;
        width: auto;
        max-height: 42vh;
        padding: 12px 14px 14px;
        transform: none;
    }

    .kl-info-intro-text { font-size: 14px; line-height: 1.55; }

    .kl-nav-buttons { margin-top: 12px; }

    /* Bottom bar: switch to a stacked column so HUD + panel + (optional)
       chart all take the full viewport width. HUD sits above the panel
       so the numeric readout isn't pushed offscreen by the wider panel
       on a narrow viewport. `order` reshuffles the flex children without
       needing to change the razor markup. `bottom: 20px` keeps the column
       clear of the version badge (bottom-left, 6–16 px up). */
    .kl-bottom {
        left: 8px; right: 8px; bottom: 20px;
        transform: none;
        flex-direction: column;
        align-items: stretch;
        gap: 6px;
        max-height: 55vh;
        overflow-y: auto;
    }
    .kl-hud {
        /* Override desktop `position: absolute` — on mobile the HUD
           rejoins the column flow above the play panel. */
        position: static;
        margin-right: 0;
        order: -1;            /* above the panel */
        font-size: 10.5px;
        white-space: normal;  /* was `nowrap` for the desktop single-line HUD */
        line-height: 1.4;
    }
    .kl-panel { width: auto; }
    /* A HUD drawn as a picture (Hohmann's burn bar) shrinks to fit. */
    .kl-hud svg { max-width: 100%; height: auto; }
    /* Only the controls panel, at the column's foot, steps in on the
       right, round the `.detail-toggle` (position:fixed, bottom-right),
       which is ~90px wide at fine-pointer sizes (10px monospace, 3px 6px
       padding); the read-outs above it keep the full width. */
    .kl-bottom > .kl-panel { margin-right: 92px; }
    /* Third-Law override: chart panel stretches too, and sits between
       HUD and play panel (it's not interactive — putting it above the
       controls keeps controls within thumb reach). */
    .kl-bottom-third .kl-panel { width: auto; }
    .kl-chart-panel { width: 100%; height: 120px; order: 0; }

    /* Top-right focus / detail toggles stay where they are but shrink so
       they don't collide with the info banner. */
    .kl-topright { top: 6px; right: 6px; gap: 3px; }
    .kl-topright .sb { font-size: 10px; padding: 4px 8px; }

    /* Absolute-positioned captions are carefully tuned for the desktop
       coordinate system (Sun at canvas centre, sectors upper-right…).
       On a phone those anchors miss their targets and clutter the view.
       Suppress them; `.kl-info` already carries the step's caption text,
       which is the essential content. */
    .kl-caption-sectors,
    .kl-caption-sun,
    .kl-caption-slider { display: none; }

    /* Planet / centre captions are centred on the sim — keep them but
       shrink so they fit. */
    .kl-caption-planet,
    .kl-caption-center { max-width: 88vw; font-size: 13px; }
}

/* Touch phones: the detail-level toggle becomes ~110 px wide at coarse
   pointer (min-height: 44 px, padding: 10px 12px, font-size: 11px), so
   the controls panel's 92 px step set above isn't wide enough. Widen it
   to 116 px, which adds a comfortable margin. */
@media (max-width: 820px) and (pointer: coarse) {
    .kl-bottom > .kl-panel { margin-right: 116px; }
}

@media (max-width: 560px) {
    /* Phone-portrait compression: let the control buttons wrap onto a
       second row instead of overflowing, and tighten typography in the
       info banner so the tour text fits in ~30 % of the viewport,
       leaving the simulation canvas the majority of the screen. */
    .kl-panel-controls { flex-wrap: wrap; row-gap: 6px; justify-content: flex-start; }
    .kl-title { font-size: 16px; }
    .kl-info { max-height: 30vh; font-size: 12px; }
    .kl-info-intro-text { font-size: 13px; line-height: 1.5; }
    .kl-panel { padding: 8px 10px; }

    /* Legend descriptions add ~160 px of height on narrow screens but
       aren't needed once the guided intro has explained each symbol.
       Hide the description text; the symbol + name row is enough for
       quick reference. Tighten item padding to match. */
    .kl-legend-desc { display: none; }
    .kl-legend-item { padding: 5px 2px; }

    /* Preset grid: planet shortcuts don't translate to a 360-px screen
       (buttons are unreadably small, labels truncate). The sliders give
       the same full range of exploration, so hide the grid entirely. */
    .kl-presets { display: none; }
    /* …except a pair, where two wide chips fit: Gravity Assists' side of
       Jupiter, the lesson's only control, and Kepler 2's Earth and comet
       with the Artemis II button under them, the lesson's main example. */
    .kl-presets.kl-presets-pair { display: grid; }
}

/* ── Touch-target sweep (independent of viewport width) ───────────
   WCAG / Material / HIG all recommend ≥44×44 CSS px for primary touch
   targets. The default `<input type="range">` thumb is ~12 px at rest
   (our `.sr` class sizes it to exactly that to match the thin-track
   aesthetic), which is ungrabbable with a fingertip. We keep the thin
   track on desktop but upgrade thumb + button sizes on any coarse
   pointer (phones, tablets, kiosk touchscreens).                    */
@media (pointer: coarse) {
    /* Slider thumb ~2× bigger in every direction → the tap target goes
       from 12² = 144 px² to 24² = 576 px² without widening the track. */
    .sr::-webkit-slider-thumb {
        width: 24px; height: 24px;
        box-shadow: 0 0 10px rgba(0,200,255,0.45);
    }
    .sr::-moz-range-thumb {
        width: 24px; height: 24px;
        box-shadow: 0 0 10px rgba(0,200,255,0.45);
    }
    /* Thicker track so the thumb doesn't look stranded; still reads
       as a thin line next to the thumb. */
    .sr { height: 5px; }

    /* Extra vertical padding around each slider row so the flick/drag
       hit area spans the recommended 44 px without changing visual
       weight. `.kl-param` is the label+track wrapper. */
    .kl-param { padding: 8px 0; margin-bottom: 4px; }

    /* Play / Step / Reset / speed buttons — 44 px min height, generous
       horizontal padding. The text size stays small so buttons still
       read as "chrome" rather than CTAs. */
    .sb {
        min-height: 44px;
        padding: 8px 14px;
        font-size: 12px;
    }
    /* Preset pills (smaller grid) — keep their look but enforce a
       usable minimum tap height. */
    .pb {
        min-height: 44px;
        padding: 10px 4px;
        font-size: 11px;
    }
    /* Guided-tour nav pills (Previous / Next / Ready). */
    .kl-nav-btn {
        min-height: 44px;
        padding: 10px 22px;
        font-size: 12px;
    }
    /* Detail-level toggle (pinned bottom-right by MainLayout) — same
       treatment so it's reachable on phones. */
    .detail-toggle {
        min-height: 44px;
        padding: 10px 12px;
        font-size: 11px;
    }

    /* The astronaut-hand cursor exists to point at the slider thumb
       during the guided tour — it's a pure hover-era hint and makes no
       sense when the user's literal finger is the cursor. Hide it. */
    .kl-hand { display: none !important; }
}

/* Devices without hover (effectively a superset of touch) also don't
   benefit from the hand pointer or from .sb:hover-only discovery cues.
   Kept separate from (pointer: coarse) because a trackpad device may
   be (pointer: fine) + (hover: none). */
@media (hover: none) {
    .kl-hand { display: none !important; }
}
