/* ============================================
   Knovli — shared styles
   Used by index.html (gated app shell) + login.html.
   ============================================ */

:root {
    /* Fixed-height topbar — pinned so the sidebar can compute
       `top: var(--topbar-height)` and start exactly below it.
       Lets the menu button in the topbar stay clickable when the
       sidebar (which slides in from the left at this same y-offset)
       is open. */
    --topbar-height: 64px;

    /* Mind Match — LIGHT theme palette. Mirrors apps/mind-match/main
       light-mode block: white surfaces, deep slate text, Mind Match
       accent blues + purples + orange. */
    --brand-blue:        #2563EB;
    --brand-blue-deep:   #3B82F6;
    --brand-purple:      #8B5CF6;
    --brand-orange:      #F97316;
    /* Knovli signature gradient — blue → violet, per the 2026-09-01 mockup.
       (mm-edu's third orange stop is gone; --brand-orange itself stays, it is
       still used for warm CTAs and the Psychology-style subject accent.) */
    --brand-gradient:    linear-gradient(90deg, #3B82F6, #8B5CF6);

    --bg:                #F5F7FA;
    --bg-card:           #FFFFFF;
    --bg-input:          #FFFFFF;
    --bg-secondary:      #E9EEF5;

    --text:              #0F172A;
    --text-muted:        #475569;
    --text-dim:          #94A3B8;

    --border:            rgba(15, 23, 42, 0.10);
    --border-strong:     rgba(15, 23, 42, 0.16);

    --danger:            #DC2626;
    --danger-bg:         rgba(220, 38, 38, 0.10);
    --success:           #16A34A;
    --success-bg:        rgba(22, 163, 74, 0.10);
    --warning:           #D97706;
    --info:              #2563EB;

    --shadow-sm: 0 2px 8px rgba(15, 23, 42, 0.06);
    --shadow-md: 0 4px 16px rgba(15, 23, 42, 0.10);
    --shadow-lg: 0 8px 32px rgba(15, 23, 42, 0.15);

    /* ─── Theme Phase A — design tokens (2026-05-27) ────────────
       Foundation layer for the "Dark Academic Arcade" redesign.
       These tokens are ADDITIVE — every existing CSS rule and
       inline style continues to work unchanged. New components
       built after Phase A pick these up automatically; the plugin
       theming pass in Phase D will migrate inline colors → tokens.

       Categories below: semantic surfaces, accent palette, status,
       typography, motion, radius, elevation, focus. */

    /* Semantic surfaces (3 layers of depth for layered glassmorphism) */
    --surface-0:         #F5F7FA;   /* page background */
    --surface-1:         #FFFFFF;   /* primary card */
    --surface-2:         #F8FAFC;   /* nested panel */
    --surface-overlay:   rgba(15, 23, 42, 0.45);   /* modal/sheet backdrop */
    --surface-glass:     rgba(255, 255, 255, 0.80); /* glassmorphism panels */
    --surface-glass-border: rgba(15, 23, 42, 0.08);

    /* Accent palette */
    --accent-blue:       #2563EB;   /* primary action — aligns with brand-blue */
    --accent-cyan:       #2FB2EA;   /* esports accent — brighter than brand-blue */
    --accent-purple:     #8B5CF6;   /* secondary accent — aligns with brand-purple */
    --accent-gold:       #FBBF24;   /* rank / achievement / streak — NEW */
    --accent-orange:     #F97316;   /* warm CTA — aligns with brand-orange */

    /* Hover / pressed variants for accents */
    --accent-blue-hover:   #1D4ED8;
    --accent-purple-hover: #7C3AED;
    --accent-gold-hover:   #F59E0B;

    /* Typography stack — Sora for headings (modern/futuristic),
       Inter for body/UI (clean + dense + legible). System fallbacks
       keep the page usable if Google Fonts fails to load. */
    --font-display: 'Sora', 'Space Grotesk', 'Outfit', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
    --font-body:    'Inter', 'Manrope', 'IBM Plex Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
    --font-mono:    'JetBrains Mono', 'SF Mono', 'Roboto Mono', monospace;

    /* Type scale (rem-based; root html size = 16px) */
    --text-2xs: 0.65rem;   /* tiny category labels (UPPERCASE) */
    --text-xs:  0.75rem;   /* small meta */
    --text-sm:  0.875rem;  /* body small */
    --text-base: 1rem;     /* body */
    --text-lg:  1.125rem;  /* lead paragraph */
    --text-xl:  1.25rem;   /* card title */
    --text-2xl: 1.5rem;    /* section heading */
    --text-3xl: 1.875rem;  /* page heading */
    --text-4xl: 2.25rem;   /* hero */
    --tracking-wide: 0.05em;   /* category labels */
    --tracking-tight: -0.02em; /* big display text */

    /* Motion — keep restrained, premium-feel timings */
    --motion-fast:    120ms;
    --motion-medium:  200ms;
    --motion-slow:    320ms;
    --motion-ease:    cubic-bezier(0.4, 0, 0.2, 1);    /* standard */
    --motion-ease-out: cubic-bezier(0.16, 1, 0.3, 1);  /* expo-out, for popovers */

    /* Border radii (tactile gaming-UI feel without going round-button) */
    --radius-sm: 6px;
    --radius-md: 10px;
    --radius-lg: 16px;
    --radius-xl: 24px;
    --radius-pill: 9999px;

    /* Elevation / shadows (Phase A keeps the existing shadow-sm/md/lg
       for back-compat AND adds a "glow" set for the gaming feel) */
    --glow-blue:   0 0 24px rgba(47, 178, 234, 0.25);
    --glow-purple: 0 0 24px rgba(139, 92, 246, 0.25);
    --glow-gold:   0 0 24px rgba(251, 191, 36, 0.30);

    /* Focus ring (a11y) — visible against both light + dark */
    --focus-ring: 0 0 0 3px rgba(47, 178, 234, 0.40);
}

/* ─── Theme Phase A — DARK overlay ───────────────────────────────
   Applied via `<html data-theme="dark">` (or `<body data-theme>`).
   Bootstrap script in index.html sets this before paint to avoid
   light-to-dark flash. Maps onto the SAME token names so any
   component reading `var(--bg-card)` etc. flips automatically.
   Brand-blue / -purple / -orange tokens stay aligned with the
   light-theme brand identity; surface colors flip to slate-900/
   navy. */
/* Dark ground re-based on the design mock's #11182B (2026-09-02).
   The WHOLE ramp moves, not just --bg: #11182B is rgb(17,24,43), which is
   LIGHTER than the old --bg-card #12141F rgb(18,20,31) — changing the ground
   alone would have put the page above the cards sitting on it and inverted the
   elevation scale. Each surface keeps its original delta from the ground, so
   the steps stay proportional. --text #F8FAFC holds 16.9:1 on the new ground. */
[data-theme="dark"] {
    /* Knovli palette (2026-09-01 mockup): a near-black ground with a faint
       blue cast, cards one step up, and the blue→violet accent doing the
       colour work. Replaces mm-edu's navy (#0B1020 / #131C31). */
    --bg:                #11182B;
    --bg-card:           #192138;
    --bg-input:          #1E2741;
    --bg-secondary:      #171F39;

    --text:              #F8FAFC;
    --text-muted:        #94A3B8;
    --text-dim:          #64748B;

    --border:            rgba(255, 255, 255, 0.08);
    --border-strong:     rgba(255, 255, 255, 0.14);

    --shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.40);
    --shadow-md: 0 4px 16px rgba(0, 0, 0, 0.50);
    --shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.55);

    /* Surfaces — 3 layers of near-black depth */
    --surface-0:         #11182B;
    --surface-1:         #192138;
    --surface-2:         #1E2741;
    --surface-overlay:   rgba(0, 0, 0, 0.72);
    --surface-glass:     rgba(25, 33, 56, 0.72);
    --surface-glass-border: rgba(255, 255, 255, 0.07);

    /* Accent hover variants — slightly brighter for dark context */
    --accent-blue-hover:   #3B82F6;
    --accent-purple-hover: #A78BFA;
    --accent-gold-hover:   #FCD34D;

    /* Glow shadows render visibly on dark. Blue glow moved onto the accent
       blue so it reads as one system with the gradient. */
    --glow-blue:   0 0 28px rgba(59, 130, 246, 0.38);
    --glow-purple: 0 0 28px rgba(139, 92, 246, 0.38);
    --glow-gold:   0 0 28px rgba(251, 191, 36, 0.40);

    --focus-ring: 0 0 0 3px rgba(47, 178, 234, 0.55);
}

* { box-sizing: border-box; }

/* iOS focus-zoom guard: Safari/WKWebView auto-zooms when a focused text
   input has font-size < 16px, and doesn't zoom back out. Force >=16px on
   every text-entry control so focusing an input never zooms the page. */
input, select, textarea { font-size: 16px; }

/* ── iOS auto-zoom, which reads as "the page is wider than the screen" ────
   🚨 OWNER, 2026-10-02: "still going beyond the width of the page when the
      mastery modal opens" — the Send button sliced off the right edge. The
      page was FINE until the sheet opened, which is the whole clue.

   Safari on iOS ZOOMS THE PAGE IN whenever focus lands on a text field whose
   font-size is under 16px. Zooming makes the layout wider than the screen, so
   everything gains a horizontal scroll and the right edge is cut off. The
   deck's "Plan this deck" sheet calls `text.focus()` the instant it opens and
   its input was `font-size:14px`, so the sheet zoomed the page the moment it
   appeared.

   🔑 WHY NOT `maximum-scale=1` / `user-scalable=no` IN THE VIEWPORT META:
      that also stops the user pinch-zooming, which is an accessibility
      regression for anyone who needs to magnify text. Raising the field to
      16px removes the trigger instead of removing the capability.

   🔑 WHY `!important` AND NOT A FIX AT EACH FIELD: these sizes are written
      as INLINE styles on markup built in JS — 31 of them across app.js and
      educationV2.js — and an inline style beats any stylesheet selector. One
      rule that cannot be out-specified is the only way to cover them all,
      including the ones nobody has opened yet.

   🔑 WHY `pointer: coarse`: desktop never auto-zooms, so it keeps the sizes
      the design asked for. Checkbox/radio/range carry no text and are left
      alone — bumping them changes control geometry for no benefit. */
@media (pointer: coarse) {
    input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="color"]),
    select,
    textarea,
    /* 🔑 (2026-10-03) Rich-text fields zoom too. The deck editor's definition
       is a contenteditable <div> with an inline 13.5px font, which none of the
       selectors above match — focusing it was still zooming the page. */
    [contenteditable]:not([contenteditable="false"]) { font-size: 16px !important; }
}

/* 🔑 (2026-10-03) Nothing may make a PHONE page wider than the screen. One
   overflowing child (or a zoom that never fully reverts) gave the whole page a
   sideways scroll that iOS keeps, leaving content off-screen. Set on <html>
   only: there it applies to the viewport, so position:sticky (the deck
   editor's save bar, filter bars) keeps working. Putting it on <body> as well
   would make body its own scroll container and break sticky. */
@media (max-width: 900px) {
    html { overflow-x: hidden; }
}

html, body {
    margin: 0;
    padding: 0;
    background: var(--bg);
    color: var(--text);
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
    min-height: 100vh;
}

a { color: var(--brand-blue); text-decoration: none; font-weight: 600; }
a:hover { text-decoration: underline; }

.bg-glow {
    background: radial-gradient(circle at 20% 20%, rgba(37, 99, 235, 0.08), transparent 55%),
                radial-gradient(circle at 80% 80%, rgba(139, 92, 246, 0.08), transparent 55%),
                var(--bg);
}

/* Knovli dark: a stronger, higher-contrast wash so the near-black ground
   still has the mockup's depth behind the cards. */
[data-theme="dark"] .bg-glow {
    background: radial-gradient(circle at 18% 12%, rgba(59, 130, 246, 0.14), transparent 58%),
                radial-gradient(circle at 82% 78%, rgba(139, 92, 246, 0.13), transparent 58%),
                var(--bg);
}

/* ===== App shell (index.html) ===== */
.shell {
    /* Normal document scroll — the page body scrolls. The .topbar is
       pinned via position:sticky (below) rather than by locking the
       scroll model, so content always scrolls freely. */
    min-height: 100vh;
    display: flex;
    flex-direction: column;
}
.topbar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    /* iOS safe area: when the native Capacitor WebView overlays the
       status bar (StatusBar.overlaysWebView: true), the topbar sits
       under the notch / Dynamic Island / front camera. Grow it by the
       top inset and pad its content down so the glass surface fills the
       inset area. env() is 0 on web/Android, so this is a no-op there.
       box-sizing: border-box (global) keeps the content row at 64px. */
    height: calc(var(--topbar-height) + env(safe-area-inset-top, 0px));
    padding: env(safe-area-inset-top, 0px) 18px 0;
    /* The header wears the same near-black navy as the slide-out rail,
       so the two read as one surface. Deliberately NOT --surface-glass:
       that one is also worn by #pluginLeaveBar and other glass panels, so
       changing it would repaint surfaces this is not about.

       🔑 Its CONTENTS have to follow. .knovli-word, .menu-btn,
       .theme-toggle and .btn-secondary all take color: var(--text), which
       is #0F172A in light theme — near-black text on a near-black bar.
       Re-pointing the token INSIDE the bar fixes every one of them at once
       without touching their own rules. (.btn-primary sets color:white
       itself, so it is unaffected.) */
    background: #071C33;
    --text:       #E8EEF8;
    --text-muted: #9FB3C8;
    --text-dim:   #7C91A8;
    border-bottom: 1px solid rgba(255, 255, 255, 0.10);
    backdrop-filter: blur(10px);
    -webkit-backdrop-filter: blur(10px);
    flex-shrink: 0;
    z-index: 70; /* above sidebar so the menu toggle stays clickable */
    /* Subtle bottom-edge accent gradient — the layered/premium feel
       called for in the brief. Doesn't impose color on the topbar
       itself, just adds a 1px gradient highlight below. */
    /* Sticky so the header stays pinned to the top as the page body
       scrolls (works because no ancestor clips overflow). Keeps normal
       document scrolling intact. */
    position: sticky;
    top: 0;
}
/* The hover wash is rgba(15,23,42,.05) globally — a dark film that does not
   read on this ground. */
.topbar .btn-secondary:hover { background: rgba(255, 255, 255, 0.08); }
.topbar::after {
    content: '';
    position: absolute;
    left: 0; right: 0; bottom: -1px; height: 1px;
    background: linear-gradient(90deg, transparent 0%, var(--accent-cyan, #2FB2EA) 30%, var(--accent-purple, #8B5CF6) 70%, transparent 100%);
    opacity: 0.18;
    pointer-events: none;
}
.brand {
    display: flex;
    align-items: center;
    gap: 14px;
    font-weight: 700;
    font-size: 16px;
    min-width: 0;
}
.brand .emoji { font-size: 22px; line-height: 1; }
.brand-logo {
    height: 48px;
    width: auto;
    display: block; /* now visible at every viewport (sidebar no longer carries the logo) */
}

/* ─── Knovli wordmark ────────────────────────────────────────────
   The gradient K is the official mark (i/knovli-mark.png, generated from
   the uploaded source by apps/knovli/tools/make-icons.py). The word beside
   it is live text in the display font, so the lockup stays crisp at any
   size and follows the theme. Markup (topbar, sidebar, footer, login,
   register, verify-email):

     <span class="knovli-logo">
       <img class="knovli-mark" src="i/knovli-mark.png" alt="">
       <span class="knovli-word">knovli</span>
     </span>  */
.knovli-logo {
    display: inline-flex;
    align-items: center;
    gap: 10px;
    line-height: 1;
    text-decoration: none;
}
.knovli-mark {
    width: 30px;
    height: 30px;
    display: block;
    flex-shrink: 0;
    /* The mark is the real artwork (i/knovli-mark.png, 128px source with a
       transparent ground). object-fit keeps it honest if a size variant ever
       sets a non-square box. */
    object-fit: contain;
}
.knovli-word {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 25px;
    letter-spacing: -0.02em;
    color: var(--text);
}
/* Large variant for the standalone auth pages, where the logo is the
   page's masthead rather than a topbar item. */
.knovli-logo--lg { gap: 14px; }
.knovli-logo--lg .knovli-mark { width: 46px; height: 46px; }
.knovli-logo--lg .knovli-word { font-size: 38px; }

@media (max-width: 420px) {
    .knovli-word { font-size: 21px; }
    .knovli-mark { width: 26px; height: 26px; }
}
/* New menu toggle — a 2x2 grid of squares when the sidebar is
   closed; a left-facing caret when the sidebar is open. Both
   SVG icons live inside the same button; CSS hides one or the
   other based on body.sidebar-is-open. */
.menu-btn {
    background: transparent;
    color: var(--text);
    border: 1px solid var(--border-strong);
    border-radius: 8px;
    width: 38px;
    height: 38px;
    padding: 0;
    cursor: pointer;
    font-family: inherit;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    transition: background 0.12s ease, border-color 0.12s ease;
}
.menu-btn:hover { background: rgba(148, 163, 184, 0.10); border-color: var(--text-muted); }
.menu-btn:focus-visible { outline: 2px solid var(--brand-blue); outline-offset: 2px; }
.menu-btn-icon {
    width: 20px;
    height: 20px;
    display: block;
}
.menu-btn-icon-caret { display: none; }
body.sidebar-is-open .menu-btn-icon-grid  { display: none; }
body.sidebar-is-open .menu-btn-icon-caret { display: block; }
/* Legacy alias — sidebarToggleBtn used to be .hamburger-btn.
   Kept as a no-op selector in case any external CSS still refs it. */
.hamburger-btn { display: none; }

/* Theme toggle (Phase A) — sits next to Sign in/out in the topbar.
   Same look as .menu-btn so the topbar stays visually consistent.
   Icon swap: moon visible in LIGHT mode (click to go dark), sun
   visible in DARK mode (click to go light). */
.theme-toggle {
    background: transparent;
    color: var(--text);
    border: 1px solid var(--border-strong);
    border-radius: 8px;
    width: 38px;
    height: 38px;
    padding: 0;
    cursor: pointer;
    font-family: inherit;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    transition: background var(--motion-fast, 120ms) ease, border-color var(--motion-fast, 120ms) ease, transform var(--motion-fast, 120ms) ease;
}
.theme-toggle:hover { background: rgba(148, 163, 184, 0.10); border-color: var(--text-muted); }
.theme-toggle:active { transform: scale(0.94); }
.theme-toggle:focus-visible { outline: 2px solid var(--accent-cyan, #2FB2EA); outline-offset: 2px; }
.theme-icon { width: 18px; height: 18px; display: block; }
/* Default (light theme): show moon. Dark theme: show sun. */
.theme-icon-sun  { display: none; }
[data-theme="dark"] .theme-icon-moon { display: none; }
[data-theme="dark"] .theme-icon-sun  { display: block; }
.actions {
    display: flex;
    align-items: center;
    gap: 10px;
}

/* ===== Notification bell (notification center) ===== */
#mmEduNotifBtn { position: relative; overflow: visible; }
/* The bell glyph renders ~2/3 size (the SVG has no .theme-icon sizing). */
#mmEduNotifBtn svg { width: 16px; height: 16px; display: block; }
.mm-notif-badge {
    position: absolute;
    top: -5px;
    right: -5px;
    min-width: 17px;
    height: 17px;
    padding: 0 4px;
    border-radius: 9px;
    background: #DC2626;
    color: #fff;
    font-size: 10px;
    font-weight: 700;
    line-height: 17px;
    text-align: center;
    box-sizing: border-box;
    box-shadow: 0 0 0 2px var(--bg, #fff);
    pointer-events: none;
}

/* Bell swing "ring" + enlarge pulse (Part B). transform-origin at the
   top so it swings like a real bell. Classes are toggled from app.js for
   the animation's duration only. */
@keyframes mmNotifRing {
    0%   { transform: rotate(0); }
    10%  { transform: rotate(16deg); }
    25%  { transform: rotate(-14deg); }
    40%  { transform: rotate(11deg); }
    55%  { transform: rotate(-8deg); }
    70%  { transform: rotate(5deg); }
    85%  { transform: rotate(-2deg); }
    100% { transform: rotate(0); }
}
@keyframes mmNotifEnlarge {
    0%   { transform: scale(1); }
    30%  { transform: scale(1.28); }
    60%  { transform: scale(0.95); }
    100% { transform: scale(1); }
}
#mmEduNotifBtn.mm-notif-ring svg { transform-origin: 50% 4px; animation: mmNotifRing 0.9s ease-in-out; }
#mmEduNotifBtn.mm-notif-enlarge { animation: mmNotifEnlarge 0.6s ease-in-out; }
@media (prefers-reduced-motion: reduce) {
    #mmEduNotifBtn.mm-notif-ring svg,
    #mmEduNotifBtn.mm-notif-enlarge { animation: none; }
}

/* Dropdown panel — appended to <body>, positioned (fixed) under the bell
   by app.js. */
.mm-notif-panel {
    background: var(--card-bg, var(--bg, #fff));
    color: var(--text, #0B1020);
    border: 1px solid var(--border-strong, rgba(148,163,184,0.35));
    border-radius: 12px;
    box-shadow: 0 12px 34px rgba(0,0,0,0.22);
    z-index: 4000;
    max-height: 60vh;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    font-size: 13px;
}
.mm-notif-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 10px 12px;
    border-bottom: 1px solid var(--border, rgba(148,163,184,0.2));
    flex-shrink: 0;
}
.mm-notif-head-title { font-weight: 700; font-size: 13px; }
.mm-notif-head-actions { display: flex; align-items: center; gap: 10px; }
.mm-notif-clear,
.mm-notif-mark {
    background: transparent;
    border: none;
    color: var(--accent-cyan, #2563EB);
    font-size: 12px;
    font-weight: 600;
    cursor: pointer;
    padding: 2px 4px;
    border-radius: 6px;
    white-space: nowrap;
}
.mm-notif-mark { color: var(--text-muted, #64748B); }
.mm-notif-clear:hover,
.mm-notif-mark:hover { text-decoration: underline; }
.mm-notif-list { overflow-y: auto; }
.mm-notif-empty {
    padding: 28px 16px;
    text-align: center;
    color: var(--text-muted, #64748B);
}
/* In-panel "enable push" / "blocked" banner. The list itself works
   regardless of browser push permission. */
.mm-notif-banner {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 9px 12px;
    font-size: 12px;
    line-height: 1.4;
    color: var(--text-muted, #64748B);
    background: rgba(37,99,235,0.07);
    border-bottom: 1px solid var(--border, rgba(148,163,184,0.14));
}
.mm-notif-banner--blocked { background: rgba(148,163,184,0.10); }
.mm-notif-banner span { flex: 1; }
.mm-notif-enable {
    flex-shrink: 0;
    background: var(--brand-blue, #2563EB);
    color: #fff;
    border: 0;
    border-radius: 6px;
    padding: 5px 12px;
    font-family: inherit;
    font-weight: 700;
    font-size: 12px;
    cursor: pointer;
}
.mm-notif-enable:hover { filter: brightness(1.05); }
.mm-notif-row {
    display: flex;
    align-items: flex-start;
    gap: 8px;
    padding: 10px 12px;
    border-bottom: 1px solid var(--border, rgba(148,163,184,0.14));
    cursor: pointer;
}
.mm-notif-row:last-child { border-bottom: none; }
.mm-notif-row:hover { background: rgba(148,163,184,0.10); }
.mm-notif-row--unread { background: rgba(37,99,235,0.07); }
.mm-notif-row--unread:hover { background: rgba(37,99,235,0.13); }
.mm-notif-row-main { flex: 1; min-width: 0; }
.mm-notif-row-title { font-weight: 700; margin-bottom: 2px; word-break: break-word; }
.mm-notif-row-body { color: var(--text-muted, #64748B); margin-bottom: 3px; word-break: break-word; }
.mm-notif-row-time { color: var(--text-muted, #94A3B8); font-size: 11px; }
.mm-notif-row-x {
    flex-shrink: 0;
    background: transparent;
    border: none;
    color: var(--text-muted, #94A3B8);
    cursor: pointer;
    font-size: 13px;
    line-height: 1;
    padding: 2px 4px;
    border-radius: 6px;
}
.mm-notif-row-x:hover { color: var(--text, #0B1020); background: rgba(148,163,184,0.18); }
.who {
    color: var(--text-muted);
    font-size: 13px;
}

/* ===== Body layout: sidebar + main panel ===== */
.shell-body {
    flex: 1;
    display: flex;
    min-height: 0;
    position: relative;
}
.sidebar {
    /* Slide-out drawer at every viewport. Starts below the
       topbar (top: var(--topbar-height)) so the menu toggle in the
       topbar stays accessible while the sidebar is open. Default
       state is `translateX(-100%)` (off-screen left); the
       .sidebar-open class on .shell-body slides it in. */
    position: fixed;
    top: calc(var(--topbar-height) + env(safe-area-inset-top, 0px));
    bottom: 0;
    left: 0;
    width: 260px;
    z-index: 60;
    flex-shrink: 0;
    /* An explicit near-black navy in BOTH themes. Deliberately not
       var(--bg-card): that token is every card on the page, so moving it
       would repaint surfaces this is not about.

       🔑 The rail's CONTENTS follow it. The nav rows, section labels, the
       account card and the footer all take --text / --text-muted /
       --text-dim / --border, which are DARK in light theme and would be
       invisible here. Re-pointing the tokens INSIDE the rail fixes every
       one of them at once without touching their own rules. */
    background: #071C33;
    --text:       #E8EEF8;
    --text-muted: #9FB3C8;
    --text-dim:   #7C91A8;
    --border:     rgba(255, 255, 255, 0.10);
    border-right: 1px solid rgba(255, 255, 255, 0.10);
    display: flex;
    flex-direction: column;
    padding: 16px 14px;
    overflow-y: auto;
    transform: translateX(-100%);
    transition: transform var(--motion-medium, 220ms) var(--motion-ease, ease);
    box-shadow: var(--shadow-md, 4px 0 24px rgba(15, 23, 42, 0.10));
}
.shell-body.sidebar-open .sidebar { transform: translateX(0); }
/* Sidebar's internal header is retired — the logo lives in the
   topbar across all viewports, and the close action moved to the
   menu toggle in the topbar (which transforms into a left-caret
   when the sidebar is open). Keeping the DOM node so older JS
   that queries it doesn't break; just hidden. */
.sidebar-header {
    display: none;
    position: relative;
    align-items: center;
    justify-content: center;
    padding: 18px 4px;
    margin-bottom: 10px;
    border-bottom: 1px solid var(--border);
    flex-shrink: 0;
}
/* The sidebar + footer marks are the Knovli wordmark component now, not
   <img>. These rules size the component instead of an image box; without
   them the img-era `display:block` / fixed width would break the inline-flex
   lockup (mark stacked over the word, or squashed to 26px). */
.sidebar-logo {
    display: inline-flex;
    max-width: 100%;
}
.sidebar-logo .knovli-mark { width: 34px; height: 34px; }
.sidebar-logo .knovli-word { font-size: 28px; }
.sidebar-close-btn {
    display: none; /* desktop: hidden; mobile media query reveals it */
    position: absolute;
    right: 4px;
    top: 50%;
    transform: translateY(-50%);
    background: transparent;
    color: var(--text-muted);
    border: 1px solid var(--border-strong);
    border-radius: 8px;
    padding: 6px 11px;
    font-size: 16px;
    line-height: 1;
    cursor: pointer;
    font-family: inherit;
}
.sidebar-close-btn:hover {
    background: rgba(148, 163, 184, 0.10);
    color: var(--text);
}
/* Pin button — the counterpart to the X. Shown in the sidebar header
   ONLY on desktop while the menu is being used the "old way" (undocked
   slide-out drawer, open). Clicking it re-docks the menu (pins it open)
   so it behaves like a fresh desktop load again. Hidden everywhere else
   (mobile, and docked mode where the X takes over). */
.sidebar-pin-btn {
    display: none;
    background: transparent;
    color: var(--text-muted);
    border: 1px solid var(--border-strong);
    border-radius: 8px;
    padding: 5px 10px;
    font-size: 15px;
    line-height: 1;
    cursor: pointer;
    font-family: inherit;
}
.sidebar-pin-btn:hover {
    background: rgba(148, 163, 184, 0.10);
    color: var(--text);
}
.sidebar-nav {
    display: flex;
    flex-direction: column;
    gap: 4px;
}
/* Section header inside the sidebar nav — small, uppercase,
   muted. Used to group "Learn" + "Play" rows; the standalone
   Schoolhouse button sits above the first header without one. */
.sidebar-section-label {
    margin-top: 14px;
    padding: 6px 13px 4px;
    color: var(--text-dim);
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
}
/* First section label has less top spacing since it follows the
   single Schoolhouse button directly. */
.sidebar-nav > .sidebar-section-label:first-child,
.sidebar-section-label + .sidebar-section-label {
    margin-top: 6px;
}
/* "Soon" badge on the not-yet-wired Play modes. Subtle so it
   doesn't compete with the row's icon + label. */
.sidebar-nav-btn .sn-soon {
    margin-left: auto;
    padding: 2px 8px;
    border-radius: 999px;
    font-size: 10px;
    font-weight: 700;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--text-dim);
    background: rgba(15, 23, 42, 0.06);
    border: 1px solid rgba(15, 23, 42, 0.08);
}
/* Same badge on dark: the light-surface fill was invisible against the
   near-black rail. */
[data-theme="dark"] .sidebar-nav-btn .sn-soon {
    background: rgba(255, 255, 255, 0.07);
    border-color: rgba(255, 255, 255, 0.12);
}
/* Sidebar account card — avatar/name row plus the level card beneath it. */
.sidebar-user-top { display: flex; align-items: center; gap: 10px; min-width: 0; }
.sidebar-user-level {
    display: block; width: 100%; padding: 9px 10px;
    background: rgba(37, 99, 235, 0.06); border: 1px solid rgba(37, 99, 235, 0.22);
    border-radius: 10px; cursor: pointer; font-family: inherit; text-align: left;
    transition: background 0.12s ease, border-color 0.12s ease;
}
.sidebar-user-level:hover { background: rgba(37, 99, 235, 0.12); border-color: rgba(37, 99, 235, 0.40); }
.sidebar-user-level .sul-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.sidebar-user-level .sul-level { color: var(--text); font-weight: 700; font-size: 12.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sidebar-user-level .sul-chev { color: var(--text-muted); font-size: 15px; line-height: 1; flex-shrink: 0; }
/* The track keeps its own height so a 0% fill still reads as an empty bar. */
.sidebar-user-level .sul-bar { display: block; height: 6px; margin: 7px 0 5px; border-radius: 999px; background: rgba(15, 23, 42, 0.10); overflow: hidden; }
[data-theme="dark"] .sidebar-user-level .sul-bar { background: rgba(255, 255, 255, 0.10); }
.sidebar-user-level .sul-fill { display: block; height: 100%; width: 0; border-radius: 999px; background: linear-gradient(90deg, #2563EB, #8B5CF6); transition: width 0.35s ease; }
.sidebar-user-level .sul-xp { display: block; color: var(--text-muted); font-size: 11px; line-height: 1.3; }

.sidebar-nav-btn, .sidebar-more-btn {
    display: flex;
    align-items: center;
    gap: 12px;
    background: transparent;
    color: var(--text-muted);
    border: 1px solid transparent;
    border-radius: 10px;
    padding: 11px 13px;
    font-family: inherit;
    font-size: 14px;
    font-weight: 600;
    text-align: left;
    cursor: pointer;
    transition: background 0.12s ease, color 0.12s ease, border-color 0.12s ease;
}
.sidebar-nav-btn:hover, .sidebar-more-btn:hover {
    background: rgba(15, 23, 42, 0.05);
    color: var(--text);
}
[data-theme="dark"] .sidebar-nav-btn:hover, [data-theme="dark"] .sidebar-more-btn:hover {
    background: rgba(255, 255, 255, 0.05);
}
/* Phase B — active state: gradient fill, gradient border, soft
   accent glow, and a 3px left-edge accent that doubles as a "you
   are here" indicator. Premium-gaming feel without going neon. */
.sidebar-nav-btn.active {
    background: linear-gradient(135deg, rgba(37, 99, 235, 0.14), rgba(139, 92, 246, 0.14));
    border-color: rgba(37, 99, 235, 0.45);
    color: var(--brand-blue);
    box-shadow: 0 0 0 1px rgba(47, 178, 234, 0.18), var(--glow-blue, 0 0 24px rgba(47, 178, 234, 0.25));
    position: relative;
}
.sidebar-nav-btn.active::before {
    content: '';
    position: absolute;
    left: 0;
    top: 8px;
    bottom: 8px;
    width: 3px;
    border-radius: 0 3px 3px 0;
    background: linear-gradient(180deg, var(--accent-cyan, #2FB2EA), var(--accent-purple, #8B5CF6));
}
/* Knovli: the active row is a blue-tinted pill with white text, per the
   2026-09-01 mockup — the cyan-on-cyan version read as a second accent
   competing with the blue→violet gradient. */
[data-theme="dark"] .sidebar-nav-btn.active {
    background: linear-gradient(135deg, rgba(59, 130, 246, 0.26), rgba(139, 92, 246, 0.20));
    border-color: rgba(59, 130, 246, 0.50);
    color: #FFFFFF;
}
.sidebar-nav-btn.locked {
    opacity: 0.65;
}
.sidebar-nav-btn.locked:hover {
    opacity: 0.85;
    background: rgba(15, 23, 42, 0.04);
}
/* Click-launch state — pulses the button after click so the user
   can see their click registered even before the plugin renders.
   Added via JS in the sidebar handler; removed after ~1.8s. */
.sidebar-nav-btn.is-launching {
    background: linear-gradient(135deg, rgba(37, 99, 235, 0.18), rgba(139, 92, 246, 0.18));
    border-color: rgba(37, 99, 235, 0.55);
    color: var(--brand-blue);
}
.sidebar-nav-btn.is-launching .sn-icon {
    animation: mmEduPulse 0.9s ease-in-out infinite;
}
@keyframes mmEduPulse {
    0%, 100% { opacity: 1; transform: scale(1); }
    50%      { opacity: 0.55; transform: scale(0.92); }
}
.sidebar-nav-btn .sn-icon {
    font-size: 18px;
    line-height: 1;
    flex-shrink: 0;
}
.sidebar-nav-btn .sn-label { flex: 1; }
.sidebar-footer {
    margin-top: auto;
    padding-top: 16px;
    color: var(--text-dim);
    font-size: 12px;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 8px;
    border-top: 1px solid var(--border);
}
.sidebar-chip {
    background: rgba(22, 163, 74, 0.10);
    color: #16A34A;
    border: 1px solid rgba(22, 163, 74, 0.35);
    padding: 4px 10px;
    border-radius: 999px;
    font-size: 11px;
    font-weight: 600;
    text-align: center;
    align-self: flex-start;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
/* 🚨 Five treatments below already had a [data-theme="dark"] variant,
   because their light-theme values are dark films or fills that vanish on a
   near-black ground. The rail is dark in BOTH themes now, so those variants
   apply unconditionally inside it.

   Placed AFTER the [data-theme="dark"] rules on purpose: these selectors
   carry the same specificity (0,3,0), so source order is what decides. */
.sidebar .sidebar-nav-btn:hover,
.sidebar .sidebar-more-btn:hover       { background: rgba(255, 255, 255, 0.05); }
.sidebar .sidebar-nav-btn.locked:hover { background: rgba(255, 255, 255, 0.04); }
.sidebar .sidebar-nav-btn .sn-soon {
    background: rgba(255, 255, 255, 0.07);
    border-color: rgba(255, 255, 255, 0.12);
}
.sidebar .sidebar-user-level .sul-bar  { background: rgba(255, 255, 255, 0.10); }
.sidebar .sidebar-nav-btn.active {
    background: linear-gradient(135deg, rgba(59, 130, 246, 0.26), rgba(139, 92, 246, 0.20));
    border-color: rgba(59, 130, 246, 0.50);
    color: #FFFFFF;
}

.sidebar-email {
    color: var(--text-muted);
    font-size: 12px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
/* When the user is signed in, the email becomes a button that
   opens the Knovli Profile plugin. Styled to read
   like a subtle link rather than a heavy button so it doesn't
   compete with the Plans & pricing CTA underneath. */
.sidebar-email-btn {
    appearance: none;
    background: transparent;
    border: none;
    color: var(--text-muted);
    font-family: inherit;
    font-size: 12px;
    font-weight: 600;
    text-align: left;
    cursor: pointer;
    padding: 4px 0;
    width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    transition: color 0.12s ease;
}
.sidebar-email-btn:hover,
.sidebar-email-btn:focus-visible {
    color: var(--brand-blue);
    text-decoration: underline;
    outline: none;
}
.sidebar-plans-btn {
    background: transparent;
    color: var(--brand-blue);
    border: 1px solid rgba(37, 99, 235, 0.30);
    border-radius: 8px;
    padding: 7px 10px;
    font-family: inherit;
    font-size: 12.5px;
    font-weight: 600;
    cursor: pointer;
    text-align: center;
    transition: background 0.12s ease, border-color 0.12s ease;
}
.sidebar-plans-btn:hover {
    background: rgba(37, 99, 235, 0.08);
    border-color: rgba(37, 99, 235, 0.50);
}
/* Prominent Help button — filled blue→purple gradient so it reads as
   the standout action in the sidebar footer (heavier than the outlined
   Plans button above it). */
.sidebar-help-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    background: linear-gradient(135deg, #2563EB, #8B5CF6);
    color: #fff;
    border: none;
    border-radius: 10px;
    padding: 11px 12px;
    font-family: inherit;
    font-size: 13.5px;
    font-weight: 700;
    cursor: pointer;
    text-align: center;
    box-shadow: 0 2px 8px rgba(37, 99, 235, 0.28);
    transition: transform 0.12s ease, box-shadow 0.12s ease, filter 0.12s ease;
}
.sidebar-help-btn .sn-icon {
    font-size: 15px;
}
.sidebar-help-btn:hover {
    filter: brightness(1.05);
    box-shadow: 0 4px 14px rgba(37, 99, 235, 0.38);
    transform: translateY(-1px);
}
.sidebar-help-btn:focus-visible {
    outline: 2px solid #8B5CF6;
    outline-offset: 2px;
}
/* Curtain that drops over the main content when the sidebar
   slides out. Starts below the topbar so the topbar (and the
   menu-toggle's caret state) stays interactable above the
   backdrop. Visible at every viewport when .sidebar-open. */
.sidebar-backdrop {
    display: none;
    position: fixed;
    top: calc(var(--topbar-height) + env(safe-area-inset-top, 0px));
    left: 0;
    right: 0;
    bottom: 0;
    background: rgba(15, 23, 42, 0.40);
    backdrop-filter: blur(2px);
    z-index: 55;
}
.shell-body.sidebar-open .sidebar-backdrop { display: block; }
.main-panel {
    flex: 1;
    /* 🚨 NO `overflow-y: auto` HERE. The page BODY scrolls (see .shell), and
       this panel has no height constraint, so it never scrolled — measured:
       docScrolls true, panelScrolls false. But `overflow-y: auto` still made
       it a SCROLL CONTAINER, which becomes the containing block for any
       `position: sticky` descendant, so a sticky filter bar inside a view
       tracked the page instead of pinning (2026-09-18). Removing it is what
       lets the quiz-picker filters stick. `_showPanel` still sets
       `.scrollTop = 0` on it; that was already a no-op and stays harmless. */
    /* Bottom inset so the last scrolled content (and the footer) clears
       the iOS home indicator; env() is 0 on web/Android. */
    padding: 24px 22px calc(24px + env(safe-area-inset-bottom, 0px));
    /* Published so a FULL-BLEED child (the hero band) can cancel exactly this
       inset instead of guessing at it — guessing is what overflowed the page
       horizontally last time. Kept in sync with the padding above and with the
       phone override further down. */
    --kv-bleed-x: 22px;
    --kv-bleed-top: 24px;
    min-width: 0;
    /* Animate the push-right/back when the desktop sidebar docks/undocks. */
    transition: margin-left var(--motion-medium, 220ms) var(--motion-ease, ease);
}

/* ===== Desktop docked sidebar ==================================
   On desktop (≥901px) the side menu defaults to pinned-open: the
   sidebar sits as a fixed left rail and the main panel is pushed
   right to make room — no overlay, no backdrop — and clicking a nav
   row leaves it open. The X in the sidebar header undocks it, which
   app.js persists (localStorage mmEduSidebarDocked=false) and reverts
   to the slide-out drawer + topbar toggle for the rest of the session.
   app.js adds body.mmedu-sidebar-docked only when the pref is on AND
   the viewport is desktop; every rule here is desktop-only, so mobile
   (≤900px) is completely unaffected. */
/* ===== Sidebar: push above tablet, overlay at tablet and below =====
   Above the tablet breakpoint the menu behaves like a rail: opening it
   PUSHES the content across and there is NO dimming curtain, so the page
   stays readable and clickable while the menu is open. At tablet width
   and below there isn't room to give up 260px, so it stays a drawer that
   slides OVER the content with a backdrop.

   901px is this file's existing desktop boundary (matching the docked-
   sidebar rules and the .main-panel/topbar queries) rather than a new
   number — one split to reason about, not two. A landscape tablet at
   1024px therefore gets the push behaviour, which is the right call at
   that width: 1024 - 260 still leaves 764px of content.

   The main panel already transitions margin-left (added for docking), so
   the push animates in step with the sidebar's own slide. */
@media (min-width: 901px) {
    .shell-body.sidebar-open .main-panel { margin-left: 260px; }
    /* No curtain: content beside the menu stays usable. */
    .shell-body.sidebar-open .sidebar-backdrop { display: none !important; }
    /* The drop shadow reads as "floating over"; docked/pushed should read
       as part of the layout, so drop it and let the border do the work. */
    .shell-body.sidebar-open .sidebar { box-shadow: none; }
}

@media (min-width: 901px) {
    body.mmedu-sidebar-docked .sidebar {
        transform: translateX(0);
        box-shadow: none;
    }
    body.mmedu-sidebar-docked .main-panel {
        margin-left: 260px;
    }
    /* No dimming curtain while docked. */
    body.mmedu-sidebar-docked .sidebar-backdrop {
        display: none !important;
    }
    /* The topbar toggle is now the SINGLE, always-present menu control —
       never hidden. (It used to be hidden while docked, which is why the
       in-menu ✕ had to exist at all.) */
    /* The sidebar header is retired along with the ✕ and 📌 it held —
       it stays display:none at every viewport. Revealing it now would
       float an EMPTY band over the menu's top-right corner. */
    body.mmedu-sidebar-docked .sidebar-pin-btn,
    .shell-body.sidebar-open .sidebar-close-btn {
        display: none;
    }
}
/* Top-level view containers — full-width so plugins (Schoolhouse,
   format launchers) and the Curriculum Library grid can expand on
   large screens. Individual sub-views still cap their own widths
   inline where appropriate (My Content + Community both use a
   max-width: 880px inner wrapper). */
.view-panel {
    width: 100%;
}
/* Pre-mount placeholder card: center inside the main panel. */
#content { display: flex; justify-content: center; padding: 24px 0; }
#content:empty { display: none; }
.recently-used-section { margin-bottom: 22px; }

/* ─── Phase B — Home / Learning Command Center ─────────────────
   Scaffold layout for the redesigned dashboard. Phase C wires real
   data into the placeholder cards. Uses the Phase A token system
   throughout so dark theme flips automatically. */
.cc-wrap {
    /* No max-width cap — Home (Learning Command Center) spans the
       full main-panel width so the dashboard reads as a real cockpit
       on wide monitors. .main-panel already has its own padding. */
    width: 100%;
    display: flex;
    flex-direction: column;
    gap: 22px;
}
.cc-hero {
    background: linear-gradient(135deg, rgba(37, 99, 235, 0.10) 0%, rgba(139, 92, 246, 0.10) 50%, rgba(251, 191, 36, 0.10) 100%);
    border: 1px solid var(--surface-glass-border, var(--border));
    border-radius: var(--radius-lg, 16px);
    padding: 28px 32px;
    position: relative;
    overflow: hidden;
}
.cc-hero::before {
    content: '';
    position: absolute;
    inset: 0;
    /* Rounded with the hero. The hero used to rely on `overflow: hidden` to
       clip this glow, which also clipped anything a child opened — the Share
       menu was cut off by the cards below (owner, 2026-09-17). */
    border-radius: inherit;
    background: radial-gradient(circle at top right, rgba(47, 178, 234, 0.12), transparent 60%);
    pointer-events: none;
}
/* Standing row — greeting + rank + streak + home art in ONE grid.
   They share a container because the two layouts put the rank card in
   different ROWS, and CSS cannot move an element between parents. Auto
   placement (rather than named areas) matters: the streak card self-hides
   when it has no data, and with areas that would leave a hole where a card
   should be. Flowing, the survivors close up. */
.cc-standing {
    display: grid;
    /* Three equal tiles — identity, rank, store. They are deliberately SMALLER
       than before: this band is a header, not the content, and shrinking it is
       what makes room for the quick links underneath. */
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 12px;
    align-items: stretch;
    margin-bottom: 14px;
}
/* Placement is explicit because DOM order encodes the MOBILE layout (art
   first, the two cards under it) while desktop wants the art on the right.
   Safe to pin now that the streak card — the only child that self-hid, which
   would have left a hole — has moved out to sit above the XP chart. */
.cc-standing > .cc-idcard    { grid-column: 1; grid-row: 1; }
.cc-standing > .cc-rankcard  { grid-column: 2; grid-row: 1; }
.cc-standing > .cc-storecard { grid-column: 3; grid-row: 1; }
/* Quick links span the whole band on their own line. */
.cc-standing > .cc-quicklinks { grid-column: 1 / -1; grid-row: 2; }
/* The store card can hide itself (no link configured, or native), and an
   explicitly-placed empty column 3 would leave a hole. Falling back to two
   columns keeps the row balanced when it does. */
.cc-standing:not(:has(.cc-storecard:not([hidden]))) {
    grid-template-columns: repeat(2, minmax(0, 1fr));
}
/* Header cards are capped at 150px so the row stays a band across the top
   rather than growing with whatever is tallest. The cap goes on the ROW's
   children, not the row: `align-items: stretch` then makes all three exactly
   as tall as the shortest constraint, so they line up instead of stair-
   stepping. The art is capped too — cover-cropped, it would otherwise take
   whatever height it was given. */
/* Shorter than the old 150px: the band shrank so the quick links fit above
   the fold with it. 128 rather than 118 because the store tile is the tallest
   thing in the row — 10px padding, a title line, and two 38px badges stacked
   comes to ~120, and a cap below that silently CLIPS the second badge rather
   than shrinking it. */
.cc-standing > .cc-rankcard,
.cc-standing > .cc-idcard,
.cc-standing > .cc-storecard { max-height: 128px; overflow: hidden; }
/* The .cc-standing streak overrides (condensed padding, smaller count, dots
   and labels) went with the card: it is back in .cc-dash-col at full width,
   which is the size those styles were shrinking it from. Left behind they
   would have matched nothing and read as though the card were still here. */

/* Identity card — the avatar over the name, mirroring the rank card's badge
   over its name so the pair reads as one unit. */
.cc-idcard {
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: 14px;
    padding: 12px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 8px;
    text-align: center;
    min-width: 0;
}
/* ONE size, --cc-tile-art, shared by the avatar and the rank badge. They must
   be identical, and a single token is the only way that is actually enforced —
   a comment saying "matched to the badge" over a hardcoded 46px was what let
   them drift ~30px apart in the first place.
   
   ⚠️ Sized, not stretched. Letting the art fill the card (`flex: 1 1 auto`)
   does make the pair match, but it eats the whole 128px cap: the username and
   the level name then get pushed past `overflow: hidden` and clipped off the
   bottom. The budget is 128 − 24 padding − 8 gap − ~18 name row ≈ 78px, so 56
   leaves real headroom for both lines. */
.cc-standing { --cc-tile-art: 56px; }
.cc-id-avatar {
    flex: 0 0 auto;
    width: var(--cc-tile-art);
    height: var(--cc-tile-art);
    border-radius: 50%;
    overflow: hidden;
    display: flex;
    align-items: center;
    justify-content: center;
    /* The initial is the fallback when there is no picture — and the fallback
       for a picture that fails to load, which the JS swaps back in. */
    background: linear-gradient(135deg, #2563EB, #8B5CF6);
    color: #fff;
    font-family: var(--font-display);
    font-size: 1.4rem;
    font-weight: 800;
    line-height: 1;
}
.cc-id-avatar > img { width: 100%; height: 100%; object-fit: cover; display: block; }
.cc-id-name {
    font-family: var(--font-display);
    font-size: 1rem;
    font-weight: 800;
    color: var(--text);
    line-height: 1.15;
    letter-spacing: var(--tracking-tight, -0.02em);
    /* A long display name must not widen the grid column or wrap to three
       lines inside a card capped at 150px. */
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Rank card: the badge and the rank name, centred, nothing else. The art is
   the card — it grows to whatever height the row lands on (the streak card
   beside it sets that), which is what "fill the box" needs. min-height:0 lets
   the flex child actually shrink; without it the image floors at its
   intrinsic size and pushes the row taller. */
.cc-rankcard {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 8px;
    padding: 12px;
    text-align: center;
}
.cc-rank-art {
    /* Same token as .cc-id-avatar above — that is the whole point of it. */
    flex: 0 0 auto;
    width: var(--cc-tile-art);
    height: var(--cc-tile-art);
    display: flex;
    align-items: center;
    justify-content: center;
    /* The placeholder trophy is a text glyph, so it needs a type size; a real
       badge replaces it with an <img> and ignores this. */
    font-size: 2.4rem;
    line-height: 1;
}
.cc-rank-art > img,
.cc-rank-badge-img {
    max-width: 100%;
    max-height: 100%;
    width: auto;
    height: auto;
    object-fit: contain;
    display: block;
}
.cc-rank-name {
    font-family: var(--font-display);
    font-size: 1rem;
    font-weight: 800;
    color: var(--text);
    line-height: 1.15;
    letter-spacing: var(--tracking-tight, -0.02em);
    /* Clamped exactly like .cc-id-name. Both art boxes are "the card minus the
       name row", so a rank name that wrapped to two lines would shrink the
       badge and NOT the avatar — putting the pair right back out of step. */
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Home art beside the streak. CONTAIN, not cover: the source is 1672x941
   (16:9) in a card nearer 2:1, so cover was shaving ~11% off the top and
   bottom — the artwork arrived cropped. contain fits the whole image inside
   the box and centres it, at the cost of a little card background showing at
   the sides, which is the right trade for a picture someone chose.
   Rounded to match the cards it sits with. */

/* Below tablet: the art takes the full width, and the identity and rank cards
   share the line under it. 767px so a portrait tablet (768) keeps the three-up
   row — the boundary the original request was framed around.
   The old rule flipped the rank card to a ROW here (badge beside the name)
   because it sat next to a greeting. It no longer does: it sits beside a card
   built the same way, so the badge stays ABOVE the name at every width and the
   two read as a matched pair. */
@media (max-width: 767px) {
    /* 2x2: identity and rank on the first line, the store card beside
       whatever follows. The brand-art tile that used to span the top is gone. */
    .cc-standing {
        grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
        align-items: stretch;
    }
    .cc-standing > .cc-idcard    { grid-column: 1; grid-row: 1; }
    .cc-standing > .cc-rankcard  { grid-column: 2; grid-row: 1; }
    .cc-standing > .cc-storecard { grid-column: 1 / -1; grid-row: 2; }
    /* Full width here, so the badges are not competing with anything for
       room — and the 128px cap no longer applies to a row of its own. */
    .cc-standing > .cc-storecard { max-height: none; }
    .cc-storecard-badge img { height: 44px; }
    .cc-standing > .cc-quicklinks { grid-column: 1 / -1; grid-row: 3; }
    /* 🚨 Every selector in this block is qualified with .cc-standing on
       purpose. The base .cc-quicklink* rules sit ~2000 lines BELOW this media
       query, and a media query adds NO specificity — so a bare `.cc-quicklinks`
       here loses to the later base rule. That is not hypothetical: the previous
       `display: flex` scroll row was silently overridden by the base
       `display: grid`, and only the negative margin survived, which is exactly
       the tiles-outside-the-card this is fixing.

       🚨 The quick links must NEVER wrap to a second row — and they must stay
       INSIDE the panel. This was a sideways-scrolling flex row bled past the
       panel edges with a negative margin; on a phone that read as the tiles
       spilling out of the card, so they now SHRINK to fit instead.

       grid-auto-flow: column, NOT repeat(4, …): Classes only appears for a
       teacher or an enrolled student, so a fixed four-column track would leave
       an empty quarter for everyone else. Auto-flow gives exactly as many equal
       columns as there are tiles — a row of 4 when there are 4, a row of 3
       when there are 3, filling the width either way.

       minmax(0, 1fr) rather than 1fr: a bare 1fr floors at min-content, so the
       widest label would push the row wider than the panel — which is the
       overflow this is fixing. */
    .cc-standing > .cc-quicklinks {
        display: grid;
        grid-auto-flow: column;
        grid-auto-columns: minmax(0, 1fr);
        gap: 6px;
        margin: 0;
        padding: 0;
    }
    /* Fluid, so the tiles fit the phone rather than one assumed width. The
       floor is sized for a 320px handset (iPhone SE), the ceiling keeps them
       from ballooning just before the desktop breakpoint takes over. */
    .cc-standing .cc-quicklink {
        min-width: 0;
        padding: 8px 3px;
        gap: 4px;
    }
    .cc-standing .cc-quicklink-icon {
        width: clamp(24px, 8.5vw, 40px);
        height: clamp(24px, 8.5vw, 40px);
    }
    .cc-standing .cc-quicklink-label {
        font-size: clamp(0.55rem, 2.5vw, 0.8rem);
        /* The labels are short ("My Decks" is the longest) so this should never
           bite — it is the guard for a longer one added later, which must
           truncate rather than reopen the overflow. */
        max-width: 100%;
        overflow: hidden;
        text-overflow: ellipsis;
    }
    /* Two cards to a phone's width: trim the artwork and type rather than let
       a long name push the pair out of shape. */
    .cc-standing > .cc-idcard { padding: 12px 10px; }
    /* Shrink the TOKEN, not one of the two elements — overriding just the
       avatar here is exactly how the pair came apart before. */
    .cc-standing { --cc-tile-art: 48px; }
    .cc-standing .cc-id-avatar { font-size: 1.2rem; }
    .cc-standing .cc-id-name,
    .cc-standing > .cc-rankcard .cc-rank-name { font-size: 0.9rem; }
}

/* `.cc-hero--bare` removed with the home greeting it existed for — it had
   exactly one user. .cc-hero itself stays: the Progress views render it and
   still want the card treatment, which is what the modifier was undoing. */

.cc-hero-eyebrow {
    color: var(--text-dim);
    font-family: var(--font-display, inherit);
    font-size: var(--text-2xs, 0.65rem);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: var(--tracking-wide, 0.05em);
    margin-bottom: 6px;
}
.cc-hero-greeting {
    color: var(--text);
    font-family: var(--font-display, inherit);
    font-size: var(--text-3xl, 1.875rem);
    font-weight: 700;
    letter-spacing: var(--tracking-tight, -0.02em);
    margin: 0 0 8px 0;
    line-height: 1.15;
}
.cc-hero-sub {
    color: var(--text-muted);
    font-size: var(--text-sm, 0.875rem);
    margin: 0;
    line-height: 1.55;
    max-width: 560px;
}

/* /progress hero: text left, level + rank chips right-aligned (owner, 2026-09-17). */
.kv-prog-hero {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 20px;
    flex-wrap: wrap;
    /* The Share menu drops out of the hero, so this one must not clip. */
    overflow: visible;
    /* …and it has to sit ABOVE the cards below, which come later in the
       document and would otherwise paint over the open menu. */
    position: relative;
    z-index: 5;
}
.kv-prog-hero-text { min-width: 0; flex: 1 1 280px; position: relative; }
.kv-standing {
    display: flex;
    gap: 10px;
    flex-wrap: wrap;
    justify-content: flex-end;
    margin-left: auto;
    position: relative;
}
.kv-standing[hidden] { display: none; }
/* 🚨 `.sidebar-nav-btn` below sets `display:flex`, and ANY author rule beats
   the browser's built-in `[hidden] { display:none }` — so the Knovli Learn
   entry showed even while it was marked hidden, and its "only once there is
   something to link to" check did nothing at all. Same fix as .kv-standing
   and .cc-lb-avatar above: say it in the author stylesheet. */
.kv-nav-link[hidden] { display: none !important; }
.kv-standing-chip {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 10px 14px 10px 10px;
    border: 1px solid var(--border);
    border-radius: 14px;
    background: var(--bg-card);
    color: var(--text);
    font: inherit;
    text-align: left;
    cursor: pointer;
    transition: border-color 0.15s ease, transform 0.15s ease;
}
.kv-standing-chip:hover { border-color: var(--brand-blue, #3B82F6); transform: translateY(-1px); }
.kv-standing-chip:focus-visible { outline: 2px solid var(--brand-blue, #3B82F6); outline-offset: 2px; }
.kv-standing-badge { flex-shrink: 0; display: flex; }
.kv-standing-text { display: flex; flex-direction: column; line-height: 1.2; min-width: 0; }
.kv-standing-kicker { font-size: 0.68rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-dim); }
.kv-standing-name { font-size: 0.98rem; font-weight: 800; }
.kv-standing-sub { font-size: 0.78rem; color: var(--text-muted); font-weight: 600; }
@media (max-width: 640px) {
    .kv-standing { width: 100%; justify-content: stretch; margin-left: 0; }
    .kv-standing-chip { flex: 1 1 0; min-width: 0; }
}

/* Any badge that opens a ladder. Resets button chrome where it wraps inline badges. */
.kv-ladder-btn { cursor: pointer; }
/* Only strip the browser's button chrome — size, weight and colour stay with
   .kv-cp-rank / .kv-lb-tier (a `font: inherit` here would out-rank them). */
button.kv-cp-rank, button.kv-lb-tier {
    background: none;
    border: 0;
    padding: 0;
    font-family: inherit;
    line-height: inherit;
    border-radius: 6px;
}
button.kv-cp-rank:hover, button.kv-lb-tier:hover { text-decoration: underline; }
button.kv-cp-rank:focus-visible, button.kv-lb-tier:focus-visible { outline: 2px solid var(--brand-blue, #3B82F6); outline-offset: 2px; }

.kv-ladder-img { width: 40px; height: 40px; border-radius: 10px; object-fit: contain; flex-shrink: 0; }
.kv-standing-badge .kv-ladder-img { width: 44px; height: 44px; }
.kv-ladder-img--blank {
    display: flex;
    align-items: center;
    justify-content: center;
    background: linear-gradient(135deg, rgba(59, 130, 246, 0.18), rgba(139, 92, 246, 0.18));
    color: var(--text);
    font-weight: 800;
    font-size: 1rem;
}
.kv-ladder-overlay {
    position: fixed;
    inset: 0;
    z-index: 12100;
    background: rgba(0, 0, 0, 0.62);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 16px;
}
.kv-ladder-dialog {
    background: var(--bg-card);
    color: var(--text);
    border: 1px solid var(--border);
    border-radius: 16px;
    width: 100%;
    max-width: 520px;
    max-height: 88vh;
    overflow: auto;
}
.kv-ladder-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 16px 20px 6px;
    position: sticky;
    top: 0;
    background: var(--bg-card);
    z-index: 1;
}
.kv-ladder-head h2 { margin: 0; font-size: 1.15rem; font-weight: 800; }
.kv-ladder-close { background: none; border: 0; font-size: 1.5rem; line-height: 1; color: var(--text-muted); cursor: pointer; padding: 4px 8px; border-radius: 8px; }
.kv-ladder-close:focus-visible { outline: 2px solid var(--brand-blue, #3B82F6); }
.kv-ladder-sub { margin: 0; padding: 0 20px 12px; color: var(--text-muted); font-size: 0.9rem; }
.kv-ladder-sub b { color: var(--text); }
.kv-ladder-list { list-style: none; margin: 0; padding: 4px 12px 16px; display: flex; flex-direction: column; gap: 6px; }
.kv-ladder-row {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 8px 10px;
    border-radius: 12px;
    border: 1px solid transparent;
}
.kv-ladder-row.is-current {
    border-color: var(--brand-blue, #3B82F6);
    background: linear-gradient(135deg, rgba(59, 130, 246, 0.14), rgba(139, 92, 246, 0.14));
}
.kv-ladder-name { display: flex; flex-direction: column; flex: 1; min-width: 0; line-height: 1.25; }
.kv-ladder-kicker { font-size: 0.68rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-dim); }
.kv-ladder-range { font-size: 0.82rem; color: var(--text-muted); font-weight: 600; white-space: nowrap; }
.kv-ladder-you {
    font-size: 0.7rem;
    font-weight: 800;
    padding: 3px 8px;
    border-radius: 999px;
    background: var(--brand-blue, #3B82F6);
    color: #fff;
    max-width: 90px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Stat-card grid — three KPI tiles. Phase C wires real values. */
.cc-stats {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 14px;
}
.cc-stat-card {
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg, 16px);
    padding: 18px 20px;
    display: flex;
    align-items: center;
    gap: 14px;
    transition: transform var(--motion-fast, 120ms) var(--motion-ease, ease), box-shadow var(--motion-fast, 120ms) var(--motion-ease, ease), border-color var(--motion-fast, 120ms) var(--motion-ease, ease);
    cursor: default;
}
.cc-stat-card:hover {
    transform: translateY(-2px);
    border-color: var(--border-strong);
    box-shadow: var(--shadow-md);
}
.cc-stat-icon {
    width: 44px;
    height: 44px;
    border-radius: var(--radius-md, 10px);
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 22px;
    flex-shrink: 0;
}
.cc-stat-icon.is-streak { background: linear-gradient(135deg, rgba(249, 115, 22, 0.18), rgba(239, 68, 68, 0.12)); color: var(--accent-orange, #F97316); }
.cc-stat-icon.is-xp     { background: linear-gradient(135deg, rgba(47, 178, 234, 0.18), rgba(139, 92, 246, 0.18)); color: var(--accent-cyan, #2FB2EA); }
.cc-stat-icon.is-rank   { background: linear-gradient(135deg, rgba(251, 191, 36, 0.18), rgba(249, 115, 22, 0.12)); color: var(--accent-gold, #FBBF24); }
/* 🚨 The three tints above were written and NEVER APPLIED — _ccProgressStatsHtml
   emitted a bare class="cc-stat-icon", so all six tiles on the Progress page
   rendered as colourless squares while the design had six distinct ones. These
   three complete the set (design/progressMockup.png). */
.cc-stat-icon.is-mastery { background: linear-gradient(135deg, rgba(99, 102, 241, 0.18), rgba(139, 92, 246, 0.12)); color: #6366F1; }
.cc-stat-icon.is-time    { background: linear-gradient(135deg, rgba(20, 184, 166, 0.18), rgba(59, 130, 246, 0.12)); color: #14B8A6; }
.cc-stat-icon.is-games   { background: linear-gradient(135deg, rgba(139, 92, 246, 0.18), rgba(236, 72, 153, 0.12)); color: #8B5CF6; }
.cc-stat-icon.is-wins    { background: linear-gradient(135deg, rgba(251, 191, 36, 0.20), rgba(34, 197, 94, 0.12)); color: #F59E0B; }

.cc-stat-eyebrow {
    color: var(--text-dim);
    font-family: var(--font-display, inherit);
    font-size: var(--text-2xs, 0.65rem);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: var(--tracking-wide, 0.05em);
    margin-bottom: 2px;
}
.cc-stat-value {
    color: var(--text);
    font-family: var(--font-display, inherit);
    font-size: var(--text-2xl, 1.5rem);
    font-weight: 700;
    line-height: 1.1;
}
.cc-stat-meta {
    color: var(--text-muted);
    font-size: var(--text-xs, 0.75rem);
    margin-top: 2px;
}

/* "Continue Learning" + "Active Challenges" — wider placeholder cards */
.cc-module {
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg, 16px);
    padding: 22px 24px;
}
.cc-module-row {
    /* Single column since the Continue Learning promo was removed: the row
       holds only the challenges card now, and a 2fr/1fr grid would have parked
       it in the wide track beside an empty one. */
    display: grid;
    grid-template-columns: 1fr;
    gap: 14px;
}
.cc-module-eyebrow {
    color: var(--text-dim);
    font-family: var(--font-display, inherit);
    font-size: var(--text-2xs, 0.65rem);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: var(--tracking-wide, 0.05em);
    margin-bottom: 4px;
}
.cc-module-title {
    color: var(--text);
    font-family: var(--font-display, inherit);
    font-size: var(--text-xl, 1.25rem);
    font-weight: 700;
    margin: 0 0 6px 0;
}
.cc-module-body {
    color: var(--text-muted);
    font-size: var(--text-sm, 0.875rem);
    line-height: 1.55;
}
.cc-module-empty {
    margin-top: 12px;
    padding: 14px 16px;
    background: var(--bg-secondary);
    border: 1px dashed var(--border-strong);
    border-radius: var(--radius-md, 10px);
    color: var(--text-muted);
    font-size: var(--text-sm, 0.875rem);
    text-align: center;
}
.cc-module-cta {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    margin-top: 14px;
    padding: 10px 18px;
    background: linear-gradient(135deg, var(--accent-blue, #2563EB), var(--accent-purple, #8B5CF6));
    color: #FFFFFF;
    text-decoration: none;
    border-radius: var(--radius-md, 10px);
    font-family: var(--font-body, inherit);
    font-weight: 700;
    font-size: var(--text-sm, 0.875rem);
    transition: transform var(--motion-fast, 120ms) var(--motion-ease, ease), box-shadow var(--motion-fast, 120ms) var(--motion-ease, ease);
}
.cc-module-cta:hover {
    transform: translateY(-1px);
    box-shadow: var(--glow-blue, 0 0 24px rgba(47, 178, 234, 0.25));
}

@media (max-width: 900px) {
    .cc-hero { padding: 22px 20px; }
    .cc-hero-greeting { font-size: 1.5rem; }
    /* auto-fit, not 1fr: the games played/won pair takes this row to SIX
       cards, and a single column made that a very long scroll on a tablet.
       220px is wide enough for the longest meta line ("This month 12 · Last
       month 8"), so a narrow phone still collapses to one column on its own
       rather than cramping it. */
    .cc-stats { grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); }
    .cc-module-row { grid-template-columns: 1fr; }
}

/* Mobile/tablet — slimmer main-panel padding. The sidebar
   slide-out + curtain + menu-toggle behavior is now base CSS
   (applies at every viewport), so this query only carries the
   small-screen typography/spacing tweaks. */
@media (max-width: 900px) {
    .main-panel { padding: 18px 16px; --kv-bleed-x: 16px; --kv-bleed-top: 18px; }
    /* Narrow phones — drop the menu button width slightly so the
       topbar's logo + actions don't overflow. */
    .menu-btn { width: 36px; height: 36px; }
    .brand-logo { height: 40px; }
}

/* Legacy `.main` selector kept for the verifying card layout — used
   only by the pre-auth #content block. */
.main {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 32px 22px;
}

/* ===== Cards (used by both pages) ===== */
.card {
    max-width: 560px;
    width: 100%;
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: 18px;
    padding: 40px;
    text-align: center;
    box-shadow: var(--shadow-lg);
}
.card .emoji {
    font-size: 64px;
    line-height: 1;
    margin-bottom: 14px;
    display: block;
}
.card h1 {
    font-size: 28px;
    margin: 0 0 8px 0;
    background: linear-gradient(135deg, var(--brand-blue), var(--brand-purple));
    -webkit-background-clip: text;
    background-clip: text;
    -webkit-text-fill-color: transparent;
}
.card .tagline {
    color: var(--text-muted);
    font-size: 15px;
    margin: 0 0 28px 0;
}
.status {
    background: rgba(37, 99, 235, 0.12);
    border: 1px solid rgba(37, 99, 235, 0.35);
    border-radius: 12px;
    padding: 18px 20px;
    text-align: left;
    margin-bottom: 24px;
}
.status-label {
    color: var(--brand-blue);
    font-weight: 600;
    font-size: 13px;
    text-transform: uppercase;
    letter-spacing: 0.5px;
    margin-bottom: 6px;
}
.status-body {
    color: var(--text);
    font-size: 14px;
    line-height: 1.5;
}
.footer {
    color: var(--text-muted);
    font-size: 13px;
    line-height: 1.6;
}

/* ===== Login shell ===== */
.login-shell {
    min-height: 100vh;
    min-height: 100dvh;                 /* dynamic viewport — accurate on mobile */
    display: flex;
    align-items: center;
    justify-content: center;
    /* Reserve the safe areas so the card never sits under the notch/home indicator. */
    padding: max(20px, env(safe-area-inset-top, 0px)) 20px max(20px, env(safe-area-inset-bottom, 0px));
    box-sizing: border-box;
}
/* Native iOS: the auth pages (login/register/verify) have no header to fill
   the top safe-area, so the light page background bled around the notch /
   camera island. Paint just that strip a solid dark. 0-height on web/Android
   (no notch), so it's invisible everywhere except notched iOS. */
.login-shell::before {
    content: '';
    position: fixed;
    top: 0; left: 0; right: 0;
    height: env(safe-area-inset-top, 0px);
    background: #0F172A;
    z-index: 10;
    pointer-events: none;
}
.login-card {
    max-width: 420px;
    width: 100%;
    /* Fit within the safe-area viewport; scroll internally if the content
       (SSO buttons + fields + links) is taller than the screen. */
    max-height: calc(100vh - 40px);
    max-height: calc(100dvh - max(20px, env(safe-area-inset-top, 0px)) - max(20px, env(safe-area-inset-bottom, 0px)));
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: 18px;
    padding: 32px;
    box-shadow: var(--shadow-lg);
    box-sizing: border-box;
}
.login-card h1 {
    font-size: 22px;
    margin: 0 0 2px 0;
    color: var(--text);
}
.login-card .sub {
    color: var(--text-muted);
    font-size: 13px;
}
.field { margin-bottom: 14px; }
.field label {
    display: block;
    font-size: 12px;
    color: var(--text-muted);
    text-transform: uppercase;
    letter-spacing: 0.5px;
    margin-bottom: 6px;
    font-weight: 600;
}
.field input {
    width: 100%;
    padding: 11px 13px;
    background: var(--bg-input);
    border: 1px solid var(--border-strong);
    border-radius: 10px;
    color: var(--text);
    /* 16px (not 14) so iOS Safari/WKWebView doesn't auto-zoom on focus —
       sub-16px inputs trigger a zoom that never resets to the viewport. */
    font-size: 16px;
    font-family: inherit;
}
.field input:focus {
    outline: none;
    border-color: var(--brand-blue);
    box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.25);
}

.btn-primary {
    width: 100%;
    padding: 12px 16px;
    background: linear-gradient(135deg, var(--brand-blue), var(--brand-purple));
    color: white;
    border: 0;
    border-radius: 10px;
    font-weight: 700;
    font-size: 14px;
    cursor: pointer;
    transition: transform 0.05s ease, box-shadow 0.2s ease;
}
.btn-primary:hover:not(:disabled) {
    box-shadow: 0 4px 14px rgba(37, 99, 235, 0.4);
}
.btn-primary:active:not(:disabled) { transform: translateY(1px); }
.btn-primary:disabled { opacity: 0.6; cursor: not-allowed; }

.btn-secondary {
    padding: 8px 14px;
    background: transparent;
    color: var(--text);
    border: 1px solid var(--border-strong);
    border-radius: 8px;
    font-weight: 600;
    font-size: 13px;
    cursor: pointer;
}
.btn-secondary:hover { background: rgba(15, 23, 42, 0.05); }

/* ── Buttons sitting ON the illustrated hero band ──────────────────────
   🚨 `.btn-secondary` is `background: transparent`, which is RIGHT on the dark
   topbar and WRONG on `.kv-dhero`: that band carries a background IMAGE, and on
   a phone it crops to a busier part of the picture, so an unfilled button stops
   reading as a button. Reported on My Decks; the Progress page's "Share" had the
   same problem sitting over the signpost artwork.
   🔑 SCOPED to the hero rather than changing `.btn-secondary` itself, so every
   other use — topbar included — keeps the transparency it wants. */
.kv-dhero .btn-secondary,
.kv-prog-hero .btn-secondary {
    background: var(--bg-card);
}
.kv-dhero .btn-secondary:hover,
.kv-prog-hero .btn-secondary:hover {
    background: var(--surface-2);
}

.divider {
    display: flex;
    align-items: center;
    gap: 12px;
    margin: 20px 0 14px;
    color: var(--text-dim);
    font-size: 12px;
    text-transform: uppercase;
    letter-spacing: 0.5px;
}
.divider::before, .divider::after {
    content: '';
    flex: 1;
    height: 1px;
    background: var(--border);
}

.sso-grid {
    display: flex;
    flex-direction: column;
    gap: 8px;
}
.sso-btn {
    display: flex;
    align-items: center;
    gap: 10px;
    width: 100%;
    padding: 11px 14px;
    background: #FFFFFF;
    color: #0F172A;
    border: 1px solid var(--border-strong);
    border-radius: 10px;
    font-weight: 600;
    font-size: 14px;
    cursor: pointer;
    transition: box-shadow 0.15s ease;
}
.sso-btn:hover:not(:disabled) {
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
.sso-btn:disabled { opacity: 0.6; cursor: not-allowed; }
/* "Use this button" — after a bad password, or arriving from the sign-up
   page's "made your account with Google?" link. */
.sso-btn--hint { border-color: #2563EB !important; box-shadow: 0 0 0 3px rgba(37,99,235,0.25); animation: ssoHint 1.2s ease 2; }
@keyframes ssoHint { 50% { box-shadow: 0 0 0 6px rgba(37,99,235,0.15); } }

.alert {
    padding: 10px 12px;
    border-radius: 10px;
    font-size: 13px;
    line-height: 1.5;
    margin-bottom: 16px;
}
.alert-error {
    background: var(--danger-bg);
    color: var(--danger);
    border: 1px solid rgba(239, 68, 68, 0.35);
}
.alert-success {
    background: var(--success-bg);
    color: var(--success);
    border: 1px solid rgba(34, 197, 94, 0.35);
}

.spinner {
    display: inline-block;
    width: 14px;
    height: 14px;
    border: 2px solid rgba(37, 99, 235, 0.20);
    border-top-color: var(--brand-blue);
    border-radius: 50%;
    animation: spin 0.7s linear infinite;
    margin-right: 6px;
    vertical-align: -2px;
}
/* White spinner variant for use on dark/coloured backgrounds */
.spinner-light {
    border: 2px solid rgba(255, 255, 255, 0.30);
    border-top-color: #FFFFFF;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* ===== Flashcard grid filter animation =====
   The set-card grids in My Sets + Community animate on filter change
   via a JS FLIP technique (see _flipCapture / _flipPlay in app.js):
   persisting cards slide from their old position to their new one,
   entering cards fade + scale in. The transition + transform are
   applied inline per-card, so no keyframe rule is needed here — this
   comment is a signpost to the JS. FLIP is skipped when the user
   prefers reduced motion. */

/* ===== Flashcard contenteditable definition ===== */
/* Empty-state placeholder for the contenteditable definition area
   in the My Flashcards create form. Browsers don't render
   placeholders on contenteditable, so we use ::before. We also
   match a stray <br> (which some browsers leave behind after the
   user deletes everything) via the data-empty attribute the JS
   keeps in sync. */
.mmEduCardDef[contenteditable="true"]:empty::before,
.mmEduCardDef[contenteditable="true"][data-empty="true"]::before {
    content: attr(data-placeholder);
    color: var(--text-dim);
    pointer-events: none;
}
.mmEduCardDef[contenteditable="true"] img {
    max-width: 100%;
    height: auto;
    vertical-align: middle;
    border-radius: 4px;
}
.mmEduCardDef[contenteditable="true"] a {
    color: var(--brand-blue);
    text-decoration: underline;
}

/* ===== Fullscreen plugin overlay (format launcher) ===== */
#pluginFullscreenOverlay {
    position: fixed;
    inset: 0;
    background: var(--bg);
    z-index: 9500;
    flex-direction: column;
    overflow: hidden;
}
#pluginFullscreenOverlay.open { display: flex !important; }

/* The header on every plugin modal — 1v1, 1vWorld, Group Play, Daily Quiz,
   Help Center, Creator Studio. It used to be a HARDCODED white
   (rgba(255,255,255,0.95)) with no dark-theme rule, so in dark mode a bright
   white slab sat above a dark app until (and only if) the game stamped
   data-game-mode-active. Now it wears exactly what .topbar wears — the same
   glass token, the same border token, the same 1px gradient accent — so the
   modal reads as part of the app instead of a window from somewhere else. */
#pluginLeaveBar {
    position: relative;          /* containing block for the ::after accent */
    display: flex;
    align-items: center;
    justify-content: space-between;
    /* iOS safe area: the fullscreen overlay covers y=0, so this Close
       bar sat under the notch / camera. Pad the top by the inset so the
       bar's own background fills the notch area. env() is 0 elsewhere. */
    padding: calc(10px + env(safe-area-inset-top, 0px)) 18px 10px;
    border-bottom: 1px solid var(--surface-glass-border, var(--border));
    background: var(--surface-glass, rgba(255, 255, 255, 0.92));
    backdrop-filter: blur(10px);
    -webkit-backdrop-filter: blur(10px);
    z-index: 1;
}
/* Same bottom-edge accent the topbar carries, so the two headers are
   recognisably the same component at a glance. */
#pluginLeaveBar::after {
    content: '';
    position: absolute;
    left: 0; right: 0; bottom: -1px; height: 1px;
    background: linear-gradient(90deg, transparent 0%, var(--accent-cyan, #2FB2EA) 30%, var(--accent-purple, #8B5CF6) 70%, transparent 100%);
    opacity: 0.18;
    pointer-events: none;
}
/* The plugin's Knovli icon, left of the title. */
#pluginLeaveBarIcon { display: none; flex-shrink: 0; }
#pluginLeaveBarIcon.has-icon { display: inline-flex; align-items: center; }
#pluginLeaveBarIcon .kvIcon { width: 22px; height: 22px; object-fit: contain; display: block; }
#pluginLeaveBarTitle {
    display: flex;
    align-items: center;
    gap: 10px;
    color: var(--text);
    font-weight: 700;
    font-size: 14px;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
#pluginCategoryLabel {
    color: var(--text-muted);
    font-size: 12px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.5px;
}
#pluginCategoryLabel:not(:empty)::after {
    content: '·';
    margin-left: 10px;
    color: var(--text-dim);
}
#pluginLeaveBtn {
    background: rgba(220, 38, 38, 0.10);
    color: #DC2626;
    border: 1px solid rgba(220, 38, 38, 0.40);
    padding: 6px 14px;
    border-radius: 8px;
    cursor: pointer;
    font-weight: 600;
    font-family: inherit;
    font-size: 13px;
}
#pluginLeaveBtn:hover { background: rgba(220, 38, 38, 0.25); }
/* On phones the modal close button is just the ✕ (the " Close" label is
   hidden) to save the cramped top-bar width. aria-label keeps it
   accessible. Desktop/tablet keep the full "✕ Close". */
@media (max-width: 768px) {
    #pluginLeaveBtn .plugin-leave-label { display: none; }
    #pluginLeaveBtn { padding: 6px 12px; font-size: 15px; }
}

#pluginFullscreenContent {
    flex: 1;
    overflow: auto;
    /* 🚨 The containing block for .kv-launch-veil. Without it the veil
       positions against the viewport and covers the leave bar — trapping the
       player behind a loading screen, which is the exact failure the Close
       button was just fixed to prevent. */
    position: relative;
    /* Bottom inset so plugin content (flashcards, games, results) clears
       the iOS home indicator. env() is 0 on web/Android. */
    padding-bottom: env(safe-area-inset-bottom, 0px);
    color: var(--text);
    background: var(--bg);
}
/* Many quiz/game FORMATS render their root with position:fixed / inset:0 /
   height:100vh, which is VIEWPORT-relative — so it starts at y=0, on top of
   the #pluginLeaveBar, and whatever the format draws at its own top is hidden
   behind that bar. A non-none `transform` makes #pluginFullscreenContent the
   containing block for those fixed descendants, confining them to the content
   box below the bar (which already pads past the notch).

   🚨 THIS USED TO BE SCOPED TO NATIVE ONLY — `body.mode-standalone` /
   `body.mode-capacitor` — "so web keeps its edge-to-edge game layout". That
   edge-to-edge layout IS the bug: on desktop web a fixed-root format paints
   over the leave bar. Owner-reported as "brain blitz and the quit button are
   hidden behind the modal header on desktop; on mobile it displays fine" —
   mobile was fine precisely BECAUSE the transform was there.
   Measured on this host: web root top=0 with Quit at y=12 behind a bar ending
   at y=50; native root top=50. Now every platform gets the containing block,
   which fixes every such format at once rather than one file at a time. */
#pluginFullscreenContent {
    /* translateZ(0) (not translate(0)) — an identity 2D transform can be
       optimized away by WebKit so it fails to establish the containing
       block; translateZ(0) reliably does. On retina iOS it does not blur. */
    transform: translateZ(0);
}
/* GAME MODE — persistent dark "modal" (mirrors consumer Mind Match).
   The competitive hosts (mmedu-1v1, mmedu-1vWorld, group-play) stamp the
   overlay data-game-mode-active while a round is live. Rather than hide the
   leave bar and go full-bleed (which let the game paint under the notch and
   let its top scroll away), we KEEP #pluginLeaveBar visible — its padding-top
   already fills the notch — hide ONLY its close button so the player exits
   through the game, and darken the overlay + bar so the game's dark
   background carries into the safe area. The format renders in the flex:1
   content box BELOW the bar (the translateZ(0) above bounds any position:fixed
   roots to that box), so nothing scrolls under the notch and the game fits
   inside the modal as best it can. Applies on every platform; env() is 0 off
   a notch, so the bar is just a plain header on web/Android. */
#pluginFullscreenOverlay[data-game-mode-active] {
    background: #0a0e1a;
}
#pluginFullscreenOverlay[data-game-mode-active] #pluginLeaveBar {
    background: rgba(10, 14, 26, 0.95);
    border-bottom-color: rgba(255, 255, 255, 0.10);
}
/* In a live round the game owns the colour; drop the accent so the bar reads
   as part of the game chrome rather than the app chrome. */
#pluginFullscreenOverlay[data-game-mode-active] #pluginLeaveBar::after { opacity: 0.10; }
#pluginFullscreenOverlay[data-game-mode-active] #pluginLeaveBarTitle,
#pluginFullscreenOverlay[data-game-mode-active] #pluginCategoryLabel {
    color: #e0e6ff;
}
#pluginFullscreenOverlay[data-game-mode-active] #pluginLeaveBtn {
    display: none;
}
/* ── COLLAPSE THE BAR DURING A LIVE ROUND (owner, 2026-09-25) ─────────
   The close button was already hidden above, so the player already had to
   exit through the game. What was left was the bar's TITLE, and the owner
   asked for that gone too.

   🔑 The bar itself STAYS — collapsed to nothing but the safe-area
   strip. That is the whole reason it survived the last two attempts to
   remove it: its padding-top is what fills the iPhone notch, and without it
   the game paints under the camera and its top row scrolls out of reach.
   On web and Android `env()` is 0, so this collapses to zero height and the
   game genuinely is full-bleed.

   ⚠️ It also HELPS the `position: fixed; top: 0; height: 100vh` formats that
   format-root-fits-the-modal.test.js exists for: with the bar collapsed, a
   format that wrongly sizes to the VIEWPORT no longer has its own Quit
   hidden behind a 52px header. */
#pluginFullscreenOverlay[data-game-mode-active] #pluginLeaveBar {
    padding: env(safe-area-inset-top, 0px) 0 0;
    min-height: 0;
    border-bottom: none;
}
#pluginFullscreenOverlay[data-game-mode-active] #pluginLeaveBarTitle { display: none; }
/* No accent line either — there is no bar left for it to underline. */
#pluginFullscreenOverlay[data-game-mode-active] #pluginLeaveBar::after { opacity: 0; }
/* Plugins render into #pluginContent which gets moved into
   #pluginFullscreenContent on open. Make sure the content fills
   the available area so plugin-internal layouts render correctly. */
#pluginFullscreenContent #pluginContent {
    width: 100%;
    min-height: 100%;
}

/* Footer — sits at the bottom of the main-panel scroll area. Scrolls
   with content; hidden when the plugin fullscreen overlay is open
   (overlay is a fixed-inset cover, so no z-index work needed). */
.mmedu-footer {
    margin-top: 40px;
    padding: 24px 16px 18px;
    border-top: 1px solid var(--border, rgba(15, 23, 42, 0.10));
    color: var(--text-muted, #8892b8);
    font-size: 13px;
}
.mmedu-footer-inner {
    max-width: 1100px;
    margin: 0 auto;
    display: flex;
    flex-direction: column;
    gap: 14px;
}
.mmedu-footer-brand {
    display: flex;
    align-items: center;
    gap: 10px;
    color: var(--text);
    font-weight: 700;
}
.mmedu-footer-mark { display: inline-flex; }
.mmedu-footer-mark .knovli-mark { width: 21px; height: 21px; }
.mmedu-footer-mark .knovli-word { font-size: 17px; }
.mmedu-footer-links {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 8px 18px;
}
.mmedu-footer-links a,
.mmedu-footer-links .mmedu-footer-linkbtn {
    color: var(--text-muted, #8892b8);
    text-decoration: none;
    font-size: 13px;
    font-family: inherit;
    background: transparent;
    border: 0;
    padding: 0;
    cursor: pointer;
}
.mmedu-footer-links a:hover,
.mmedu-footer-links .mmedu-footer-linkbtn:hover {
    color: var(--accent, #00f0ff);
    text-decoration: underline;
}
/* Social links (Studio Admin → App Admin → Social networks). The [hidden]
   rule is needed: `display:flex` below would otherwise beat the attribute
   and show an empty row. */
.mmedu-footer-social {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 8px 10px;
}
.mmedu-footer-social[hidden] { display: none; }
.mmedu-footer-social-label {
    color: var(--text-dim, #8892b8);
    font-size: 12px;
    margin-right: 2px;
}
.mmedu-footer-social-link {
    color: var(--text-muted, #8892b8);
    text-decoration: none;
    font-size: 13px;
    border: 1px solid var(--border, rgba(15, 23, 42, 0.14));
    border-radius: 999px;
    padding: 4px 11px;
    line-height: 1.3;
}
/* A network shown as its themed icon (i/icons_social_media). */
.mmedu-footer-social-icon {
    display: inline-flex;
    border-radius: 9px;
    line-height: 0;
    transition: transform 0.12s ease, box-shadow 0.12s ease;
}
.mmedu-footer-social-icon img {
    width: 34px;
    height: 34px;
    display: block;
}
.mmedu-footer-social-icon:hover,
.mmedu-footer-social-icon:focus-visible {
    transform: translateY(-2px);
    box-shadow: 0 4px 12px rgba(11, 70, 209, 0.28);
}
@media (prefers-reduced-motion: reduce) {
    .mmedu-footer-social-icon { transition: none; }
    .mmedu-footer-social-icon:hover,
    .mmedu-footer-social-icon:focus-visible { transform: none; }
}
.mmedu-footer-social-link:hover,
.mmedu-footer-social-link:focus-visible {
    color: var(--accent, #00f0ff);
    border-color: currentColor;
}
.mmedu-footer-meta {
    color: var(--text-dim, #8892b8);
    font-size: 12px;
}
@media (min-width: 720px) {
    .mmedu-footer-inner {
        flex-direction: row;
        flex-wrap: wrap;
        justify-content: space-between;
        align-items: center;
    }
    .mmedu-footer-links {
        order: 2;
    }
    .mmedu-footer-social {
        order: 3;
        flex-basis: 100%;
    }
    .mmedu-footer-meta {
        order: 3;
        margin-left: auto;
    }
}

/* ─── Knovli dashboard: Your Decks + Continue Studying ───────────
   Section shell shared by both, then the deck tiles and the
   continue rows. Colours come from tokens so the light theme is
   still correct; --deck-accent is set per tile inline (the palette
   is cycled by position in app.js). */
.cc-section-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    margin: 26px 0 12px;
}
.cc-section-title {
    font-family: var(--font-display);
    font-size: 1.15rem;
    font-weight: 700;
    color: var(--text);
    margin: 0;
}
.cc-section-link {
    background: none;
    border: 0;
    padding: 0;
    font-family: inherit;
    font-size: 0.85rem;
    font-weight: 700;
    color: var(--brand-blue-deep);
    cursor: pointer;
}
.cc-section-link:hover { text-decoration: underline; }

/* ── Create-deck card (home) ───────────────────────────────────────────
   Sits as the FIRST tile in .cc-decks-grid. When the student has no decks it
   gets --wide and spans every column, so an empty library reads as an
   invitation instead of a narrow box with a sentence in it. */
.cc-create-deck {
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    gap: 12px;
    padding: 14px;
    border-radius: 14px;
    border: 1.5px dashed var(--border-strong);
    background: var(--surface-2);
    min-height: 132px;
}
/* 🔑 `1 / -1` spans whatever the auto-fill grid resolved to, at any width —
   a fixed span would leave a hole on a 2-column phone. */
.cc-create-deck--wide { grid-column: 1 / -1; }
.cc-create-deck__head { display: flex; align-items: flex-start; gap: 10px; }
.cc-create-deck__plus {
    flex: 0 0 auto;
    width: 30px; height: 30px; border-radius: 9px;
    display: grid; place-items: center;
    font-size: 19px; font-weight: 700; line-height: 1;
    color: #fff; background: linear-gradient(135deg, #2563EB, #8B5CF6);
}
.cc-create-deck__title { color: var(--text); font-weight: 700; font-size: 14.5px; }
.cc-create-deck__sub {
    color: var(--text-muted); font-size: 12.5px; line-height: 1.45; margin-top: 2px;
}
.cc-create-deck__acts { display: flex; flex-wrap: wrap; gap: 8px; }
.cc-create-deck__btn {
    flex: 1 1 auto;
    padding: 9px 12px;
    border-radius: 9px;
    border: 1px solid var(--border-strong);
    background: var(--bg-card);
    color: var(--text);
    font-family: inherit; font-weight: 600; font-size: 12.5px;
    cursor: pointer; white-space: nowrap;
}
.cc-create-deck__btn:hover { border-color: var(--accent-blue, #2563EB); }
.cc-create-deck__btn--ai {
    border-color: transparent;
    background: linear-gradient(135deg, #2563EB, #8B5CF6);
    color: #fff;
}
.cc-create-deck__btn--ai:hover { filter: brightness(1.07); }

.cc-decks-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
    gap: 14px;
}
/* Two across on a phone. auto-fill with a 190px floor gives ONE column under
   ~404px, so a phone showed a single tall stack of decks; an explicit 2-col
   track overrides the floor and fits two ~160px cards on a 375px screen. */
@media (max-width: 640px) {
    .cc-decks-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
}

/* Set-card grids — Decks, Explore, and Explore's Trending / Most Popular rows.
   These carried their columns as INLINE styles, which a media query cannot
   override, so the mobile layout was unreachable from CSS. Moved here; the
   markup now just names the class. */
.kvSetGrid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
    gap: 14px;
}
.kvSetGrid--tight {
    grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
    gap: 10px;
}
@media (max-width: 640px) {
    .kvSetGrid,
    .kvSetGrid--tight { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
}
.cc-deck-card {
    position: relative;
    display: flex;
    flex-direction: column;
    gap: 5px;
    text-align: left;
    padding: 16px 16px 18px;
    border-radius: 14px;
    border: 1px solid var(--border);
    /* The accent tints the tile rather than filling it, so four tiles in a
       row read as one family instead of four unrelated blocks. The plain
       declaration first is the fallback: an engine without color-mix() drops
       the second line and keeps a correct (just untinted) card, instead of
       losing the background entirely. */
    background: var(--bg-card);
    background:
        linear-gradient(160deg, color-mix(in srgb, var(--deck-accent) 32%, transparent), transparent 62%),
        var(--bg-card);
    color: var(--text);
    font-family: inherit;
    cursor: pointer;
    transition: transform 0.12s ease, box-shadow 0.2s ease, border-color 0.2s ease;
}
.cc-deck-card:hover {
    transform: translateY(-2px);
    border-color: var(--border-strong);
    border-color: color-mix(in srgb, var(--deck-accent) 55%, transparent);
    box-shadow: var(--shadow-md);
}
.cc-deck-name {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 1rem;
    line-height: 1.25;
    /* Long deck names must not blow out the grid column. */
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}
.cc-deck-count { font-size: 0.78rem; color: var(--text-muted); }
.cc-deck-mastery { font-size: 0.78rem; color: var(--text-muted); margin-top: 6px; }
.cc-deck-mastery b { color: var(--text); font-size: 0.86rem; }

/* Shared progress bar. The fill uses the tile's accent where one is set and
   the brand gradient everywhere else (the continue rows). */
.cc-bar {
    height: 5px;
    border-radius: 999px;
    background: var(--bg-secondary);
    overflow: hidden;
}
.cc-bar i {
    display: block;
    height: 100%;
    border-radius: 999px;
    background: var(--deck-accent, var(--brand-gradient));
    transition: width 0.4s ease;
}

/* .cc-deck-new (the "+ New Deck" tile) removed 2026-09-18 — the section's
   own "View all" already goes to /decks. */

.cc-deck-skeleton {
    min-height: 118px;
    border-radius: 14px;
    border: 1px solid var(--border);
    background: var(--bg-card);
    opacity: 0.55;
}

/* Continue Studying items read as CARDS, not full-width bars. The list is a
   grid of card-width tracks (matching the .cc-decks-grid feel) instead of
   stacked 100% rows, and each row is capped so a single item doesn't stretch
   across the whole page. justify-content:start keeps one item card-sized
   rather than letting it fill the row. */
#ccHomeContinueList {
    /* One row per line, each filling its container. Was an auto-fill grid of
       280-360px tracks, which packed two or three rows across and left them
       card-sized — the rows carry a name, a fraction, a percent and a bar, and
       none of that had room to breathe at 360px. */
    display: flex;
    flex-direction: column;
    gap: 12px;
}
.cc-continue-row {
    display: flex;
    align-items: center;
    gap: 14px;
    padding: 13px 16px;
    border-radius: 12px;
    border: 1px solid var(--border);
    background: var(--bg-card);
    width: 100%;
    margin-bottom: 0;
}
/* No phone override needed any more: the list is a single column at every
   width, so what used to be the small-screen special case is now the rule. */
.cc-continue-main { flex: 1; min-width: 0; }
.cc-continue-name {
    font-weight: 700;
    font-size: 0.92rem;
    color: var(--text);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.cc-continue-meta {
    font-size: 0.78rem;
    color: var(--text-muted);
    margin: 2px 0 7px;
    /* Fraction left, percent right — the percent sits directly above the bar
       it describes, so the two read as one statement rather than two numbers. */
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 10px;
}
.cc-continue-pct { font-weight: 800; font-variant-numeric: tabular-nums; }
.cc-continue-btn {
    flex-shrink: 0;
    padding: 9px 18px;
    border-radius: 9px;
    border: 1px solid var(--border-strong);
    background: transparent;
    color: var(--text);
    font-family: inherit;
    font-size: 0.83rem;
    font-weight: 700;
    cursor: pointer;
    transition: background 0.2s ease, border-color 0.2s ease;
}
.cc-continue-btn:hover {
    background: var(--brand-gradient);
    border-color: transparent;
    color: #fff;
}

@media (max-width: 560px) {
    /* .cc-decks-grid's auto-fill override removed: it sat LATER in the file
       than the 640px 2-column rule, so it won below 560px and fell back to
       auto-fill — which still gives 2 columns at 375px but drops to ONE at
       320px, the exact case the 2-up rule exists for. */
    .cc-continue-btn { padding: 9px 13px; }
}

/* ─── Knovli dashboard: weekly XP chart ──────────────────────────
   Seven day-columns; the bar fills from the bottom of a fixed-height
   track so days sit on a common baseline and are comparable. */
.cc-xpchart-total {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 0.95rem;
    color: var(--text);
}
/* The heading and total sit OUTSIDE the card, above it — matching how every
   other section in this column is titled (.cc-section-head over its content),
   which is the flow they belong to. Briefly moved inside (#3102) and taken
   back out: enclosing it made the XP block the odd one out beside the
   leaderboard and the decks. So the CARD is .cc-xpchart-bars, not the section. */
.cc-xpchart > .cc-section-head { margin-bottom: 8px; }
.cc-xpchart-bars {
    display: grid;
    grid-template-columns: repeat(7, 1fr);
    gap: 10px;
    align-items: end;
    padding: 16px 16px 12px;
    border-radius: 14px;
    border: 1px solid var(--border);
    background: var(--bg-card);
}
/* ── Wins/losses line chart (/progress) ──────────────────────────────
   Same card language as .cc-xpchart-bars: the heading sits OUTSIDE, above,
   and the plot is the card. Plain inline SVG — the XP chart next to it draws
   with elements too, and a charting CDN here would be the app's only one. */
.cc-linechart > .cc-section-head { margin-bottom: 8px; }
.cc-linechart-plot {
    padding: 14px 14px 8px;
    border-radius: 14px;
    border: 1px solid var(--border);
    background: var(--bg-card);
}
/* Fixed height + a viewBox that does not preserve its ratio: the chart should
   fill the column at any width without the strokes scaling with it. */
.cc-lc-svg { display: block; width: 100%; height: 170px; overflow: visible; }
/* vector-effect keeps the stroke 2px after the viewBox is stretched — without
   it a wide column draws hairlines and a narrow one draws slabs. */
.cc-lc-line {
    fill: none;
    stroke-width: 2;
    stroke-linejoin: round;
    stroke-linecap: round;
    vector-effect: non-scaling-stroke;
}
.cc-lc-line--w { stroke: #16A34A; }
.cc-lc-line--l { stroke: #F97316; }
.cc-lc-dot--w { fill: #16A34A; }
.cc-lc-dot--l { fill: #F97316; }
.cc-lc-axis { stroke: var(--border); stroke-width: 1; vector-effect: non-scaling-stroke; }
/* The text sits inside a stretched viewBox, so it would stretch too. Both
   labels are anchored and small enough that the distortion reads as a font
   weight change rather than as broken type; keeping them in the SVG avoids a
   second absolutely-positioned layer that would have to track the points. */
.cc-lc-xlabel {
    fill: var(--text-dim);
    font-size: 11px;
    font-family: inherit;
    text-anchor: middle;
}
.cc-lc-ylabel {
    fill: var(--text-dim);
    font-size: 11px;
    font-family: inherit;
    text-anchor: end;
}
.cc-linechart-key {
    display: flex;
    gap: 16px;
    justify-content: center;
    margin-top: 8px;
}
.cc-lc-key {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    font-size: 0.78rem;
    font-weight: 700;
    color: var(--text-muted);
}
.cc-lc-key::before {
    content: '';
    width: 14px;
    height: 3px;
    border-radius: 2px;
}
.cc-lc-key--w::before { background: #16A34A; }
.cc-lc-key--l::before { background: #F97316; }

.cc-xpbar {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 7px;
    min-width: 0;
}
.cc-xpbar-track {
    width: 100%;
    max-width: 34px;
    height: 84px;
    display: flex;
    align-items: flex-end;
    border-radius: 7px;
    background: var(--bg-secondary);
    overflow: hidden;
}
.cc-xpbar-track i {
    display: block;
    width: 100%;
    border-radius: 7px;
    background: linear-gradient(180deg, var(--brand-purple), var(--brand-blue-deep));
    transition: height 0.45s ease;
}
.cc-xpbar-label {
    font-size: 0.72rem;
    font-weight: 700;
    letter-spacing: 0.04em;
    color: var(--text-dim);
}

/* ─── Knovli dashboard: class leaderboard ────────────────────────
   Rank · avatar · name · XP. The caller's own row is tinted so it
   is findable without scanning, matching the mockup. */
.cc-lb-scope { font-size: 0.8rem; color: var(--text-muted); }
.cc-lb-list {
    border-radius: 14px;
    border: 1px solid var(--border);
    background: var(--bg-card);
    padding: 6px;
}
.cc-lb-row {
    display: flex;
    align-items: center;
    gap: 11px;
    padding: 9px 11px;
    border-radius: 10px;
}
.cc-lb-row.is-me {
    background: linear-gradient(135deg, rgba(59, 130, 246, 0.20), rgba(139, 92, 246, 0.16));
}
.cc-lb-rank {
    width: 22px;
    flex-shrink: 0;
    text-align: center;
    font-weight: 700;
    font-size: 0.85rem;
    color: var(--text-muted);
}
.cc-lb-row.is-me .cc-lb-rank { color: var(--text); }
.cc-lb-avatar {
    width: 26px;
    height: 26px;
    flex-shrink: 0;
    border-radius: 50%;
    object-fit: cover;
    background: var(--bg-secondary);
}
.cc-lb-avatar[hidden] { display: none; }
/* Country flag after a leaderboard name. */
.kv-lb-flag {
    display: inline-block;
    width: 18px;
    height: 13px;
    margin-left: 6px;
    vertical-align: -1px;
    border-radius: 2px;
    object-fit: cover;
    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.12);
}
.cc-lb-avatar--blank {
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 0.72rem;
    font-weight: 700;
    color: var(--text-muted);
}
.cc-lb-name {
    flex: 1;
    min-width: 0;
    font-size: 0.88rem;
    font-weight: 600;
    color: var(--text);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.cc-lb-row.is-me .cc-lb-name { font-weight: 800; }
.cc-lb-xp {
    flex-shrink: 0;
    font-size: 0.83rem;
    font-weight: 700;
    color: var(--text-muted);
}
.cc-lb-row.is-me .cc-lb-xp { color: var(--brand-blue-deep); }
.cc-lb-gap {
    text-align: center;
    color: var(--text-dim);
    font-size: 0.8rem;
    line-height: 1;
    padding: 2px 0 4px;
}

/* ─── Knovli dashboard: Today's Plan ─────────────────────────────
   Three big-number tiles. Same accent-tint treatment as the deck
   cards so the dashboard reads as one system. */
.cc-plan-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
    gap: 14px;
}
.cc-plan-tile {
    display: flex;
    flex-direction: column;
    gap: 3px;
    text-align: left;
    padding: 16px 18px 18px;
    border-radius: 14px;
    border: 1px solid var(--border);
    background: var(--bg-card);
    background:
        linear-gradient(160deg, color-mix(in srgb, var(--deck-accent) 30%, transparent), transparent 62%),
        var(--bg-card);
    color: var(--text);
    font-family: inherit;
    cursor: pointer;
    transition: transform 0.12s ease, box-shadow 0.2s ease;
}
.cc-plan-tile.is-static { cursor: default; }
.cc-plan-tile:not(.is-static):hover { transform: translateY(-2px); box-shadow: var(--shadow-md); }
.cc-plan-value {
    font-family: var(--font-display);
    font-size: 1.6rem;
    font-weight: 800;
    line-height: 1.1;
}
.cc-plan-verb {
    font-size: 0.82rem;
    font-weight: 700;
    color: var(--text);
}
.cc-plan-sub {
    font-size: 0.78rem;
    color: var(--text-muted);
    margin-bottom: 8px;
    /* Deck and assignment titles are user/teacher input — clamp rather
       than let a long one stretch the tile. */
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}
.cc-plan-tile .cc-bar { margin-top: auto; }

/* ─── Knovli loader ──────────────────────────────────────────────
   The brand mark inside a sweeping ring, used everywhere the app
   makes someone wait: boot, the access gate, plugin launches and
   list loads. One component so every wait looks like the same
   product rather than four different spinners. */
.kv-loader {
    display: inline-flex;
    flex-direction: column;
    align-items: center;
    gap: 14px;
}
.kv-loader-ring {
    position: relative;
    width: 74px;
    height: 74px;
    display: grid;
    place-items: center;
}
/* The sweep. A conic gradient masked to a ring reads as a smooth
   orbit rather than a chasing dot. */
.kv-loader-ring::before {
    content: '';
    position: absolute;
    inset: 0;
    border-radius: 50%;
    background: conic-gradient(from 0deg, transparent 0deg, var(--brand-blue-deep) 210deg, var(--brand-purple) 320deg, transparent 360deg);
    -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 4px), #000 calc(100% - 3px));
            mask: radial-gradient(farthest-side, transparent calc(100% - 4px), #000 calc(100% - 3px));
    animation: kv-spin 1.05s linear infinite;
}
.kv-loader-mark {
    width: 40px;
    height: 40px;
    object-fit: contain;
    animation: kv-breathe 1.9s ease-in-out infinite;
}

/* ── The BOOT loader uses Play's game-loading animation ───────────────
   Owner, 2026-10-02: "can we use the animation that play uses when
   loading games".

   🔑 PORTED, NOT IMPORTED. The original lives in
   packages/quiz-formats/questionLoaderUI.js (`.mm-brandloader`), which
   is fetched from mind-match-plugins.web.app only when a FORMAT loads.
   Pulling it in at boot would put a network round trip in front of the
   first meaningful paint — the same mistake the splash logo had, where
   a cold or offline launch showed nothing until the request resolved.
   So the look is mirrored here in ~40 lines of CSS and the module stays
   where it is. Keep the two in step: same stops, same durations.

   The mark it spins is i/icon-512.png, the same K as the splash, so the
   launch image, the boot animation and the in-game loader are one
   sequence rather than three different logos. */
.kv-loader--brand .kv-loader-ring {
    --bl-cyan:    #22e0e0;
    --bl-blue:    #2f6bff;
    --bl-violet:  #7a3ef0;
    --bl-magenta: #c92fe0;
    width: clamp(132px, 30vw, 190px);
    height: clamp(132px, 30vw, 190px);
    perspective: 900px;
}
/* A faint static track, so it still reads as a ring at the moment the
   sweep's transparent segment passes the top. */
.kv-loader--brand .kv-loader-ring::after {
    content: '';
    position: absolute; inset: 0;
    border-radius: 50%;
    border: 2px solid rgba(140, 130, 255, 0.12);
}
.kv-loader--brand .kv-loader-ring::before {
    background: conic-gradient(from 0deg, transparent 0deg,
        var(--bl-cyan) 70deg, var(--bl-blue) 170deg,
        var(--bl-violet) 260deg, var(--bl-magenta) 320deg, transparent 360deg);
    -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 7px), #000 calc(100% - 7px));
            mask: radial-gradient(farthest-side, transparent calc(100% - 7px), #000 calc(100% - 7px));
    filter: drop-shadow(0 0 6px rgba(120, 100, 255, 0.35));
    animation: kv-spin 2.4s linear infinite;
}
.kv-loader--brand .kv-loader-mark {
    /* 🔁 Scaled with the hi-res K (2026-10-02), which sits in a larger
       transparent margin — same on-screen size, more real pixels behind it. */
    width: clamp(74px, 16vw, 108px);
    height: clamp(74px, 16vw, 108px);
    transform-style: preserve-3d;
    /* The angle CLIMBS to 1080deg rather than resetting each loop, so the
       spin never visibly jumps at the boundary; every third beat throbs
       harder. Both faces show — seeing the back is what makes the flip
       legible as a flip. */
    -webkit-backface-visibility: visible;
            backface-visibility: visible;
    animation: kv-bl-spinthrob 4.5s ease-in-out infinite;
    filter: drop-shadow(0 0 14px rgba(70, 130, 255, 0.45));
}
@keyframes kv-bl-spinthrob {
    0%      { transform: rotateY(0deg)    scale(1);    }
    16.667% { transform: rotateY(180deg)  scale(1.14); }
    33.333% { transform: rotateY(360deg)  scale(1);    }
    50%     { transform: rotateY(540deg)  scale(1.14); }
    66.667% { transform: rotateY(720deg)  scale(1);    }
    83.333% { transform: rotateY(900deg)  scale(1.42); }
    100%    { transform: rotateY(1080deg) scale(1);    }
}
.kv-loader-msg {
    font-family: var(--font-display);
    font-size: 0.92rem;
    font-weight: 600;
    color: var(--text-muted);
    letter-spacing: 0.01em;
}
.kv-loader--sm .kv-loader-ring { width: 50px; height: 50px; }
.kv-loader--sm .kv-loader-mark { width: 27px; height: 27px; }
.kv-loader--sm .kv-loader-msg  { font-size: 0.84rem; }

@keyframes kv-spin { to { transform: rotate(360deg); } }
@keyframes kv-breathe {
    0%, 100% { transform: scale(1);    opacity: 1; }
    50%      { transform: scale(0.90); opacity: 0.78; }
}
/* Respect a reduced-motion preference: keep the mark, drop the
   spin and the pulse. */
@media (prefers-reduced-motion: reduce) {
    .kv-loader-ring::before { animation-duration: 3.2s; }
    .kv-loader-mark { animation: none; }
    /* 🔑 Must repeat the selector: `.kv-loader--brand .kv-loader-mark`
       above is MORE specific than the bare `.kv-loader-mark` here, so the
       3D flip would otherwise keep running for someone who asked for less
       motion. (Same specificity trap as the mobile study rail.) */
    .kv-loader--brand .kv-loader-ring::before { animation-duration: 5s; }
    .kv-loader--brand .kv-loader-mark { animation: none; }
}

/* ─── Knovli store badges ────────────────────────────────────────
   Centred row at the foot of the dashboard, above the footer.
   Hidden unless a store link is configured. */
.cc-stores {
    margin: 38px 0 6px;
    text-align: center;
}
.cc-stores-title {
    font-family: var(--font-display);
    font-size: 0.92rem;
    font-weight: 700;
    color: var(--text-muted);
    margin-bottom: 14px;
}
.cc-stores-row {
    display: flex;
    justify-content: center;
    flex-wrap: wrap;
    gap: 12px;
}
/* The badge IS the artwork now — no card, border or background of our own.
   Apple's and Google's badge guidelines both expect their asset to sit on the
   page unaltered, and a bordered box around it reads as a mistake. */
.cc-store-badge {
    display: inline-flex;
    align-items: center;
    text-decoration: none;
    transition: transform 0.12s ease, opacity 0.2s ease;
}
/* Sized by HEIGHT, not width: the two assets are 2.94:1 and 3.05:1, so a
   shared width would leave them visibly different heights sitting side by
   side. A shared height makes them read as a pair. */
.cc-store-badge img {
    height: 48px;
    width: auto;
    display: block;
}
.cc-store-badge:hover {
    transform: translateY(-2px);
    opacity: 0.9;
    text-decoration: none;
}
.cc-store-badge:focus-visible {
    outline: 3px solid var(--accent-blue);
    outline-offset: 3px;
    border-radius: 8px;
}

/* ============ Signed-out Community hero ============
   Three panels: the brand pitch, a card that LINKS to the real sign-in form
   (deliberately not an inline auth form — one auth surface, not two), and the
   feature list. Uses site tokens throughout so it works in light and dark;
   the design mock is dark, but this app themes both ways. */
.kv-hero {
    /* COMPACT (owner, 2026-09-17: "make the cards above the hero more compact
       so they are not as tall"). Every spacing and type size below was
       tightened together; the content is unchanged. */
    display: grid;
    /* Four EQUAL cards: pitch | sign-in | mark | features.
       minmax(0, 1fr) rather than plain 1fr — a bare 1fr floors at each track's
       min-content, so the wordiest card (the feature list) would quietly grow
       its column and the four would stop being equal. */
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: 12px;
    align-items: stretch;
    margin-bottom: 16px;
}
/* Shared card shell — every panel in the hero now reads as its own card. */
.kv-hero > * {
    background: var(--bg-card);
    border: 1px solid rgba(37,99,235,0.20);
    border-radius: 14px;
    padding: 12px 16px;
}
.kv-hero-pitch { display: flex; flex-direction: column; justify-content: center; }
.kv-hero-title {
    margin: 0 0 6px;
    font-family: var(--font-display);
    /* Retuned for EQUAL columns: at ~1200px the four cards are ~220px wide and
       the old clamp (up to 2.9rem / 3.6vw) put "Learn it." right at the edge of
       overflowing its card. */
    font-size: clamp(1.35rem, 2vw, 1.85rem);
    font-weight: 800;
    line-height: 1.08;
    letter-spacing: -0.02em;
    color: var(--text);
}
.kv-hero-title-accent {
    background: var(--brand-gradient);
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
}
.kv-hero-sub {
    margin: 0;
    font-family: var(--font-body);
    font-size: 0.9rem;
    line-height: 1.4;
    color: var(--text-muted);
}

/* Sign-in card */
.kv-hero-card { display: flex; flex-direction: column; justify-content: center; gap: 8px; }
.kv-hero-card-title {
    font-family: var(--font-display);
    font-size: 1.05rem;
    font-weight: 700;
    color: var(--text);
    text-align: center;
}
.kv-hero-cta {
    display: block;
    background: var(--brand-gradient);
    color: #fff;
    text-align: center;
    text-decoration: none;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 0.95rem;
    padding: 9px 16px;
    border-radius: 10px;
}
.kv-hero-alt {
    text-align: center;
    font-family: var(--font-body);
    font-size: 0.85rem;
    color: var(--text-muted);
}
.kv-hero-alt a { color: var(--brand-purple); font-weight: 700; text-decoration: none; }
.kv-hero-alt a:hover { text-decoration: underline; }
/* Store badges — rendered only when app_configs has the link. */
.kv-hero-stores { display: flex; flex-direction: column; gap: 6px; margin-top: 0; }
.kv-hero-store {
    display: flex; align-items: center; justify-content: center; gap: 8px;
    border: 1px solid var(--border-strong); border-radius: 10px;
    padding: 7px 12px; text-decoration: none;
    font-family: var(--font-display); font-weight: 600; font-size: 0.85rem;
    color: var(--text); background: transparent;
}
.kv-hero-store:hover { border-color: rgba(37,99,235,0.45); }

/* Feature panel */
/* Mark now stands alone in its own card, tinted so the row has a focal point. */
/* Built for one <img>, so it was a ROW flex. It now holds a title AND a stack
   of badges, which a row laid out side by side — hence column. */
.kv-hero-markcard {
    display: flex; flex-direction: column;
    align-items: center; justify-content: center;
    background: linear-gradient(135deg, rgba(37,99,235,0.09), rgba(139,92,246,0.09));
}
/* The hero art is the BANNER (1672x589), not the old square mark. The 132px
   cap was sized for a square icon and would render the banner ~46px tall —
   a stamp floating in its card. It now fills the column and takes its height
   from its own aspect ratio. Single-use class, so retuning it is safe. */
.kv-hero-mark { width: 100%; max-width: 100%; height: auto; object-fit: contain; }
.kv-hero-feats { display: flex; align-items: center; }
.kv-hero-featlist { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; flex: 1; min-width: 0; }
.kv-hero-featlist li { display: flex; align-items: flex-start; gap: 10px; }
.kv-hf-ico { font-size: 20px; line-height: 1.1; flex-shrink: 0; }
.kv-hf-txt { display: flex; flex-direction: column; min-width: 0; }
.kv-hf-txt b { font-family: var(--font-display); font-size: 0.95rem; font-weight: 700; color: var(--text); }
.kv-hf-txt span { font-family: var(--font-body); font-size: 0.8rem; line-height: 1.3; color: var(--text-muted); }

/* Tablet: pitch spans the top, card + features share the row below. */
/* Four across gets tight well before tablet — go 2x2, mark beside the pitch. */
/* Tablet — a true 2x2. The spans that used to stretch the sign-in and feature
   cards full width are REMOVED on purpose: with them the layout was 1+1 over
   two full-width rows, not a 2x2. */
@media (max-width: 1180px) {
    .kv-hero { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
/* Mobile: one column, and the mark CARD drops entirely — a card holding only
   a decorative image isn't worth a full screen-width row on a phone. */
/* Phone — single column. The mark card used to DROP here, on the reasoning
   that a card holding only a decorative image was not worth a full-width row.
   It now holds the App Store / Play badges, and a phone is precisely where a
   "get the app" link earns its space — so it stays. */
@media (max-width: 700px) {
    /* Compact on a phone too: the pitch and the sign-in card share the first
       row, the app badges and the feature list each take a full row below. */
    .kv-hero { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
    .kv-hero-markcard, .kv-hero-feats { grid-column: 1 / -1; }
    .kv-hero > * { padding: 10px 12px; }
    .kv-hero-pitch { text-align: left; }
    .kv-hero-title { font-size: 1.3rem; }
    .kv-hero-sub { font-size: 0.82rem; }
    .kv-hero-feats { align-items: stretch; }
    /* The sign-in card's store BUTTON repeats the badges in the "Get the app"
       card right below it — both are painted from the same links by
       _kvPaintHeroStores, so the badges are always there when it is. At half
       a phone's width the button wrapped to three lines. */
    .kv-hero-card .kv-hero-stores { display: none; }
}

/* ============ Clickable set cards — hover / press / focus ============
   These cards carry their layout as INLINE styles (they're built in app.js
   template strings), and an inline style can't express :hover — so the
   interaction states have to live here.

   Deliberately never touches `background`: the subject wash is applied inline
   as a background-image, and overriding background here would wipe it on hover.
   Only transform, shadow and border move.

   Covers Explore cards (.kvSetOpen) and both clickable Decks cards. The Home
   deck tiles already had :hover — they only gain the pressed + focus states. */
.kvSetOpen,
.mmEduSetOpenCard,
.mmEduBulkSelectCard {
    transition: transform 0.12s ease, box-shadow 0.2s ease, border-color 0.2s ease;
}
.kvSetOpen:hover,
.mmEduSetOpenCard:hover,
.mmEduBulkSelectCard:hover {
    transform: translateY(-2px);
    box-shadow: var(--shadow-md);
    border-color: var(--border-strong);
}
/* Pressed — settles BELOW the resting position so the click reads as a press
   rather than a second lift. Matches .btn-primary:active. */
.kvSetOpen:active,
.mmEduSetOpenCard:active,
.mmEduBulkSelectCard:active,
.cc-deck-card:active {
    transform: translateY(1px);
    box-shadow: var(--shadow-sm);
}
/* Keyboard users need a visible focus state — without one the card is
   reachable but invisible to Tab. The set cards are no longer role="button":
   they carry a stretched <a> (.kvSetLink) which takes the focus and draws this
   same ring. .mmEduBulkSelectCard IS still a role="button" div — selection is
   not navigation — so it keeps its own entry here. */
.kvSetOpen:focus-visible,
.mmEduSetOpenCard:focus-visible,
.mmEduBulkSelectCard:focus-visible,
.cc-deck-card:focus-visible {
    outline: 3px solid var(--accent-blue);
    outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
    .kvSetOpen,
    .mmEduSetOpenCard,
    .mmEduBulkSelectCard { transition: box-shadow 0.2s ease, border-color 0.2s ease; }
    .kvSetOpen:hover,
    .mmEduSetOpenCard:hover,
    .mmEduBulkSelectCard:hover,
    .kvSetOpen:active,
    .mmEduSetOpenCard:active,
    .mmEduBulkSelectCard:active,
    .cc-deck-card:active { transform: none; }
}

/* ============ Dashboard row: leaderboard | streak + XP ============
   Leaderboard on the left, streak and XP stacked on the right, per the mock.
   align-items:start so a short right column doesn't stretch to match a long
   leaderboard. Both children self-hide when they have no data, and the grid
   copes with either being absent. */
.cc-dash-row {
    display: grid;
    /* Left column carries the student's own work (plan, decks, continue) and
       needs the room; the right is stat cards + leaderboard at a fixed-ish
       width. */
    grid-template-columns: minmax(0, 1.45fr) minmax(0, 0.9fr);
    gap: 16px;
    align-items: start;
    margin-bottom: 26px;
}
.cc-dash-col { display: flex; flex-direction: column; gap: 16px; min-width: 0; }
/* The stacked sections carry their own margins elsewhere; the grid gap owns
   spacing here, so zero them out to avoid doubling. The leaderboard moved
   INSIDE .cc-dash-col, so the old `.cc-dash-row > .cc-leaderboard` selector
   stopped matching — hence the child selector below rather than a descendant
   one, which would also catch nothing after a future move. */
.cc-dash-col > .cc-leaderboard,
.cc-dash-col > .cc-plan,
.cc-dash-col > .cc-decks,
.cc-dash-col > .cc-continue { margin-bottom: 0; }
/* .cc-xpchart and .cc-streakcard are NOT in that list, and do not need to be:
   neither carries a bottom margin of its own, so the grid gap already owns the
   spacing. (The streak card is back in this column, above the XP chart; the
   comment that said both had left for .cc-standing was stale for the chart,
   which never moved.) */

/* Level / rank card — same shell as the streak card so the dashboard column
   reads as one set. */
.cc-levelcard {
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: 14px;
    padding: 16px 18px;
}
/* .cc-level-value removed: the rank card now uses .cc-rank-name, and no
   markup carried the old class any more. */

/* Streak card */
.cc-streakcard {
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: 14px;
    padding: 16px 18px;
}
/* Flame + count, with the label beneath — the mock's treatment. Tokens, not a
   literal white: --text is near-white in dark and near-black in light, so the
   caption stays legible in both. A hardcoded #fff would vanish on the light
   card. */
.cc-streak-hero {
    display: flex;
    /* flex-start, NOT center — the flame aligns itself to the count's line box
       (see .cc-streak-flame below) rather than to the whole stack. */
    align-items: flex-start;
    gap: 14px;
    margin-bottom: 16px;
    --cc-streak-count-size: 2rem;
    --cc-streak-flame-size: 26px;
}
/* The flame sits level with the day COUNT, not with the middle of the
   count+caption stack. `align-items:center` on the hero centred a 26px icon
   against the ~52px two-line block, so it landed ~9px low — optically level
   with the gap UNDER the number. Giving the flame the count's own line box as
   its height and centring inside that fixes it, and keeps holding if either
   font-size changes. */
.cc-streak-flame {
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    height: calc(var(--cc-streak-count-size) * 1.05);  /* the count's line-height */
    font-size: var(--cc-streak-flame-size);            /* emoji fallback only */
    line-height: 1;
}
.cc-streak-headline { display: flex; flex-direction: column; min-width: 0; }
.cc-streak-count {
    font-family: var(--font-display);
    font-size: var(--cc-streak-count-size);
    font-weight: 800;
    line-height: 1.05;
    color: var(--text);
    font-variant-numeric: tabular-nums;
}
.cc-streak-caption {
    font-family: var(--font-body);
    font-size: 0.8rem;
    font-weight: 600;
    color: var(--text-muted);
    letter-spacing: 0.01em;
}
.cc-streak-dots { display: flex; gap: 8px; justify-content: space-between; }
.cc-streak-day { display: flex; flex-direction: column; align-items: center; gap: 6px; flex: 1; min-width: 0; }
.cc-streak-dot {
    width: 30px; height: 30px; border-radius: 50%;
    display: flex; align-items: center; justify-content: center;
    border: 2px solid var(--border-strong);
    background: transparent;
    color: #fff; font-size: 14px; font-weight: 800; line-height: 1;
}
.cc-streak-dot.is-on {
    background: var(--brand-gradient);
    border-color: transparent;
    box-shadow: var(--glow-blue);
}
.cc-streak-label {
    font-size: 10.5px; font-weight: 700; letter-spacing: 0.04em;
    color: var(--text-muted); text-transform: uppercase;
}

/* Mobile: each card full width, stacked. */
@media (max-width: 860px) {
    .cc-dash-row { grid-template-columns: 1fr; }
}
@media (max-width: 420px) {
    /* Seven 30px circles + gaps overflow a narrow phone. */
    .cc-streak-dot { width: 26px; height: 26px; font-size: 12px; }
    .cc-streak-dots { gap: 4px; }
}

/* Leaderboard scope picker — replaces the plain scope label once the user is
   in more than one class. Sized to sit inside .cc-section-head without
   changing the heading's rhythm. */
.cc-lb-scope-select {
    font-family: inherit;
    font-size: 12.5px;
    font-weight: 600;
    color: var(--text);
    background: var(--bg-card);
    border: 1px solid var(--border-strong);
    border-radius: 8px;
    padding: 5px 8px;
    max-width: 190px;
    cursor: pointer;
}
.cc-lb-scope-select:focus-visible { outline: 2px solid var(--accent-blue); outline-offset: 1px; }

/* Leaderboard loading + empty states. Skeleton rows pulse while a class switch
   round-trips to the callable, so the list reads as "loading" rather than
   "frozen". Height matches .cc-lb-row so the card doesn't jump when real rows
   land. */
@keyframes ccLbPulse { 0%, 100% { opacity: 0.45; } 50% { opacity: 0.8; } }
.cc-lb-skeleton {
    height: 44px;
    border-radius: 10px;
    background: var(--bg-input, var(--bg-card));
    border: 1px solid var(--border);
    margin-bottom: 8px;
    animation: ccLbPulse 1.1s ease-in-out infinite;
}
.cc-lb-skeleton:nth-child(2) { animation-delay: 0.12s; }
.cc-lb-skeleton:nth-child(3) { animation-delay: 0.24s; }
.cc-lb-skeleton:nth-child(4) { animation-delay: 0.36s; }
@media (prefers-reduced-motion: reduce) {
    .cc-lb-skeleton { animation: none; opacity: 0.6; }
}
.cc-lb-empty {
    padding: 18px 4px;
    color: var(--text-muted);
    font-size: 13px;
    text-align: center;
}

/* Mastery setup wizard — the typing indicator's three bouncing dots.
   Respects reduced-motion: the dots still show, they just hold still. */
@keyframes kvWizDot {
    0%, 60%, 100% { opacity: 0.25; transform: translateY(0); }
    30%           { opacity: 1;    transform: translateY(-3px); }
}
@media (prefers-reduced-motion: reduce) {
    .kvWizCard [style*="kvWizDot"] { animation: none !important; opacity: 0.5 !important; }
}

/* Lesson flashcards. The flip and slide are driven by inline transforms, so
   honouring reduced-motion means overriding them here — the card still flips
   and still changes, it just arrives instead of travelling. */
@media (prefers-reduced-motion: reduce) {
    #kvFcCard { transition: none !important; }
}

/* Quiz interstitial spinner. Reduced-motion stops the rotation — the step
   still shows and still reveals, it just doesn't spin while it waits. */
@keyframes kvQiSpin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
    #kvQiRing { animation: none !important; }
    #kvQiBody, #kvQiFoot { transition: none !important; }
}

/* ── Subject watermark ────────────────────────────────────────────────
   Custom subject artwork, bled off the right edge of a card and sitting
   BEHIND the text as part of the background.

   The `> *:not(.cc-subj-mark)` rule is what makes this non-invasive: it
   lifts whatever a card already renders above the mark, so adding the
   watermark to a card means adding one class and one <img>, never
   restyling that card's insides.

   Artwork lives in i/subjects/<slug>.svg (or .png). It is OPTIONAL — the
   <img> removes itself when the file is missing, so cards look exactly as
   they do today until the files land. */
.cc-subj-host { position: relative; overflow: hidden; }
/* Every direct child is lifted above the watermark. 🚨 .kvSetLink is excluded:
   this selector is (0,2,0) and .kvSetLink is (0,1,0), so without the exclusion
   it OVERRODE the stretched link's position:absolute with position:relative —
   collapsing it to a zero-size box with nothing to click. That took My Decks,
   recently-used and the Explore grid down together, because those are exactly
   the cards carrying .cc-subj-host. The two cards WITHOUT it
   (.mmEduPublicSetCard, .mmEduClassSharedCard) kept working, which is what
   made the breakage look selective rather than total. */
.cc-subj-host > *:not(.cc-subj-mark):not(.kvSetLink) { position: relative; z-index: 1; }
/* The link does NOT hit-test — see .kvSetLink below — so it needs no z-index
   fight with the card's content. That fight is what broke clicking twice. */
.cc-subj-mark {
    position: absolute;
    right: -4%;
    top: 50%;
    transform: translateY(-50%);
    /* Sized by WIDTH, because the artwork is 3:2 landscape (1200x800).
       Driving it from height with a max-width cap — the first cut, written
       before the art existed — clamped the box and let object-fit letterbox
       the drawing down into a small floating graphic instead of a watermark.
       Width-first lets the aspect ratio set the height, and overflow:hidden on
       the host crops any vertical spill evenly. */
    width: 58%;
    height: auto;
    max-width: 340px;
    object-fit: contain;
    object-position: right center;
    /* Low enough that body text keeps its contrast, high enough to read as
       deliberate rather than as a rendering artefact. Dark mode needs more:
       the same art over a near-black ground reads far fainter. */
    opacity: 0.09;
    pointer-events: none;
    user-select: none;
    z-index: 0;
}
:root:not([data-theme="light"]) .cc-subj-mark { opacity: 0.14; }
@media (prefers-color-scheme: light) {
    :root:not([data-theme="dark"]) .cc-subj-mark { opacity: 0.09; }
}
:root[data-theme="dark"] .cc-subj-mark { opacity: 0.14; }
/* Small cards can't carry a half-width mark without crowding the text. */
@media (max-width: 480px) {
    .cc-subj-mark { width: 52%; opacity: 0.07; }
    :root[data-theme="dark"] .cc-subj-mark { opacity: 0.12; }
}

/* Dashboard trending section — same spacing as the other full-width sections
   it sits between, so the page keeps one rhythm. The row inside is rendered by
   Explore's own renderer, which brings its own card styles. */
.cc-trending { margin-bottom: 26px; }
.cc-trending .cc-section-head { margin-bottom: 12px; }

/* ── Deck / set cards ─────────────────────────────────────────────────
   One shape everywhere: chips row, title, body, footer — in a box with
   3x5 index-card proportions so a grid of them lines up regardless of
   how much text each carries. */
.kvDeckCard {
    /* 5:3 — a 3x5 index card lying landscape, which is the proportion a
       deck reads as. aspect-ratio (not a fixed height) keeps the row
       uniform at every column width. */
    aspect-ratio: 5 / 3;
    display: flex;
    flex-direction: column;
    overflow: hidden;
}
/* Chips ABOVE the title, on their own full row. Shrunk HERE rather than in
   _visibilityBadgeHtml / _contentRatingPill, which are shared with surfaces
   that still want them full size. */
.kvChipRow {
    display: flex;
    align-items: center;
    gap: 6px;
    flex-wrap: wrap;
    margin-bottom: 6px;
    min-height: 17px;          /* reserves the row so titles align across cards */
}
.kvChipRow > span,
.kvChipRow > span > span {
    font-size: 9.5px !important;
    padding: 1px 6px !important;
    line-height: 1.5 !important;
}
/* Title gets the full width and is clamped to TWO lines — a third line would
   push the card past its aspect ratio, and every card in the row with it.

   🚨 flex-shrink: 0 is load-bearing, not tidiness. A flex item's automatic
   minimum size is its CONTENT height only while `overflow` is visible; set
   `overflow: hidden` (three lines down, for the line-clamp) and that minimum
   becomes ZERO. Every other child of .kvDeckCard resists being squeezed —
   .kvChipRow has min-height, .kvDeckFoot has flex-shrink:0, and the plain
   description/attribution divs keep overflow:visible so their content floor
   applies. That left the TITLE as the single item able to absorb overflow, so
   on a 2-up phone grid the whole card's excess landed on it and it collapsed
   to nothing: titles vanished on Explore rather than truncating. */
.kvDeckTitle {
    flex-shrink: 0;
    margin: 0 0 6px;
    font-size: 14.5px;
    font-weight: 700;
    color: var(--text);
    line-height: 1.25;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    word-break: break-word;
}
/* Everything between title and footer absorbs the slack; the footer pins to
   the bottom so status sits on the same line across a row of cards. */
.kvDeckBody { flex: 1 1 auto; min-height: 0; overflow: hidden; }
.kvDeckFoot { margin-top: auto; flex-shrink: 0; }
/* At the 2-up phone grid a 5:3 card is ~104px tall, and Explore's card carries
   chips + title + description + attribution + meta + footer — roughly twice
   that. Protecting the title alone would only move the clipping onto the
   footer, so below the breakpoint the card sizes to its content instead. Grid
   rows still stretch every card to the tallest, so a row stays level. */
@media (max-width: 640px) {
    .kvDeckCard { aspect-ratio: auto; min-height: 104px; }
}

/* ── Content Expert Approved pill ─────────────────────────────────────
   Blue, to match the badge artwork (sampled: #001848 navy + #0090F0),
   which the previous emerald clashed with.

   THEME-AWARE, and that is the point rather than a nicety: the pill tints
   its own background with its own colour, so a single hex cannot work on
   both grounds. The emerald it replaces scored 2.31:1 in LIGHT mode —
   well under the 4.5 AA floor, i.e. genuinely hard to read. This pair is
   5.57 light / 5.10 dark. */
.kvExpertPill {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    border-radius: 999px;
    padding: 5px 12px 5px 6px;   /* tighter on the left; the badge sits there */
    font-size: 0.82rem;
    font-weight: 700;
    font-family: inherit;
    line-height: 1.2;
    background: rgba(29, 78, 216, 0.12);
    border: 1px solid rgba(29, 78, 216, 0.42);
    color: #1D4ED8;
}
:root:not([data-theme="light"]) .kvExpertPill {
    background: rgba(96, 165, 250, 0.14);
    border-color: rgba(96, 165, 250, 0.45);
    color: #60A5FA;
}
@media (prefers-color-scheme: light) {
    :root:not([data-theme="dark"]) .kvExpertPill {
        background: rgba(29, 78, 216, 0.12);
        border-color: rgba(29, 78, 216, 0.42);
        color: #1D4ED8;
    }
}
:root[data-theme="dark"] .kvExpertPill {
    background: rgba(96, 165, 250, 0.14);
    border-color: rgba(96, 165, 250, 0.45);
    color: #60A5FA;
}
button.kvExpertPill { cursor: pointer; }
/* Sized to match the corner mark's presence rather than the old 15px chip. */
.kvExpertPill img { width: 26px; height: 26px; object-fit: contain; display: block; flex-shrink: 0; }

/* ── Quality Check: the public badge, and the creator's own panel ─────
   The badge is an image the owner supplied, so it carries its own colour —
   these rules only give it a hit area and stop it reflowing. The PANEL is
   owner-only feedback and themes from the app's tokens like everything else. */
.kvQualityBadge { display: inline-block; line-height: 0; border-radius: 10px; max-width: 100%; }
button.kvQualityBadge { cursor: pointer; }
.kvQualityBadge:focus-visible { outline: 3px solid #6366F1; outline-offset: 3px; }
/* The card-sized badge. Native size is 132x28 — never scaled UP, which is the
   one thing that would look worse than leaving it off the card. */
.kvQualityBadgeSm { display: block; width: 132px; height: 28px; max-width: 100%; }

.kvQualityPanel {
    /* 22px below, not 6px: the panel sits directly above the filter row and the
       two-column body, and at 6px a COLLAPSED panel read as part of the card
       under it rather than as its own thing. Owner asked for the gap. */
    margin: 14px 0 22px;
    padding: 16px 18px;
    border-radius: 14px;
    background: rgba(15, 23, 42, 0.04);
    border: 1px solid rgba(15, 23, 42, 0.10);
}
/* The panel is a <details>: the summary is the score row, the detail is the
   list. Collapsed by default, so the bar lives in the summary too. */
.kvQualityPanel > summary { cursor: pointer; list-style: none; }
.kvQualityPanel > summary::-webkit-details-marker { display: none; }
.kvQualityHead { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
/* A chevron that turns, so the row reads as openable without a marker. */
.kvQualityHead::after {
    content: ''; width: 9px; height: 9px; flex: 0 0 auto; margin-left: 2px;
    border-right: 2px solid currentColor; border-bottom: 2px solid currentColor;
    transform: rotate(45deg); opacity: 0.55; transition: transform 0.15s ease;
}
.kvQualityPanel[open] .kvQualityHead::after { transform: rotate(-135deg); }
.kvQualityPanel > summary:focus-visible { outline: 3px solid #6366F1; outline-offset: 2px; border-radius: 10px; }
.kvQualityHead h3 { margin: 0; font-size: 1.02rem; flex: 1 1 auto; }
.kvQualityScore { font-size: 1.5rem; font-weight: 800; line-height: 1; }
.kvQualityScore span { font-size: 0.85rem; font-weight: 600; opacity: 0.7; }
.kvQualityBand { border-radius: 999px; padding: 3px 10px; font-size: 0.78rem; font-weight: 700; }
/* The ? beside the score opens the explainer. A circle, because a bare "?" in
   running text does not read as a control. */
.kvQualityWhat {
    width: 22px; height: 22px; flex: 0 0 auto; border-radius: 50%; cursor: pointer;
    border: 1px solid currentColor; background: none; color: inherit;
    font: 700 12px/1 inherit; opacity: 0.6; padding: 0;
}
.kvQualityWhat:hover, .kvQualityWhat:focus-visible { opacity: 1; }
.kvQualityBar {
    display: block; flex: 1 1 120px; min-width: 90px;
    height: 7px; border-radius: 999px; overflow: hidden;
    background: rgba(15, 23, 42, 0.12);
}
.kvQualityPanel[open] .kvQualityLead { margin-top: 12px; }
.kvQualityBar span { display: block; height: 100%; border-radius: 999px; }
.kvQualityLead { margin: 0 0 8px; font-size: 0.9rem; font-weight: 600; }
.kvQualityList { list-style: none; margin: 0; padding: 0; display: grid; gap: 10px; }
.kvQualityRow {
    padding: 10px 12px; border-radius: 10px;
    background: var(--kv-surface, #fff);
    border: 1px solid rgba(15, 23, 42, 0.10);
}
.kvQualityRowTop { display: flex; align-items: baseline; gap: 8px; }
.kvQualityRowTop strong { flex: 1 1 auto; font-size: 0.92rem; }
.kvQualityPts { font-size: 0.82rem; font-weight: 700; opacity: 0.75; white-space: nowrap; }
.kvQualityDetail { font-size: 0.86rem; opacity: 0.85; margin-top: 2px; }
/* The specific cards to fix — the actionable half of the panel. */
.kvQualityCards { margin: 8px 0 0; padding-left: 18px; font-size: 0.84rem; opacity: 0.9; }
.kvQualityCards li { margin: 2px 0; }
.kvQualityCards .kvQualityMore { list-style: none; margin-left: -18px; opacity: 0.7; font-style: italic; }
.kvQualityNote { margin: 12px 0 0; font-size: 0.82rem; opacity: 0.8; line-height: 1.5; }

:root:not([data-theme="light"]) .kvQualityPanel {
    background: rgba(148, 163, 184, 0.10);
    border-color: rgba(148, 163, 184, 0.22);
}
:root:not([data-theme="light"]) .kvQualityRow {
    background: rgba(15, 23, 42, 0.35);
    border-color: rgba(148, 163, 184, 0.22);
}
:root:not([data-theme="light"]) .kvQualityBar { background: rgba(148, 163, 184, 0.25); }
@media (prefers-color-scheme: light) {
    :root:not([data-theme="dark"]) .kvQualityPanel {
        background: rgba(15, 23, 42, 0.04);
        border-color: rgba(15, 23, 42, 0.10);
    }
    :root:not([data-theme="dark"]) .kvQualityRow {
        background: #fff;
        border-color: rgba(15, 23, 42, 0.10);
    }
    :root:not([data-theme="dark"]) .kvQualityBar { background: rgba(15, 23, 42, 0.12); }
}
:root[data-theme="dark"] .kvQualityPanel {
    background: rgba(148, 163, 184, 0.10);
    border-color: rgba(148, 163, 184, 0.22);
}
:root[data-theme="dark"] .kvQualityRow {
    background: rgba(15, 23, 42, 0.35);
    border-color: rgba(148, 163, 184, 0.22);
}
:root[data-theme="dark"] .kvQualityBar { background: rgba(148, 163, 184, 0.25); }

/* ── "Get to know Knovli" tour ────────────────────────────────────────
   Marketing-shaped but built from the app's own tokens, so it themes with
   everything else rather than being a stranded landing page. */
.kvTourWrap { max-width: 980px; margin: 0 auto; }
.kvTourHero { text-align: center; margin-bottom: 28px; }
.kvTourH1 {
    font-family: var(--font-display);
    font-size: clamp(1.6rem, 4vw, 2.3rem);
    font-weight: 800;
    letter-spacing: var(--tracking-tight, -0.02em);
    color: var(--text);
    margin: 6px 0 10px;
    line-height: 1.15;
}
.kvTourLede {
    color: var(--text-muted);
    font-size: 1rem;
    line-height: 1.6;
    max-width: 620px;
    margin: 0 auto;
}
.kvTourGrid {
    display: grid;
    /* Two up where there is room, one on a phone. minmax(0,1fr) so a long
       word cannot push a column past the viewport. */
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 340px), 1fr));
    gap: 16px;
}
.kvTourCard {
    display: flex;
    gap: 14px;
    align-items: flex-start;
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: 14px;
    padding: 18px 20px;
}
.kvTourIcon {
    font-size: 1.7rem;
    line-height: 1;
    flex-shrink: 0;
    width: 44px; height: 44px;
    display: flex; align-items: center; justify-content: center;
    border-radius: 12px;
    background: linear-gradient(135deg, rgba(37,99,235,0.12), rgba(139,92,246,0.12));
}
.kvTourText { min-width: 0; }
.kvTourTitle {
    font-family: var(--font-display);
    font-size: 1.02rem; font-weight: 700; color: var(--text);
    margin: 2px 0 6px; line-height: 1.25;
}
.kvTourBody { color: var(--text-muted); font-size: 0.9rem; line-height: 1.55; margin: 0; }
.kvTourCta {
    margin-top: 12px;
    background: none;
    border: 1px solid var(--border-strong);
    color: var(--text);
    padding: 8px 14px;
    border-radius: 9px;
    cursor: pointer;
    font-family: inherit;
    font-weight: 600;
    font-size: 0.82rem;
}
.kvTourCta:hover { border-color: #8B5CF6; color: #8B5CF6; }
.kvTourFoot {
    margin-top: 26px;
    text-align: center;
    padding: 22px;
    border-radius: 14px;
    border: 1px solid var(--border);
    background: linear-gradient(135deg, rgba(37,99,235,0.06), rgba(139,92,246,0.06));
}
.kvTourFoot .kvTourBody { margin-bottom: 4px; }

/* ── Custom icon set ──────────────────────────────────────────────────
   Brand artwork replacing the shipped emoji. Rendered as <img> (never
   inlined — every file defines the same gradient ids, so inlining would
   make them all share whichever loaded first). */
.kvIcon { display: inline-block; vertical-align: middle; flex-shrink: 0; }
/* The nav tile was sized for an emoji GLYPH; an <img> inside it needs the
   box centring the character used to get for free. */
.cc-nav-tile.is-emoji { display: inline-flex; align-items: center; justify-content: center; }
/* The streak/rank slots size their contents by font-size for the emoji
   fallback; an image ignores that, so it carries its own width/height. */
.cc-streak-flame .kvIcon { width: var(--cc-streak-flame-size); height: var(--cc-streak-flame-size); }
.cc-rank-art .kvIcon { max-width: 100%; max-height: 100%; width: auto; height: auto; }
.kvTourIcon .kvIcon { width: 30px; height: 30px; }

/* ── Plan descriptions in the subscription modal ──────────────────────
   These come from Subscription Manager's WYSIWYG field and arrive as
   HTML, so a plan that lists what it includes arrives as a <ul>. The
   browser's default list padding (40px) and paragraph margins (1em) were
   written for a document, not for a 13px line inside a card — without
   these the first bullet sits half a card-width in. */
.mmEduPlanDesc > :first-child { margin-top: 0; }
.mmEduPlanDesc > :last-child  { margin-bottom: 0; }
.mmEduPlanDesc p  { margin: 0 0 6px; }
.mmEduPlanDesc ul,
.mmEduPlanDesc ol { margin: 4px 0; padding-left: 18px; }
.mmEduPlanDesc li { margin: 2px 0; }
.mmEduPlanDesc a  { color: #2563EB; text-decoration: underline; }
/* Authored copy can carry a long unbroken URL or product key; the card is
   a fixed-width flex child, so let it wrap rather than push the layout. */
.mmEduPlanDesc { overflow-wrap: anywhere; }

/* ── Dropdown menus (_kvMenu) ─────────────────────────────────────────
   A trigger button and the panel it opens. The wrap is the positioning
   context, so the panel tracks the button wherever the flex row puts it. */
.kvMenuWrap { position: relative; display: inline-flex; }
.kvMenu {
    position: absolute;
    top: calc(100% + 6px);
    left: 0;
    z-index: 60;
    /* min() so the minimum can never beat the maximum on a narrow phone —
       a plain 234px min-width wins over max-width and overflows. */
    min-width: min(234px, calc(100vw - 16px));
    padding: 6px;
    background: var(--bg-card);
    border: 1px solid var(--border-strong);
    border-radius: 12px;
    box-shadow: 0 14px 34px rgba(15, 23, 42, 0.16);
    /* Backstop for _kvFitPopover: never wider than the screen. */
    max-width: calc(100vw - 16px);
}
/* Opened UPWARD by _kvFitPopover when there is more room above the trigger
   than below it (a menu near the bottom of the screen). */
.kvMenu.kv-pop--up, .kv-share-menu.kv-pop--up { top: auto; bottom: calc(100% + 6px); }
/* .kvMenuItem sets display:flex, and a display rule beats the hidden
   attribute — so state the closed case explicitly rather than relying on
   the UA default. */
.kvMenu[hidden] { display: none; }
/* The create button sits at the right edge of its row; a left-anchored
   panel there would hang off the page on a phone. */
.kvMenu--right { left: auto; right: 0; }
.kvMenuItem {
    display: flex;
    align-items: flex-start;
    gap: 10px;
    width: 100%;
    box-sizing: border-box;
    padding: 9px 10px;
    background: transparent;
    border: 0;
    border-radius: 8px;
    color: var(--text);
    font-family: inherit;
    text-align: left;
    cursor: pointer;
}
.kvMenuItem:hover:not([disabled]),
.kvMenuItem:focus-visible { background: rgba(37, 99, 235, 0.08); outline: none; }
.kvMenuItem[disabled] { opacity: 0.55; cursor: default; }
.kvMenuItemIcon { display: inline-flex; font-size: 15px; line-height: 1.35; flex-shrink: 0; }
.kvMenuItemText { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.kvMenuItemLabel { font-weight: 700; font-size: 13.5px; line-height: 1.35; }
.kvMenuItemHint { color: var(--text-muted); font-size: 12px; line-height: 1.35; }
/* An item can legitimately have no hint — don't leave a blank line for it. */
.kvMenuItemHint:empty { display: none; }

/* ── Stretched set-card link ──────────────────────────────────────────
   Every set card carries a real <a href="/decks/<id>"> covering its box.
   Until this existed there was not a single anchor pointing at a set
   anywhere in the app, so the set pages were unreachable to a crawler
   however good their metadata — and middle-click, cmd/ctrl-click and
   "open in new tab" all did nothing, because a <div role="button"> can
   not do them.

   An overlay rather than making the card root an <a>: the card keeps its
   element, its classes and every existing style, so the FLIP transform,
   the hover lift and the focus ring all keep working. Making the root an
   inline <a> would have silently killed the first two. */
/* 🚨 pointer-events:none is load-bearing, not a detail.
   A stretched link has to win a stacking fight against the card's own content
   to receive the click — and inside .cc-subj-host, where every direct child is
   force-lifted to z-index:1, that fight is unwinnable without also burying the
   in-card buttons, whose ancestors create their own stacking contexts. Two
   attempts at it shipped My Decks and Explore broken.
   So the anchor is now PURELY a crawlable link: it carries the href a crawler
   follows and a human sees in the status bar, and the click is handled by a
   delegated listener on the card itself, which no z-index can interfere with.
   The cost is honest: cmd/middle-click "open in new tab" does not work. It
   never did before this either, so nothing regressed — an improvement was
   traded for reliability. */
.kvSetLink {
    position: absolute;
    inset: 0;
    z-index: 0;
    pointer-events: none;
    border-radius: inherit;
    /* No colour, no underline: the card's own content is the visible link
       text. The anchor itself is a transparent hit area. */
    text-decoration: none;
    color: inherit;
}
/* The card is the focus indicator, since the anchor has no visible box of
   its own. Mirrors the .kvSetOpen:focus-visible ring the cards already had
   when they were role="button". */
.kvSetLink:focus-visible {
    outline: 3px solid var(--accent-blue);
    outline-offset: 2px;
}
/* Anything interactive INSIDE a card has to sit above the overlay, or the
   link swallows its clicks. Share, Block and the author filter. */
.kvCardAction {
    position: relative;
    z-index: 2;
}
/* Visually hidden, still announced. The anchor needs an accessible name —
   without one a screen reader reads a bare "link", seven times over. */
.kvSrOnly {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

/* ── Standing band: store card + quick links ──────────────────────────
   Replaces the brand-art tile. The band is a header, so everything in it is
   deliberately small — the point of shrinking the identity and rank tiles was
   to make room for these without pushing the dashboard's real content down. */
.cc-storecard {
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: 14px;
    padding: 10px 12px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 7px;
    min-width: 0;
}
.cc-storecard-title {
    color: var(--text-dim);
    font-size: 0.68rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    /* Its own full-width line, and never broken across two. The tile is a
       third of the band, which is exactly narrow enough for "GET THE APP" to
       wrap to "GET THE / APP" and eat a badge's worth of height. */
    width: 100%;
    text-align: center;
    white-space: nowrap;
}
/* STACKED, not side by side — the tile is a third of the band, and two badges
   in a row there would each be too small to read. */
.cc-storecard-links { display: flex; flex-direction: column; gap: 6px; align-items: center; }
.cc-storecard-badge { display: block; text-decoration: none; transition: opacity 0.15s ease; }
.cc-storecard-badge:hover { opacity: 0.85; }
.cc-storecard-badge img { height: 38px; width: auto; display: block; }

/* Quick links — same tile language as the identity and rank cards. */
.cc-quicklinks {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(0, 1fr));
    gap: 12px;
}
.cc-quicklink {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 7px;
    padding: 12px 10px;
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: 14px;
    cursor: pointer;
    font-family: inherit;
    color: var(--text);
    transition: transform 0.12s ease, border-color 0.2s ease, box-shadow 0.2s ease;
}
.cc-quicklink:hover {
    transform: translateY(-2px);
    border-color: var(--border-strong);
    box-shadow: var(--shadow-md);
}
.cc-quicklink:focus-visible { outline: 3px solid var(--accent-blue); outline-offset: 2px; }
/* Icon sized to match the avatar and the rank badge — the three tile types
   should look like one family. */
.cc-quicklink-icon { display: inline-flex; width: 46px; height: 46px; align-items: center; justify-content: center; }
.cc-quicklink-icon img { max-width: 100%; max-height: 100%; width: auto; height: auto; }
.cc-quicklink-label {
    font-family: var(--font-display);
    font-weight: 800;
    font-size: 0.86rem;
    line-height: 1.15;
    letter-spacing: var(--tracking-tight, -0.02em);
    white-space: nowrap;
}

/* Explore hero: the card that used to hold the banner now holds the badges. */
.kv-hero-storecard-title {
    color: var(--text-dim);
    font-size: 0.68rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    margin-bottom: 6px;
    text-align: center;
}
.kv-hero-storecard-links { display: flex; flex-direction: column; gap: 8px; align-items: center; }
.kv-hero-storecard-links .cc-storecard-badge img { height: 40px; }

/* The hero pitch's link through to the feature tour. Styled as a text link
   rather than a button: the pitch is prose, and a third button in a hero that
   already has "Sign in" and the store badges would compete with the primary
   action rather than support it. */
.kv-hero-pitch-link {
    display: inline-block;
    margin-top: 8px;
    color: var(--accent-blue, #2563EB);
    font-weight: 700;
    font-size: 0.92rem;
    text-decoration: none;
}
.kv-hero-pitch-link:hover { text-decoration: underline; }
.kv-hero-pitch-link:focus-visible {
    outline: 3px solid var(--accent-blue);
    outline-offset: 3px;
    border-radius: 6px;
}

/* One-off notice at the top of Explore — shown when a link could not be
   honoured (a private or class-only set) and the visitor was sent here
   instead. role="status" so a screen reader announces it on arrival without
   stealing focus. */
.kvExploreNotice {
    display: flex;
    align-items: flex-start;
    gap: 10px;
    padding: 12px 14px;
    margin-bottom: 16px;
    border-radius: 12px;
    background: rgba(245, 158, 11, 0.10);
    border: 1px solid rgba(245, 158, 11, 0.40);
    color: var(--text);
    font-size: 13.5px;
    line-height: 1.5;
}

/* ── Container card behind the top rows ──────────────────────────────
   A purple-tinted panel behind the dashboard's standing band (both rows) and
   Explore's hero row, so the tiles inside read as one group rather than as
   loose cards floating on the page background.

   Tint, not a solid: --brand-purple at ~6% keeps the cards inside legible in
   both themes, where a solid panel would fight the cards' own --bg-card and
   make the group look like a modal. Dark mode needs a touch more to register
   against a near-black ground. */
.kv-topgroup {
    background: linear-gradient(135deg, rgba(139, 92, 246, 0.07), rgba(59, 130, 246, 0.05));
    border: 1px solid rgba(139, 92, 246, 0.22);
    border-radius: 18px;
    padding: 14px;
    /* The panel now owns the gap to whatever follows it. Its children had
       their own bottom margins (22px on .kv-hero, 14px on .cc-standing) and
       those are zeroed below — without this the Explore search input sits
       flush against the panel's border. */
    margin-bottom: 22px;
}
:root[data-theme="dark"] .kv-topgroup,
:root:not([data-theme="light"]) .kv-topgroup {
    background: linear-gradient(135deg, rgba(139, 92, 246, 0.13), rgba(59, 130, 246, 0.09));
    border-color: rgba(139, 92, 246, 0.30);
}
@media (prefers-color-scheme: light) {
    :root:not([data-theme="dark"]) .kv-topgroup {
        background: linear-gradient(135deg, rgba(139, 92, 246, 0.07), rgba(59, 130, 246, 0.05));
        border-color: rgba(139, 92, 246, 0.22);
    }
}
/* The band already carried its own bottom margin; the panel owns the spacing
   now, or the group sits in a gap of its own making. */
.kv-topgroup > .cc-standing { margin-bottom: 0; }
.kv-topgroup > .kv-hero { margin-bottom: 0; }
@media (max-width: 767px) {
    /* Less inset on a phone — the quick-links row already bleeds to the panel
       edges, and doubling the padding would cut its scroll runway. */
    .kv-topgroup { padding: 10px; border-radius: 14px; margin-bottom: 16px; }
}

/* An IMMERSIVE view owns the whole scroll area. .main-panel keeps its own
   padding for every other surface; here it comes off so the artwork reaches
   the edges, and the panel carries data-theme="dark" (set in _showPanel) so
   the page FOOTER — a sibling of the view, which cannot inherit from it —
   wears the dark palette instead of staying white under the hub. */
.main-panel.kv-immersive {
    padding: 0 0 env(safe-area-inset-bottom, 0px);
    background: var(--bg);
}
/* The footer sits on the hub's ground rather than on a white slab. */
.main-panel.kv-immersive .mmedu-footer {
    margin-top: 0;
    background: var(--bg);
    border-top-color: rgba(255, 255, 255, 0.08);
}

/* NATIVE BOTTOM BANNER. The AdMob banner is a native view laid over the
   WebView, pinned just above the home indicator, so the page has to make
   room for it: native-bridge.js sets body.kv-native-banner and
   --kv-native-banner-h (the height AdMob reports) only while it is up.
   Both rules out-rank the plain, mobile and immersive .main-panel padding. */
body.kv-native-banner .main-panel {
    padding-bottom: calc(24px + var(--kv-native-banner-h, 0px) + env(safe-area-inset-bottom, 0px));
}
body.kv-native-banner .main-panel.kv-immersive {
    padding-bottom: calc(var(--kv-native-banner-h, 0px) + env(safe-area-inset-bottom, 0px));
}
/* The bar BEHIND the banner. AdMob keeps the ad above the home indicator on
   purpose (a swipe home must never land on the ad — invalid clicks), which
   left a strip below it where the page scrolled past. This solid bar runs
   from the screen's bottom edge up to the ad's top, full width, so the ad and
   the strip read as one bar sitting on the edge. The native ad view is drawn
   OVER the WebView, so this sits underneath it and never covers the ad.
   Only while the banner is up — the class comes off whenever it hides. */
body.kv-native-banner::after {
    content: '';
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    height: calc(var(--kv-native-banner-h, 0px) + env(safe-area-inset-bottom, 0px));
    background: var(--bg-card);
    border-top: 1px solid var(--border);
    z-index: 2147483000;
    pointer-events: none;
}

/* WEB bottom-of-page ad (zone homepage-bottom), injected by app.js above the
   footer on content views. Empty until AdDisplay fills it, and AdDisplay
   leaves it empty for students and ad-free accounts, so it takes no space
   then. */
.kv-web-bottom-ad { margin: 28px auto 8px; max-width: 980px; text-align: center; }
.kv-web-bottom-ad:empty { margin: 0; }

/* "GO AD-FREE" (native app). The page line is inserted by app.js in the
   native app only and shown ONLY while the bottom banner is up, so students,
   paid accounts and anyone on a screen without the banner never see it. It
   sits above the footer — never against the ad itself (AdMob forbids tappable
   elements beside an ad). */
.kv-adfree-line { display: none; }
body.kv-native-banner .kv-adfree-line {
    display: block;
    margin: 28px auto 4px;
    max-width: 560px;
    padding: 12px 16px;
    border: 1px dashed var(--border-strong, var(--border));
    border-radius: 12px;
    text-align: center;
    font-size: 13px;
    line-height: 1.55;
    color: var(--text-muted);
}
.kv-adfree-line button {
    background: transparent;
    border: 0;
    padding: 0;
    margin-left: 2px;
    color: var(--accent-blue, #2563EB);
    font: inherit;
    font-weight: 700;
    cursor: pointer;
}
/* The once-a-week card after a full-screen ad. At the TOP of the screen, well
   away from the bottom banner. */
.kv-adfree-nudge {
    position: fixed;
    left: 50%;
    top: max(16px, calc(env(safe-area-inset-top, 0px) + 10px));
    transform: translateX(-50%);
    z-index: 29000;
    width: min(92vw, 460px);
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 12px 12px 12px 16px;
    background: var(--bg-card);
    color: var(--text);
    border: 1px solid var(--border);
    border-radius: 14px;
    box-shadow: 0 10px 32px rgba(15, 23, 42, 0.18);
    font-size: 13.5px;
    font-weight: 600;
}
.kv-adfree-nudge__text { flex: 1; min-width: 0; line-height: 1.4; }
.kv-adfree-nudge__go {
    flex: 0 0 auto;
    background: linear-gradient(135deg, #2563EB, #8B5CF6);
    color: #fff;
    border: 0;
    border-radius: 9px;
    padding: 8px 12px;
    font: inherit;
    font-size: 13px;
    font-weight: 700;
    cursor: pointer;
}
.kv-adfree-nudge__x {
    flex: 0 0 auto;
    background: transparent;
    border: 0;
    color: var(--text-muted);
    font-size: 20px;
    line-height: 1;
    padding: 2px 6px;
    cursor: pointer;
}

/* ===================================================================
   PLAY DASHBOARD — /play/dashboard
   The game hub from apps/knovli/design/playSpash.png. Scoped entirely
   under .kv-play so none of it can leak into another view.
   =================================================================== */
.kv-play {
    position: relative;
    /* 🚨 No negative margins. They were -18px, guessing at .view-panel's
       padding — but the padding is .main-panel's and it is 24px/22px, so a
       light strip showed down each side. _showPanel now puts .kv-immersive on
       the panel and the padding comes off there instead, which cannot drift.
       Fill the viewport too, or the page shows below the content. */
    margin: 0;
    padding: 0 0 32px;
    min-height: calc(100vh - var(--topbar-height));
    box-sizing: border-box;
    border-radius: 0;
    overflow: hidden;
    /* 🔑 NO palette of its own. renderPlayDashboard() stamps
       data-theme="dark" on #playDashboardView, so --bg / --text /
       --text-muted already resolve to the dark theme inside here — even when
       the reader is on the light theme. Hardcoding colours would mean the hub
       stopped following the theme the moment it changed. */
    background: var(--bg);
    color: var(--text);
}
/* The room artwork. A separate layer rather than a background on .kv-play
   so the scrim can sit between it and the content. */
.kv-play-bg {
    position: absolute;
    inset: 0;
    background-image:
        /* Darkest at the foot, where the cards and text sit. Built from the
           theme ground so it stays in step with it. */
        /* 🔑 SEASONAL THEME: the scrim base and the artwork are both
           overridable, and each keeps TODAY'S value as its fallback — with no
           theme set this renders exactly what it always has. */
        linear-gradient(180deg,
            rgba(var(--kv-th-scrim, 17, 24, 43), 0.38) 0%,
            rgba(var(--kv-th-scrim, 17, 24, 43), 0.62) 46%,
            rgba(var(--kv-th-scrim, 17, 24, 43), 0.92) 100%),
        var(--kv-th-play-bg, url('i/play-bg.webp'));
    background-size: cover, cover;
    background-position: center top, center top;
    background-repeat: no-repeat, no-repeat;
    pointer-events: none;
}
.kv-play-inner {
    position: relative;   /* above .kv-play-bg */
    z-index: 1;
    padding: clamp(20px, 4vw, 44px) clamp(16px, 3.5vw, 40px) 0;
}
/* The hero is a row now: greeting on the left, the player's own card top
   right (design: playSpash.png). It wraps rather than shrinking the greeting,
   because the card's numbers stop being readable long before the title does. */
.kv-play-hero {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: clamp(12px, 2vw, 24px);
    flex-wrap: wrap;
}
.kv-play-hero-text { min-width: min(100%, 280px); flex: 1 1 320px; }

/* ── The player's card ── avatar, level, streak, XP, and a chevron.
   The WHOLE card is the link, so the chevron and the card cannot disagree
   about where they go. */
.kv-play-me {
    display: flex;
    align-items: center;
    gap: clamp(10px, 1.2vw, 16px);
    padding: 10px clamp(12px, 1.4vw, 18px);
    border-radius: 999px;
    border: 1px solid rgba(148, 163, 184, 0.28);
    background: rgba(15, 23, 42, 0.55);
    color: var(--text, #F1F5F9);
    font: inherit;
    cursor: pointer;
    flex: 0 1 auto;
    max-width: 100%;
    text-align: left;
    transition: border-color .18s ease, background-color .18s ease, transform .18s ease;
}
.kv-play-me:hover { border-color: rgba(148, 163, 184, 0.55); background: rgba(15, 23, 42, 0.72); }
.kv-play-me:active { transform: translateY(1px); }
.kv-play-me:focus-visible { outline: 2px solid var(--accent-blue, #2563EB); outline-offset: 3px; }
.kv-play-me[hidden] { display: none; }

.kv-play-me-av {
    flex: 0 0 auto;
    width: clamp(38px, 3.4vw, 48px);
    height: clamp(38px, 3.4vw, 48px);
    border-radius: 50%;
    overflow: hidden;
    display: grid;
    place-items: center;
    background: linear-gradient(135deg, var(--accent-cyan, #2FB2EA), var(--accent-purple, #8B5CF6));
    color: #FFFFFF;
    font-weight: 800;
    font-size: clamp(0.95rem, 1.5vw, 1.15rem);
}
.kv-play-me-av img { width: 100%; height: 100%; object-fit: cover; display: block; }

.kv-play-me-who { display: flex; flex-direction: column; min-width: 0; }
.kv-play-me-who b {
    font-size: clamp(0.9rem, 1.3vw, 1.05rem);
    font-weight: 800;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
    max-width: clamp(110px, 12vw, 200px);
}
.kv-play-me-who span {
    font-size: clamp(0.7rem, 1vw, 0.8rem);
    color: var(--text-muted, #94A3B8);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
    max-width: clamp(110px, 12vw, 200px);
}
.kv-play-me-who span:empty { display: none; }

.kv-play-me-stat { display: flex; align-items: center; gap: 7px; flex: 0 0 auto; }
.kv-play-me-stat > span:last-child { display: flex; flex-direction: column; line-height: 1.15; }
.kv-play-me-stat b { font-size: clamp(0.95rem, 1.4vw, 1.15rem); font-weight: 800; }
.kv-play-me-stat span span { font-size: clamp(0.62rem, 0.85vw, 0.72rem); color: var(--text-muted, #94A3B8); white-space: nowrap; }
.kv-play-me-ico { font-size: clamp(1rem, 1.5vw, 1.25rem); line-height: 1; }
.kv-play-me-chev { font-size: 1.5rem; line-height: 1; color: var(--text-muted, #94A3B8); flex: 0 0 auto; }

/* On a phone the card would push the greeting to one word per line. It takes
   its own full-width row under the greeting instead, and the name column is
   allowed to grow now that nothing competes with it. */
@media (max-width: 720px) {
    .kv-play-me { width: 100%; border-radius: 16px; justify-content: space-between; }
    .kv-play-me-who b, .kv-play-me-who span { max-width: none; }
}

.kv-play-hero h1 {
    margin: 0 0 6px;
    font-size: clamp(1.7rem, 4.2vw, 2.9rem);
    font-weight: 800;
    letter-spacing: -0.5px;
    line-height: 1.1;
}
.kv-play-hero-brand {
    background: linear-gradient(135deg, var(--accent-cyan, #2FB2EA), var(--accent-purple, #8B5CF6));
    -webkit-background-clip: text; background-clip: text;
    -webkit-text-fill-color: transparent; color: transparent;
}
.kv-play-hero p {
    margin: 0 0 clamp(18px, 3vh, 28px);
    color: var(--text-muted);
    font-size: clamp(0.9rem, 1.6vw, 1.05rem);
}

/* ── The three mode cards ── */
.kv-play-modes {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: clamp(12px, 1.6vw, 20px);
    /* The arc drops the outer cards' bottoms well below the centre one, so the
       next section needs real clearance, not a token gap. */
    margin-bottom: clamp(34px, 6vh, 72px);
    /* The mockup's cards sit in a shallow 3D fan — the outer two turned in
       toward the middle and tilted back, the centre one facing front. The
       perspective lives on the ROW so all three share one vanishing point;
       per-card perspective would give each its own and the fan would read as
       three unrelated tilts. */
    /* 🔑 The CARDS are the only capped thing on this screen. Full width they
       grew to ~600px each; everything below them — the quick links, the games
       rail — runs the full width, and so does the artwork behind. */
    /* 1200, not 1280: the cap used to sit on .kv-play-inner, INSIDE its ~40px
       side padding, so the row itself was 1200 and each card 387px. Capping
       the row directly has to subtract that padding or the cards come back
       bigger than they were (413px). */
    max-width: 1200px;
    margin-left: auto;
    margin-right: auto;
    /* A closer vanishing point exaggerates the turn — the same rotation reads
       as far more angle at 1100px than at 1600px. */
    perspective: 1100px;
    perspective-origin: 50% 38%;
}
/* One column on a phone: three of these side by side stop being tappable
   long before they stop fitting. */
@media (max-width: 820px) { .kv-play-modes { grid-template-columns: 1fr; } }
.kv-play-mode {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
    padding: 18px 16px clamp(16px, 2vw, 22px);
    border-radius: 18px;
    overflow: hidden;               /* clip the bleeding artwork to the radius */
    border: 1px solid rgba(255, 255, 255, 0.14);
    background: linear-gradient(160deg, rgba(255, 255, 255, 0.07), rgba(255, 255, 255, 0.02));
    color: var(--text);
    font-family: inherit;
    cursor: pointer;
    text-align: center;
    transition: transform 0.16s ease, border-color 0.16s ease, box-shadow 0.16s ease;
}
/* Lean: outer cards rotate IN toward the centre, all three tip back a little.
   translateZ pulls the turned cards forward so the fan does not look like it
   is sinking. */
/* The ARC: the outer two turn hard toward the middle and sit back.
   🔑 transform-origin is the TOP edge on all three. Depth scales a card —
   the centre one sits forward so it grows, the outer ones sit back so they
   shrink — and with a centred origin that scaling pushed the middle card's top
   ~33px above its neighbours. Pivoting at the top pins all three tops level
   and lets the difference show along the BOTTOM instead, which is where the
   curve wants to be anyway. The outer cards keep their inward pivot (top
   right / top left), so the fan still turns toward the middle. */
.kv-play-mode--solo  { transform: rotateY(20deg)  rotateX(5deg) translateZ(-90px); transform-origin: top right; }
.kv-play-mode--duo   { transform: rotateX(5deg)   translateZ(18px) translateY(6px); transform-origin: top center; }
/* 🚨 The 6px is a MEASURED nudge, not a guess. Depth scales a card, so the
   forward centre card's top sat ~33px above its neighbours; pivoting every
   card from its top edge took that to 6px, and this closes it. Verified level
   at 300px, 382px and 460px card heights — the residue comes from the
   perspective projection and is near-constant, so px beats %. */
.kv-play-mode--group { transform: rotateY(-20deg) rotateX(5deg) translateZ(-90px); transform-origin: top left; }
/* Hover straightens the card and brings it forward — it comes to meet you. */
/* Hover straightens the card completely and brings it to the front — the
   bigger the resting angle, the more that reads as "this one, then". */
.kv-play-mode:hover         { transform: rotateY(0deg) rotateX(0deg) translateZ(60px); }
.kv-play-mode:focus-visible { transform: rotateY(0deg) rotateX(0deg) translateZ(60px); }
/* 🚨 Stacked on a phone the fan reads as wonky rather than designed — one
   column of tilted cards has nothing to fan around. Flatten it. */
@media (max-width: 820px) {
    .kv-play-modes { perspective: none; }
    .kv-play-mode--solo,
    .kv-play-mode--duo,
    .kv-play-mode--group { transform: none; }
    .kv-play-mode:hover  { transform: translateY(-3px); }
}
/* Someone who has asked for less motion gets the flat row too. */
@media (prefers-reduced-motion: reduce) {
    .kv-play-modes { perspective: none; }
    .kv-play-mode,
    .kv-play-mode--solo,
    .kv-play-mode--duo,
    .kv-play-mode--group,
    .kv-play-mode:hover { transform: none; transition: none; }
}
.kv-play-mode:focus-visible { outline: 2px solid var(--accent-cyan, #2FB2EA); outline-offset: 3px; }
/* One accent per mode, matching the mockup: blue solo, violet duo, green group. */
.kv-play-mode--solo  { border-color: rgba(59, 130, 246, 0.45); box-shadow: 0 0 0 1px rgba(59, 130, 246, 0.12), 0 10px 30px rgba(59, 130, 246, 0.14); }
.kv-play-mode--duo   { border-color: rgba(139, 92, 246, 0.45); box-shadow: 0 0 0 1px rgba(139, 92, 246, 0.12), 0 10px 30px rgba(139, 92, 246, 0.14); }
.kv-play-mode--group { border-color: rgba(16, 185, 129, 0.45); box-shadow: 0 0 0 1px rgba(16, 185, 129, 0.12), 0 10px 30px rgba(16, 185, 129, 0.14); }
.kv-play-mode--solo:hover  { border-color: rgba(59, 130, 246, 0.85); }
.kv-play-mode--duo:hover   { border-color: rgba(139, 92, 246, 0.85); }
.kv-play-mode--group:hover { border-color: rgba(16, 185, 129, 0.85); }
.kv-play-mode-art { display: flex; align-items: center; justify-content: center; margin-bottom: 2px; }
/* THE WHOLE CARD AS AN IMAGE (KV_PLAY_CARD_ART). The supplied art has the
   title, tagline and button baked in, so the button holds the image and
   nothing else — no padding, no border, no background of our own to peek out
   from behind a transparent PNG. The lean, the hover and the focus ring still
   come from .kv-play-mode. */
.kv-play-mode--card {
    padding: 0;
    border: none;
    background: none;
    box-shadow: none;
    display: block;
}
.kv-play-mode--card img {
    width: 100%;
    height: auto;
    display: block;
    border-radius: 18px;
}
/* The per-mode accent glow belongs to the card edge, and these PNGs are
   transparent-cornered — so it goes on the IMAGE, which has the real shape,
   rather than on the button box behind it. */
.kv-play-mode--card.kv-play-mode--solo  img { box-shadow: 0 10px 34px rgba(59, 130, 246, 0.22); }
.kv-play-mode--card.kv-play-mode--duo   img { box-shadow: 0 10px 34px rgba(139, 92, 246, 0.22); }
.kv-play-mode--card.kv-play-mode--group img { box-shadow: 0 10px 34px rgba(16, 185, 129, 0.22); }

/* Mode ARTWORK (window.MMEDU_PLAY_BRAND) — the fallback when no card image is
   set. These are wide banners, so the card gives them a full-width block
   with a fixed ratio; object-fit keeps any future art from distorting. */
.kv-play-mode-art--img {
    width: calc(100% + 32px);          /* bleed past the card's side padding */
    margin: -18px -16px 10px;
    aspect-ratio: 16 / 7;
    border-radius: 17px 17px 0 0;
    overflow: hidden;
    background: rgba(255, 255, 255, 0.04);
}
.kv-play-mode-art--img img {
    width: 100%; height: 100%;
    object-fit: cover;
    display: block;
}
/* The artwork carries the mode name, so the card starts at the tagline. */
.kv-play-mode-art--img + .kv-play-mode-tag { margin-top: 2px; font-size: 0.9rem; }
.kv-play-mode-title {
    font-size: clamp(1.1rem, 2.2vw, 1.5rem);
    font-weight: 800;
    letter-spacing: 0.5px;
}
.kv-play-mode-title b { font-size: 1.25em; color: var(--accent-cyan, #2FB2EA); }
.kv-play-mode--duo   .kv-play-mode-title b { color: #C084FC; }
.kv-play-mode-tag { color: var(--text-muted); font-size: 0.85rem; }
.kv-play-mode-cta {
    margin-top: 10px;
    width: 100%;
    padding: 11px 14px;
    border-radius: 999px;
    font-weight: 700;
    font-size: 0.95rem;
    color: #FFFFFF;
}
.kv-play-mode--solo  .kv-play-mode-cta { background: linear-gradient(135deg, #2563EB, #3B82F6); }
.kv-play-mode--duo   .kv-play-mode-cta { background: linear-gradient(135deg, #7C3AED, #A855F7); }
.kv-play-mode--group .kv-play-mode-cta { background: linear-gradient(135deg, #059669, #10B981); }

/* ── Quick links ── */
.kv-play-links {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: clamp(10px, 1.2vw, 14px);
    margin-bottom: clamp(18px, 3vh, 30px);
}
@media (max-width: 980px) { .kv-play-links { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 520px) { .kv-play-links { grid-template-columns: 1fr; } }
.kv-play-link {
    display: flex;
    align-items: center;
    gap: 11px;
    padding: 13px 14px;
    border-radius: 14px;
    border: 1px solid rgba(255, 255, 255, 0.10);
    background: rgba(255, 255, 255, 0.05);
    color: var(--text);
    font-family: inherit;
    text-align: left;
    cursor: pointer;
    transition: background 0.14s ease, border-color 0.14s ease;
}
.kv-play-link:hover { background: rgba(255, 255, 255, 0.09); border-color: rgba(255, 255, 255, 0.20); }
.kv-play-link:focus-visible { outline: 2px solid var(--accent-cyan, #2FB2EA); outline-offset: 2px; }
.kv-play-link-icon { flex-shrink: 0; display: flex; }
.kv-play-link-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.kv-play-link-text b { font-size: 0.92rem; font-weight: 700; }
.kv-play-link-text span { font-size: 0.76rem; color: var(--text-muted); }
.kv-play-link-chev { margin-left: auto; color: var(--text-dim); font-size: 1.3rem; line-height: 1; }

/* ── Featured rail ── */
.kv-play-section-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
.kv-play-section-head h2 { margin: 0; font-size: clamp(1.05rem, 2vw, 1.3rem); font-weight: 800; }
.kv-play-rail {
    display: flex;
    gap: 12px;
    overflow-x: auto;
    padding-bottom: 6px;
    scrollbar-width: none;
    -ms-overflow-style: none;
}
.kv-play-rail::-webkit-scrollbar { display: none; }
/* ✂️ .kv-play-tile (the old mode rail) removed with the rail itself — the
   section is Featured Games now. See .kv-feat below. */

/* ── Featured Games ── the top quiz+format pairs people actually play ──── */
/* The rail already scrolls horizontally (.kv-play-rail above); these cards are
   wider than the old mode tiles, so 10 of them overflow on purpose. */
.kv-feat-rail { gap: clamp(10px, 1.2vw, 16px); scroll-snap-type: x proximity; }
.kv-feat {
    flex: 0 0 clamp(190px, 21vw, 248px);
    scroll-snap-align: start;
    display: flex;
    flex-direction: column;
    gap: 8px;
    padding: 0 0 10px;
    border-radius: 16px;
    border: 1px solid rgba(255, 255, 255, 0.12);
    background: rgba(255, 255, 255, 0.05);
    color: inherit;
    font: inherit;
    text-align: left;
    cursor: pointer;
    overflow: hidden;
    transition: transform .15s ease, border-color .15s ease, background-color .15s ease;
}
.kv-feat:hover { transform: translateY(-2px); background: rgba(255, 255, 255, 0.09); border-color: rgba(255, 255, 255, 0.24); }
.kv-feat:focus-visible { outline: 2px solid var(--accent-cyan, #2FB2EA); outline-offset: 2px; }

.kv-feat-art {
    position: relative;
    display: block;
    width: 100%;
    aspect-ratio: 16 / 10;
    overflow: hidden;
    background: rgba(8, 15, 32, 0.6);
}
.kv-feat-art img { width: 100%; height: 100%; object-fit: cover; display: block; }
.kv-feat-art-fallback { position: absolute; inset: 0; display: grid; place-items: center; opacity: 0.7; }

/* 🚨 The tags sit OVER an admin-uploaded picture, which can be any
   brightness — white-on-white is not a readable tag. The scrim is part of the
   art, not the tag, so a long format name cannot outrun its own backing. */
.kv-feat-art::after {
    content: '';
    position: absolute;
    inset: auto 0 0 0;
    height: 62%;
    background: linear-gradient(180deg, rgba(8, 15, 32, 0) 0%, rgba(8, 15, 32, 0.82) 100%);
    pointer-events: none;
}
.kv-feat-tags {
    position: absolute;
    left: 8px; right: 8px; bottom: 8px;
    display: flex; flex-wrap: wrap; gap: 6px;
    z-index: 1;
}
.kv-feat-tag {
    padding: 3px 9px;
    border-radius: 999px;
    font-size: 0.68rem;
    font-weight: 800;
    letter-spacing: 0.02em;
    color: #FFFFFF;
    background: rgba(15, 23, 42, 0.78);
    border: 1px solid rgba(255, 255, 255, 0.28);
    max-width: 100%;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* The mode tag takes the mode's own hue, so "2 Player" reads as the duel
   colour the rest of the flow uses for it. */
.kv-feat--solo  .kv-feat-tag--mode { background: rgba(37, 99, 235, 0.92); border-color: rgba(147, 197, 253, 0.6); }
.kv-feat--duo   .kv-feat-tag--mode { background: rgba(139, 92, 246, 0.92); border-color: rgba(196, 181, 253, 0.6); }

.kv-feat-name {
    padding: 0 10px;
    font-size: 0.85rem;
    font-weight: 700;
    line-height: 1.3;
    display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
    overflow: hidden;
}
.kv-feat-fmt { font-weight: 600; opacity: 0.72; }

@media (prefers-reduced-motion: reduce) { .kv-feat:hover { transform: none; } }

/* ===================================================================
   CHOOSING A MODE — the hub → format-selection transition
   The chosen card swells and glows, the rest fall back, the chosen one
   dissolves, and a wave in that mode's colour crosses the screen.
   =================================================================== */
.kv-play-modes.kv-play-choosing .kv-play-mode { pointer-events: none; }
/* The others fall back: smaller, dimmer, desaturated. */
.kv-play-mode.is-dimmed {
    transform: scale(0.9) translateZ(-140px) !important;
    opacity: 0.25;
    filter: saturate(0.5) blur(1px);
    transition: transform 0.42s cubic-bezier(0.4, 0, 0.2, 1),
                opacity 0.42s ease, filter 0.42s ease;
}
/* The chosen one swells, glows in its own colour, then dissolves. */
.kv-play-mode.is-chosen {
    z-index: 2;
    animation: kvChosenSwell 0.86s cubic-bezier(0.34, 1.2, 0.64, 1) forwards;
}
.kv-play-mode--solo.is-chosen  img { box-shadow: 0 0 0 2px rgba(59,130,246,.9),  0 0 64px 12px rgba(59,130,246,.75)  !important; }
.kv-play-mode--duo.is-chosen   img { box-shadow: 0 0 0 2px rgba(192,38,211,.9),  0 0 64px 12px rgba(192,38,211,.75)  !important; }
.kv-play-mode--group.is-chosen img { box-shadow: 0 0 0 2px rgba(16,185,129,.9),  0 0 64px 12px rgba(16,185,129,.75)  !important; }
/* Starts flat and forward rather than from wherever the card was resting: the
   chosen card straightens INTO the swell, which is the same motion hover makes
   and keeps the two reading as one gesture. */
@keyframes kvChosenSwell {
    0%   { transform: rotateY(0) rotateX(0) translateZ(40px) scale(1);    opacity: 1; }
    46%  { transform: rotateY(0) rotateX(0) translateZ(90px) scale(1.09); opacity: 1; }
    /* Dissolve: it brightens out rather than simply fading, so it reads as
       turning into the wave rather than being switched off. */
    100% { transform: rotateY(0) rotateX(0) translateZ(130px) scale(1.16); opacity: 0; filter: brightness(1.7); }
}

/* The wave. position:fixed so it crosses the WHOLE screen — the hub bleeds to
   the edges, and a panel-sized wipe would show its seams. */
.kv-play-wave {
    position: fixed;
    inset: 0;
    z-index: 9000;
    pointer-events: none;
    transform: scaleX(0);
    transform-origin: left center;
    background: linear-gradient(100deg,
                transparent 0%,
                var(--kv-wave-color, #3B82F6) 18%,
                var(--kv-wave-color, #3B82F6) 82%,
                rgba(255, 255, 255, 0.92) 100%);
    will-change: transform;
}
.kv-play-wave.is-covering {
    transform: scaleX(1);
    transition: transform 0.46s cubic-bezier(0.66, 0, 0.34, 1);
}
/* Leaves to the RIGHT rather than retreating the way it came, so the motion
   reads as one sweep across the screen. */
.kv-play-wave.is-leaving {
    transform-origin: right center;
    transform: scaleX(0);
    transition: transform 0.5s cubic-bezier(0.66, 0, 0.34, 1);
}

/* 🚨 Someone who asked for less motion gets none of it: no swell, no dimming,
   no wave. The JS also skips straight to the destination, so this is belt and
   braces rather than the only guard. */
@media (prefers-reduced-motion: reduce) {
    .kv-play-mode.is-chosen,
    .kv-play-mode.is-dimmed { animation: none !important; transform: none !important;
                              opacity: 1 !important; filter: none !important; transition: none !important; }
    .kv-play-wave { display: none !important; }
}

/* ===================================================================
   FORMAT SELECTION (/play/format) — phase 1 shell
   Light, as the mockup has it; the hub it came from is the dark one.
   =================================================================== */
/* Full width, like everything below the mode cards on the hub. */
.kv-fmt { width: 100%; }
.kv-fmt-hero {
    position: relative;
    border-radius: 18px;
    padding: clamp(22px, 4vw, 40px);
    margin-bottom: clamp(18px, 3vh, 30px);
    color: #FFFFFF;
    background: linear-gradient(120deg, #0E2143 0%, #16305C 60%, #1E3A6E 100%);
    overflow: hidden;
}
/* The featured-quiz banner, behind the hero's own gradient.
   🚨 Nothing is revealed until the image has LOADED (see _kvApplyHeroBanner):
   the scrim over a dead URL would just darken the hero for no reason. */
.kv-fmt-hero-bg {
    position: absolute; inset: 0;
    background-size: cover; background-position: center;
    opacity: 0; transition: opacity 0.45s ease;
}
.kv-fmt-hero-bg.is-on { opacity: 1; }
/* 🚨 The hero's text is WHITE, and it was white on a gradient this stylesheet
   controls. A photograph can be any colour, so the banner has to bring its own
   guarantee — without this scrim a pale banner makes the heading unreadable. */
.kv-fmt-hero-bg.is-on::after {
    content: ''; position: absolute; inset: 0;
    /* 🚨 TWO constraints at once, and they pull against each other.
       (1) The mode's hue has to survive: a neutral scrim hid it completely,
           so the banner made every mode look the same.
       (2) White text has to stay readable over ANY uploaded image, whose
           worst case is pure white. Composited on white, white text needs
           4.5:1, and the LIGHTEST stop is what decides — the hero's
           paragraph wraps across the full width.
       So the tint is the mode's own DARK and MID stops, never its bright
       one: #2563EB at 0.62 measures 2.6:1 and #10B981 measures 1.8:1. At
       the alphas below the weakest mode (green) measures 5.11:1.
       play-hero-banner.test.js recomputes every mode × every stop from
       these exact values. */
    background: linear-gradient(100deg,
        rgba(var(--kv-hero-tint),   0.94)   0%,
        rgba(var(--kv-hero-tint),   0.90)  38%,
        rgba(var(--kv-hero-tint-2), 0.86)  72%,
        rgba(var(--kv-hero-tint-2), 0.82) 100%);
}
/* The hue each mode tints its banner with — the dark and mid stops of the
   same gradient the hero uses when there is no banner, so tinted and untinted
   heroes read as the same screen. */
.kv-fmt-hero        { --kv-hero-tint:  8, 15, 32; --kv-hero-tint-2: 22,  40,  78; }
/* 🔑 Only the SOLO tone is themed. Duo and group are SEMANTIC — purple is
   1v1, green is Group Play, and "the screen remembers what you picked" stops
   working if a season repaints them. */
.kv-fmt--solo  .kv-fmt-hero {
    --kv-hero-tint:   var(--kv-th-hero-tint,  11, 30, 63);
    --kv-hero-tint-2: var(--kv-th-hero-tint-2, 23, 64, 127);
}
.kv-fmt--duo   .kv-fmt-hero { --kv-hero-tint: 42, 11, 63; --kv-hero-tint-2: 91,  23, 128; }
.kv-fmt--group .kv-fmt-hero { --kv-hero-tint:  6, 43, 34; --kv-hero-tint-2: 11,  92,  70; }
/* The backdrop is absolutely positioned, so everything else needs a layer
   above it or the text renders behind the image. */
.kv-fmt-hero > *:not(.kv-fmt-hero-bg) { position: relative; z-index: 1; }
.kv-fmt-hero-credit {
    display: block; margin-top: 12px;
    font-size: 0.72rem; font-weight: 700; letter-spacing: 0.4px;
    text-transform: uppercase; color: rgba(255, 255, 255, 0.78);
}
.kv-fmt-hero-credit[hidden] { display: none; }

/* The chosen mode tints the hero, so the screen remembers what you picked. */
.kv-fmt--solo  .kv-fmt-hero {
    background: linear-gradient(120deg,
        var(--kv-th-hero-from, #0B1E3F) 0%,
        var(--kv-th-hero-via,  #17407F) 60%,
        var(--kv-th-hero-to,   #2563EB) 130%);
}
.kv-fmt--duo   .kv-fmt-hero { background: linear-gradient(120deg, #2A0B3F 0%, #5B1780 60%, #C026D3 130%); }
.kv-fmt--group .kv-fmt-hero { background: linear-gradient(120deg, #062B22 0%, #0B5C46 60%, #10B981 130%); }
.kv-fmt-hero-pill {
    display: inline-block;
    padding: 5px 13px;
    border-radius: 999px;
    background: rgba(255, 255, 255, 0.18);
    font-size: 0.72rem;
    font-weight: 800;
    letter-spacing: 1.2px;
    text-transform: uppercase;
    margin-bottom: 12px;
}
.kv-fmt-hero h1 { margin: 0 0 8px; font-size: clamp(1.5rem, 3.4vw, 2.4rem); font-weight: 800; }
.kv-fmt-hero p  { margin: 0; opacity: 0.86; font-size: clamp(0.86rem, 1.5vw, 1rem); max-width: 60ch; }

/* The heading row: back chevron and the chosen mode's image on the LEFT, the
   heading itself still centred in the space that is left. */
.kv-fmt-head {
    display: flex;
    align-items: center;
    gap: 12px;
    margin-bottom: clamp(14px, 2.4vh, 22px);
}
.kv-fmt-head-titles { flex: 1; min-width: 0; text-align: center; }
/* The left controls have a twin on the right with the same width, so the
   centred heading is centred on the ROW rather than pushed off by them. */
.kv-fmt-head-left,
.kv-fmt-head-spacer { flex: 0 0 auto; display: flex; align-items: center; gap: 10px; min-width: 0; }
.kv-fmt-back {
    display: flex; align-items: center; justify-content: center;
    width: 40px; height: 40px; flex-shrink: 0;
    border-radius: 50%;
    border: 1px solid var(--border-strong, rgba(15, 23, 42, 0.14));
    background: var(--bg-card, #FFFFFF);
    color: var(--text);
    font-size: 1.25rem; line-height: 1;
    cursor: pointer;
    transition: background 0.14s ease, transform 0.14s ease;
}
.kv-fmt-back:hover { background: var(--surface-2, rgba(15, 23, 42, 0.06)); transform: translateX(-2px); }
.kv-fmt-back:focus-visible { outline: 2px solid var(--accent-blue, #2563EB); outline-offset: 2px; }
@media (max-width: 620px) {
    /* No room to balance it on a phone — the heading goes left with the rest. */
    .kv-fmt-head-spacer { display: none; }
    .kv-fmt-head-titles { text-align: left; }
}
.kv-fmt-head h2 { margin: 0 0 4px; font-size: clamp(1.2rem, 2.6vw, 1.9rem); font-weight: 800; color: var(--text); }
.kv-fmt-head p  { margin: 0 0 14px; color: var(--text-muted); font-size: 0.9rem; }

/* The launch veil: covers the 1v1 plugin's own dashboard and picker while the
   flow hands it the choices the player already made. Same throbbing-icon
   language as the solo runner's loading screen, so both modes feel alike.
   🚨 Positioned over #pluginFullscreenContent, which is why it needs its own
   dark ground — the plugin's surface may not have painted yet. */
.kv-launch-veil {
    position: absolute; inset: 0; z-index: 30;
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    gap: 14px; padding: 32px 20px; text-align: center; box-sizing: border-box;
    background: #0B0F1A; color: #F1F5F9;
}
.kv-launch-icon {
    font-size: 4.2rem; line-height: 1;
    animation: kvLaunchThrob 1.6s ease-in-out infinite;
}
.kv-launch-title { margin: 4px 0 0; font-size: clamp(1.25rem, 3.4vw, 1.9rem); font-weight: 800; }
.kv-launch-sub   { margin: 0; font-size: clamp(0.9rem, 2.2vw, 1.05rem); opacity: 0.75; }
.kv-launch-bar {
    width: min(320px, 80%); height: 6px; border-radius: 99px; overflow: hidden;
    background: rgba(255, 255, 255, 0.14);
}
.kv-launch-bar > span {
    display: block; height: 100%; width: 35%; border-radius: 99px;
    background: linear-gradient(90deg, #8B5CF6, #2563EB);
    animation: kvLaunchSlide 1.25s ease-in-out infinite;
}
@keyframes kvLaunchThrob { 0%, 100% { transform: scale(1); opacity: 0.92; } 50% { transform: scale(1.09); opacity: 1; } }
@keyframes kvLaunchSlide { 0% { transform: translateX(-110%); } 100% { transform: translateX(320%); } }
@media (prefers-reduced-motion: reduce) {
    .kv-launch-icon { animation: none; }
    .kv-launch-bar > span { animation: none; width: 100%; }
}

/* ── Public Arena: the live 1v1 lobby ────────────────────────────── */
.kv-arena-tabs {
    display: flex; gap: 6px; padding: 5px;
    margin: 0 auto clamp(14px, 2.4vh, 22px);
    width: fit-content; max-width: 100%;
    border-radius: 999px;
    background: var(--surface-2, rgba(15, 23, 42, 0.06));
}
.kv-arena-tab {
    appearance: none; border: none; cursor: pointer;
    padding: 9px 20px; border-radius: 999px;
    background: transparent; color: var(--text-muted);
    font: inherit; font-size: 0.86rem; font-weight: 700;
    transition: background 0.18s ease, color 0.18s ease;
}
.kv-arena-tab[aria-selected="true"] { background: var(--accent-blue, #2563EB); color: #FFFFFF; }
.kv-arena-tab:focus-visible { outline: 2px solid var(--accent-blue, #2563EB); outline-offset: 2px; }

/* §17 FIND A MATCH, above the lobby list. */
.kv-arena-find { display: flex; flex-direction: column; align-items: center; gap: 8px; margin: 0 0 18px; }
.kv-arena-find-btn { width: auto; min-width: 200px; padding: 12px 28px; }
.kv-arena-find-btn.is-searching { filter: saturate(0.6); }
.kv-arena-find-status { margin: 0; min-height: 1.3em; text-align: center; color: var(--text-muted); font-size: 0.86rem; }
.kv-arena-lede { margin: 0 0 14px; text-align: center; color: var(--text-muted); font-size: 0.88rem; }
.kv-arena-list { display: flex; flex-direction: column; gap: 10px; }
.kv-arena-empty {
    margin: 0; padding: 34px 18px; text-align: center;
    color: var(--text-muted); font-size: 0.9rem;
    border: 1px dashed var(--border-strong, rgba(15, 23, 42, 0.18));
    border-radius: 14px;
}

/* One waiting player per row: who / what they are playing / how to join. */
.kv-arena-card {
    display: flex; align-items: center; gap: 14px;
    width: 100%; padding: 12px 16px;
    border: 1px solid var(--border, rgba(15, 23, 42, 0.12));
    border-radius: 14px;
    background: var(--surface, #FFFFFF);
    color: var(--text);
    font: inherit; text-align: left; cursor: pointer;
    transition: border-color 0.18s ease, transform 0.18s ease, box-shadow 0.18s ease;
}
.kv-arena-card:hover {
    border-color: var(--accent-blue, #2563EB);
    transform: translateY(-2px);
    box-shadow: 0 10px 26px rgba(37, 99, 235, 0.14);
}
.kv-arena-card:focus-visible { outline: 2px solid var(--accent-blue, #2563EB); outline-offset: 2px; }
/* Fixed so the quiz titles line up in a column rather than stepping in and
   out with the length of each name. */
.kv-arena-who   { display: flex; align-items: center; gap: 11px; min-width: 0; flex: 0 0 190px; }
.kv-arena-avatar {
    width: 42px; height: 42px; border-radius: 50%; flex-shrink: 0;
    object-fit: cover; background: var(--surface-2, rgba(37, 99, 235, 0.12));
}
.kv-arena-avatar--letter {
    display: flex; align-items: center; justify-content: center;
    color: var(--accent-blue, #2563EB); font-weight: 800; font-size: 1.05rem;
}
.kv-arena-names   { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.kv-arena-name    { font-weight: 700; font-size: 0.95rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.kv-arena-waiting { display: inline-flex; align-items: center; gap: 5px; color: #16A34A; font-size: 0.74rem; font-weight: 700; }
.kv-arena-dot     { width: 6px; height: 6px; border-radius: 50%; background: #16A34A; animation: kvArenaPulse 1.6s ease-in-out infinite; }
@keyframes kvArenaPulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }

/* The quiz is the thing a player is actually choosing between, so it takes
   the flexible middle and truncates rather than wrapping the row. */
.kv-arena-what { display: flex; flex-direction: column; gap: 2px; flex: 1; min-width: 0; }
.kv-arena-quiz { font-weight: 700; font-size: 0.92rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.kv-arena-fmt  { color: var(--text-muted); font-size: 0.78rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.kv-arena-go   { display: flex; align-items: center; gap: 12px; flex: 0 0 auto; }
.kv-arena-expiry { color: var(--text-dim, var(--text-muted)); font-size: 0.74rem; white-space: nowrap; }
.kv-arena-join {
    padding: 8px 18px; border-radius: 999px;
    background: var(--accent-blue, #2563EB); color: #FFFFFF;
    font-size: 0.82rem; font-weight: 800; white-space: nowrap;
}
@media (max-width: 620px) {
    .kv-arena-card { flex-wrap: wrap; gap: 10px; }
    .kv-arena-what { flex-basis: 100%; order: 3; }
    .kv-arena-go   { margin-left: auto; }
}
@media (prefers-reduced-motion: reduce) {
    .kv-arena-card:hover { transform: none; }
    .kv-arena-dot { animation: none; }
}

/* 🚨 `el.hidden` works through the UA rule `[hidden] { display: none }`,
   which ANY author `display` outranks. Without this rule the grid below keeps
   its `display: grid` while hidden, and the Public Arena renders UNDERNEATH
   the format list instead of replacing it. Same trap as setting
   `style.display = 'block'` to reveal something. */
.kv-fmt-grid[hidden],
.kv-arena[hidden],
.kv-fmt-note[hidden] { display: none !important; }

.kv-fmt-grid {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: clamp(12px, 1.6vw, 18px);
}
@media (max-width: 1000px) { .kv-fmt-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 560px)  { .kv-fmt-grid { grid-template-columns: 1fr; } }
/* Placeholder cards. Deliberately skeletal — an empty shape reads as "not
   built yet", where invented format names would read as a catalogue. */
.kv-fmt-card {
    border-radius: 16px;
    overflow: hidden;
    background: var(--bg-card, #FFFFFF);
    border: 1px solid var(--border);
}
.kv-fmt-card-art {
    aspect-ratio: 16 / 10;
    background: linear-gradient(135deg, rgba(37,99,235,.14), rgba(139,92,246,.14));
}
.kv-fmt-card-body { padding: 13px 14px 15px; }
.kv-fmt-card-title,
.kv-fmt-card-line {
    height: 10px; border-radius: 999px;
    background: var(--border-strong, rgba(15, 23, 42, 0.10));
    margin-bottom: 8px;
}
.kv-fmt-card-title { height: 14px; width: 62%; margin-bottom: 11px; }
.kv-fmt-card-line--short { width: 74%; }
.kv-fmt-card-pills { display: flex; gap: 6px; margin-top: 12px; }
.kv-fmt-card-pills span {
    height: 20px; width: 54px; border-radius: 999px;
    background: var(--border-strong, rgba(15, 23, 42, 0.10));
}
/* A REAL format card — the skeleton rules above still describe the loading
   state, and these take over once the formats arrive. */
.kv-fmt-card--real {
    display: flex;
    flex-direction: column;
    padding: 0;
    text-align: left;
    font-family: inherit;
    color: var(--text);
    cursor: pointer;
    transition: transform 0.14s ease, border-color 0.14s ease, box-shadow 0.14s ease;
}
.kv-fmt-card--real:hover {
    transform: translateY(-3px);
    border-color: var(--accent-blue, #2563EB);
    box-shadow: var(--shadow-md);
}
.kv-fmt-card--real:focus-visible { outline: 2px solid var(--accent-blue, #2563EB); outline-offset: 2px; }
.kv-fmt-card--real .kv-fmt-card-art {
    position: relative;
    display: flex; align-items: center; justify-content: center;
    overflow: hidden;
}
.kv-fmt-card--real .kv-fmt-card-art img { width: 100%; height: 100%; object-fit: cover; display: block; }
.kv-fmt-card-emoji { font-size: 2.6rem; line-height: 1; }
/* A beta format is one a normal player cannot see at all — when it IS shown,
   say so, or it looks like any other choice. */
.kv-fmt-beta {
    position: absolute; top: 8px; right: 8px;
    padding: 3px 8px; border-radius: 999px;
    background: rgba(234, 88, 12, 0.92); color: #FFFFFF;
    font-size: 0.62rem; font-weight: 800; letter-spacing: 0.6px;
}
/* The body is DARK regardless of the page theme, as in the mockup — the
   artwork above it is dark, and a light strip under it split every card in
   two. Fixed colours rather than tokens for exactly that reason: this panel
   must not follow the theme while the art above it cannot. */
.kv-fmt-card--real .kv-fmt-card-body {
    display: flex; flex-direction: column; gap: 6px;
    flex: 1;                       /* equal-height bodies across the row */
    padding: 13px 14px 14px;
    background: #0F1A2E;
    color: #E8EEF8;
    /* Rounded at the TOP only, and lifted so it overlaps the artwork — the
       panel reads as sitting ON the image, the way the mockup has it. The
       bottom corners are left square because the card's own overflow:hidden
       already rounds them to its radius; rounding both ends would show a
       sliver of card behind the corners. */
    border-radius: 14px 14px 0 0;
    margin-top: -14px;
    position: relative;            /* above the art it overlaps */
    z-index: 1;
}
.kv-fmt-card-name { font-weight: 800; font-size: 0.95rem; color: #FFFFFF; }
.kv-fmt-card-desc {
    color: #9FB3C8; font-size: 0.8rem; line-height: 1.4;
    display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
/* Chips sit at the FOOT of the body, so they line up across a row whatever
   the description length. */
.kv-fmt-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: auto; padding-top: 8px; }
.kv-fmt-chip {
    display: inline-flex; align-items: center; gap: 5px;
    padding: 4px 9px;
    border-radius: 999px;
    background: rgba(255, 255, 255, 0.08);
    border: 1px solid rgba(255, 255, 255, 0.10);
    color: #C7D6EA;
    font-size: 0.62rem; font-weight: 800; letter-spacing: 0.6px;
    white-space: nowrap;
}
/* The card is a flex column so the body can stretch; the art keeps its ratio. */
.kv-fmt-card--real { overflow: hidden; }
.kv-fmt-card--real .kv-fmt-card-art { flex: 0 0 auto; aspect-ratio: 16 / 10; }

.kv-fmt-note {
    margin-top: clamp(18px, 3vh, 28px);
    padding: 18px;
    border-radius: 14px;
    border: 1px dashed var(--border-strong, rgba(15, 23, 42, 0.18));
    text-align: center;
    color: var(--text-muted);
}
.kv-fmt-note p { margin: 0 0 12px; }
.kv-fmt-note-actions { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }
/* .btn-primary is full-width by default (it is a form button elsewhere); in a
   row of two it has to size to its text or it wraps onto its own line. */
.kv-fmt-note-actions .btn-primary,
.kv-fmt-note-actions .btn-secondary { width: auto; padding: 10px 20px; }

/* ===================================================================
   QUIZ SELECTION (/play/content) — the structure from
   apps/knovli/design/playContentSelection.png.
   =================================================================== */
.kv-content { width: 100%; }

/* The wizard header. */
.kv-steps {
    display: flex; flex-wrap: wrap; gap: 8px;
    list-style: none; margin: 0 0 clamp(16px, 3vh, 26px); padding: 0;
}
.kv-step {
    display: inline-flex; align-items: center; gap: 7px;
    padding: 9px 16px; border-radius: 999px;
    background: var(--surface-2, rgba(15, 23, 42, 0.05));
    color: var(--text-muted);
    font-size: 0.82rem; font-weight: 700;
}
.kv-step.is-current { background: var(--accent-blue, #2563EB); color: #FFFFFF; }
/* The way back to the hub. A DIFFERENT colour from the blue "you are here"
   chip, because it is not a step in the trail — it leaves it.
   🚨 A SOLID fill with white text, not the self-tinted
   `background: rgba(X,0.1); color: X` idiom used elsewhere in Knovli: that
   pattern moves the ground with the text and fails WCAG AA on one theme or
   the other for every colour measured. Teal-700 on white is ~5.5:1 and is
   the same on both themes because neither value is a token. */
.kv-step--home { background: #0F766E; color: #FFFFFF; }
.kv-step--home .kv-step-icon { opacity: 1; }
.kv-step--home:hover { background: #115E59; }
.kv-step--home .kv-step-btn:focus-visible { outline-color: #0F766E; }
.kv-step.is-done    { color: var(--text); }
.kv-step.is-done .kv-step-icon { opacity: 0.7; }
@media (max-width: 620px) { .kv-step-text { font-size: 0.74rem; } .kv-step { padding: 7px 11px; } }

/* A step already walked is a button back to it. It has to look identical to
   a plain chip — the affordance is the cursor and the hover, not a different
   shape, or the trail reads as a row of buttons. */
.kv-step-btn {
    appearance: none; border: none; background: none; padding: 0; margin: 0;
    font: inherit; color: inherit; cursor: pointer;
    display: inline-flex; align-items: center; gap: 7px;
}
.kv-step.is-done:hover { background: var(--surface-3, rgba(15, 23, 42, 0.09)); }
.kv-step:has(.kv-step-btn) { padding: 0; }
.kv-step:has(.kv-step-btn) .kv-step-btn { padding: 9px 16px; border-radius: 999px; }
.kv-step-btn:focus-visible { outline: 2px solid var(--accent-blue, #2563EB); outline-offset: 2px; }
/* The chip names the CHOICE once one is made, so it can run long. */
.kv-step-text { max-width: 22ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ── Step 2 shelves: Featured / Most popular ─────────────────────────── */
/* A rule under each shelf, so Featured, Most popular and the full library
   below them read as three sections rather than one long scroll of cards.
   ⚠️ --border IS defined (unlike --surface / --surface-3), so this follows the
   theme instead of pinning one colour. */
.kv-row {
    margin: 0 0 26px;
    padding-bottom: 22px;
    border-bottom: 1px solid var(--border, rgba(15, 23, 42, 0.12));
}
.kv-row-head {
    display: flex; align-items: flex-end; justify-content: space-between;
    gap: 12px; flex-wrap: wrap; margin: 0 0 12px;
}
.kv-row-titles h3 { margin: 0; font-size: 1.05rem; font-weight: 800; color: var(--text); }
.kv-row-titles p  { margin: 3px 0 0; font-size: 0.82rem; color: var(--text-muted); }
/* Only rendered when the row actually overflows, so it never promises more
   than the six cards already on screen. */
.kv-row-all {
    appearance: none; flex: 0 0 auto;
    padding: 8px 15px; border-radius: 999px;
    border: 1px solid var(--border, rgba(15, 23, 42, 0.18));
    background: transparent; color: var(--text);
    font: inherit; font-weight: 700; font-size: 0.82rem; cursor: pointer;
}
.kv-row-all span { opacity: 0.6; margin-left: 4px; font-weight: 800; }
.kv-row-all:hover { background: rgba(127, 127, 127, 0.12); }
.kv-row-all:focus-visible { outline: 2px solid var(--accent-blue, #2563EB); outline-offset: 2px; }

/* A shelf is ONE row that scrolls sideways — never a block that wraps. The
   full wrapping grid is what "View all" opens. */
.kv-row-grid {
    display: flex;
    flex-wrap: nowrap;
    overflow-x: auto;
    gap: clamp(12px, 1.6vw, 18px);
    /* The cards lift 3px on hover and draw a focus ring; overflow-x:auto
       computes overflow-y to auto as well, so without this padding both get
       clipped. The negative margin keeps the row flush with the text above. */
    padding: 4px 4px 12px;
    margin: 0 -4px;
    scroll-snap-type: x proximity;
    overscroll-behavior-x: contain;
    scrollbar-width: thin;
}
/* 🚨 Flex children SHRINK by default, so without this the six cards squeeze
   themselves into the visible width and the row never scrolls — it just
   renders six slivers. Same trap as the subject-chip row. */
.kv-row-grid > * {
    flex: 0 0 clamp(208px, 23vw, 252px);
    scroll-snap-align: start;
}
@media (max-width: 520px) { .kv-row-grid > * { flex-basis: 78vw; } }

/* 🚨 The back control is a child of .kv-quiz-grid, which is a GRID — without
   this it becomes one cell and sits beside a quiz card. */
.kv-row-back-wrap {
    grid-column: 1 / -1;
    display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
    margin: 0 0 4px;
}
.kv-row-back {
    appearance: none; padding: 8px 15px; border-radius: 999px;
    border: 1px solid var(--border, rgba(15, 23, 42, 0.18));
    background: transparent; color: var(--text);
    font: inherit; font-weight: 700; font-size: 0.82rem; cursor: pointer;
}
.kv-row-back:hover { background: rgba(127, 127, 127, 0.12); }
.kv-row-back:focus-visible { outline: 2px solid var(--accent-blue, #2563EB); outline-offset: 2px; }
.kv-row-back-label { color: var(--text-muted); font-size: 0.82rem; font-weight: 700; }

/* ── Step 1 (Group Play only): join a game, or host one ────────────── */
/* Two equal choices, side by side where there is room and stacked where there
   is not. Joining is FIRST because a joiner has nothing else to do here. */
.kv-gp-entry {
    display: grid; gap: clamp(14px, 2vw, 20px);
    grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
    margin: 0 0 clamp(18px, 3vh, 26px);
    align-items: start;
}
.kv-gp-entry-card {
    display: flex; flex-direction: column; gap: 10px;
    padding: 20px 18px 22px;
    border: 1px solid var(--border, rgba(15, 23, 42, 0.12)); border-radius: 16px;
    background: var(--surface-1, #FFFFFF);
}
.kv-gp-entry-card h3 { margin: 0; font-size: 1.05rem; font-weight: 800; color: var(--text); }
.kv-gp-entry-card p {
    margin: 0 0 4px; font-size: 0.85rem; line-height: 1.5; color: var(--text-muted);
    /* Push the button to the bottom so both cards' buttons line up however
       long the wording is. */
    flex: 1 0 auto;
}
.kv-gp-join { display: flex; gap: 10px; flex-wrap: wrap; }
.kv-gp-join input {
    flex: 1 1 140px; min-width: 0;
    padding: 14px 16px; border-radius: 13px;
    border: 2px solid var(--border, rgba(15, 23, 42, 0.16));
    background: var(--surface-2, #F8FAFC); color: var(--text);
    font: inherit; font-size: 1.15rem; font-weight: 800;
    letter-spacing: 0.16em; text-transform: uppercase; text-align: center;
}
.kv-gp-join input::placeholder { letter-spacing: 0.16em; opacity: 0.45; font-weight: 700; }
.kv-gp-join input:focus-visible {
    outline: 2px solid var(--accent-blue, #2563EB); outline-offset: 2px;
    border-color: var(--accent-blue, #2563EB);
}
/* The Join button sits BESIDE the code, so it must not take the full width
   .kv-gp-start gives it everywhere else. */
.kv-gp-join .kv-gp-start { width: auto; flex: 0 0 auto; padding-inline: 26px; }

/* "You have a lobby open" — shown only when there actually is one. */
.kv-gp-openbar {
    display: flex; flex-wrap: wrap; gap: 12px; align-items: center;
    justify-content: space-between;
    padding: 14px 16px; border-radius: 14px;
    border: 1px solid var(--border, rgba(15, 23, 42, 0.12));
    background: var(--surface-2, #F8FAFC);
    font-size: 0.88rem; color: var(--text);
}
.kv-gp-openbar-acts { display: flex; gap: 8px; flex-wrap: wrap; }
.kv-gp-openbar-go, .kv-gp-openbar-x {
    appearance: none; padding: 9px 16px; border-radius: 10px;
    font: inherit; font-weight: 700; font-size: 0.85rem; cursor: pointer;
}
.kv-gp-openbar-go { border: none; background: var(--accent-blue, #2563EB); color: #FFFFFF; }
.kv-gp-openbar-x {
    border: 1px solid var(--border, rgba(15, 23, 42, 0.18));
    background: transparent; color: var(--text-muted);
}
.kv-gp-openbar-go:hover, .kv-gp-openbar-x:hover { filter: brightness(1.08); }
.kv-gp-openbar-go:focus-visible, .kv-gp-openbar-x:focus-visible {
    outline: 2px solid var(--accent-blue, #2563EB); outline-offset: 2px;
}

/* A label that only a screen reader reads. The join input is captioned by its
   placeholder for everyone else, which is not a label. */
.kv-sr-only {
    position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
    overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* ── Step 3b: Group Play setup — all three panels on one page ────────── */
.kv-gp-panels {
    display: grid; gap: clamp(14px, 2vw, 20px);
    grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
    margin: 0 0 clamp(18px, 3vh, 26px);
    align-items: start;
}
.kv-gp-panel {
    padding: 18px 18px 20px;
    border: 1px solid var(--border, rgba(15, 23, 42, 0.12)); border-radius: 16px;
    background: var(--surface-1, #FFFFFF);
}
.kv-gp-panel h3 { margin: 0 0 4px; font-size: 1rem; font-weight: 800; color: var(--text); }
.kv-gp-panel-sub { margin: 0 0 14px; font-size: 0.8rem; color: var(--text-muted); line-height: 1.45; }
.kv-gp-choices { display: flex; flex-direction: column; gap: 10px; }
.kv-gp-choice {
    display: flex; align-items: flex-start; gap: 12px; width: 100%;
    padding: 13px 14px; border-radius: 12px; cursor: pointer; text-align: left;
    border: 2px solid var(--border, rgba(15, 23, 42, 0.14));
    background: transparent; color: var(--text); font: inherit;
}
.kv-gp-choice:hover { background: rgba(127, 127, 127, 0.08); }
.kv-gp-choice.is-on { border-color: var(--accent-blue, #2563EB); background: rgba(37, 99, 235, 0.10); }
.kv-gp-choice:focus-visible { outline: 2px solid var(--accent-blue, #2563EB); outline-offset: 2px; }
.kv-gp-choice-icon { font-size: 1.35rem; line-height: 1.1; flex: 0 0 auto; }
.kv-gp-choice-text { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.kv-gp-choice-text b    { font-size: 0.9rem; font-weight: 800; }
.kv-gp-choice-text span { font-size: 0.78rem; color: var(--text-muted); line-height: 1.4; }

.kv-gp-toggle {
    display: flex; align-items: center; gap: 10px; width: 100%; margin-top: 14px;
    padding: 11px 12px; border-radius: 12px; cursor: pointer; text-align: left;
    border: 2px solid var(--border, rgba(15, 23, 42, 0.14));
    background: transparent; color: var(--text); font: inherit; font-size: 0.84rem;
}
.kv-gp-toggle.is-on { border-color: var(--accent-blue, #2563EB); background: rgba(37, 99, 235, 0.10); }
.kv-gp-toggle:focus-visible { outline: 2px solid var(--accent-blue, #2563EB); outline-offset: 2px; }
.kv-gp-toggle i { font-style: normal; color: var(--text-muted); }
.kv-gp-toggle-box {
    flex: 0 0 auto; width: 20px; height: 20px; border-radius: 6px;
    border: 2px solid var(--border, rgba(15, 23, 42, 0.3));
    display: flex; align-items: center; justify-content: center;
    font-size: 0.8rem; font-weight: 900; color: #FFFFFF;
}
.kv-gp-toggle.is-on .kv-gp-toggle-box { background: var(--accent-blue, #2563EB); border-color: var(--accent-blue, #2563EB); }

.kv-gp-groups { margin-top: 14px; }
.kv-gp-group-list { list-style: none; margin: 0 0 10px; padding: 0; display: flex; flex-direction: column; gap: 6px; }
.kv-gp-group {
    display: flex; align-items: center; gap: 8px;
    padding: 8px 10px; border-radius: 10px;
    background: var(--surface-2, rgba(15, 23, 42, 0.05));
    font-size: 0.83rem; color: var(--text);
}
.kv-gp-group--empty { color: var(--text-muted); font-style: italic; }
.kv-gp-group-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 700; }
.kv-gp-group-max  { color: var(--text-muted); font-size: 0.76rem; }
.kv-gp-group-x {
    appearance: none; border: none; background: none; cursor: pointer;
    color: var(--text-muted); font-size: 1.1rem; line-height: 1; padding: 2px 4px;
}
.kv-gp-group-x:hover { color: #DC2626; }
.kv-gp-group-add { display: flex; gap: 6px; }
.kv-gp-group-add input {
    padding: 9px 10px; border-radius: 9px; font: inherit; font-size: 0.83rem;
    border: 1px solid var(--border, rgba(15, 23, 42, 0.2));
    background: var(--bg-input, #FFFFFF); color: var(--text); min-width: 0;
}
.kv-gp-group-add input[type="text"]   { flex: 1; }
.kv-gp-group-add input[type="number"] { width: 72px; flex: 0 0 auto; }
.kv-gp-group-add-btn {
    appearance: none; flex: 0 0 auto; padding: 9px 15px; border-radius: 9px;
    border: 1px solid var(--accent-blue, #2563EB);
    background: transparent; color: var(--accent-blue, #2563EB);
    font: inherit; font-weight: 700; font-size: 0.83rem; cursor: pointer;
}

.kv-gp-sliders { display: flex; flex-direction: column; gap: 16px; }
.kv-gp-slider label {
    display: flex; justify-content: space-between; align-items: baseline; gap: 10px;
    font-size: 0.84rem; font-weight: 700; color: var(--text); margin-bottom: 6px;
}
.kv-gp-slider output { font-variant-numeric: tabular-nums; color: var(--accent-blue, #2563EB); font-weight: 800; }
.kv-gp-slider input[type="range"] { width: 100%; accent-color: var(--accent-blue, #2563EB); }
/* "Use every question in the deck" stands the regular-round slider down. */
.kv-gp-slider.is-off { opacity: .45; }
.kv-gp-slider-help { display: block; margin-top: 4px; font-size: 0.74rem; color: var(--text-muted); line-height: 1.4; }

.kv-gp-note {
    margin: 0 0 14px; padding: 10px 12px; border-radius: 10px;
    background: rgba(234, 88, 12, 0.14); color: #7C2D12;
    font-size: 0.85rem; font-weight: 700;
}
:root[data-theme="dark"] .kv-gp-note { color: #FDBA74; }
.kv-gp-note[hidden] { display: none; }
.kv-gp-note-btn {
    appearance: none; margin-left: 8px; padding: 6px 13px; border-radius: 999px;
    border: 1px solid currentColor; background: transparent; color: inherit;
    font: inherit; font-weight: 800; font-size: 0.78rem; cursor: pointer;
}
.kv-gp-note-btn:disabled { opacity: 0.6; cursor: default; }
.kv-gp-start:disabled { opacity: 0.5; cursor: not-allowed; }
.kv-gp-start {
    appearance: none; width: 100%; padding: 15px 20px; border: none; border-radius: 13px;
    background: var(--accent-blue, #2563EB); color: #FFFFFF;
    font: inherit; font-weight: 800; font-size: 1rem; cursor: pointer;
}
.kv-gp-start:hover { filter: brightness(1.08); }
.kv-gp-start:focus-visible { outline: 2px solid var(--accent-blue, #2563EB); outline-offset: 3px; }

/* ── Waiting for an opponent (/play/waiting) ─────────────────────────── */
.kv-wait {
    max-width: 520px; margin: 0 auto; padding: 34px 24px 30px;
    border: 1px solid var(--border, rgba(15, 23, 42, 0.12)); border-radius: 18px;
    /* 🚨 --surface-1, NOT --surface: the latter is defined nowhere in this
       stylesheet, so `var(--surface, #FFFFFF)` silently pinned this card white
       in dark mode. A var() fallback fails silently and reads as correct. */
    background: var(--surface-1, #FFFFFF); text-align: center;
}
.kv-wait-spinner {
    width: 46px; height: 46px; margin: 0 auto 18px;
    border: 4px solid var(--border, rgba(15, 23, 42, 0.12));
    border-top-color: var(--accent-blue, #2563EB);
    border-radius: 50%; animation: kv-wait-spin 0.9s linear infinite;
}
@keyframes kv-wait-spin { to { transform: rotate(360deg); } }
/* Respect a reader who has asked for less motion — this spins indefinitely. */
@media (prefers-reduced-motion: reduce) {
    .kv-wait-spinner { animation: none; border-top-color: var(--accent-blue, #2563EB); }
}
.kv-wait-title { margin: 0 0 8px; font-size: 1.3rem; font-weight: 800; color: var(--text); }
.kv-wait-sub   { margin: 0 0 20px; color: var(--text-muted); font-size: 0.92rem; line-height: 1.5; }
.kv-wait-facts { display: flex; flex-direction: column; gap: 1px; margin: 0 0 18px;
    border-radius: 12px; overflow: hidden; background: var(--border, rgba(15, 23, 42, 0.12)); }
.kv-wait-fact  { display: flex; justify-content: space-between; gap: 14px;
    padding: 11px 14px; background: var(--surface-2, rgba(15, 23, 42, 0.04)); text-align: left; }
.kv-wait-fact dt { margin: 0; color: var(--text-muted); font-size: 0.82rem; font-weight: 700; }
.kv-wait-fact dd { margin: 0; color: var(--text); font-size: 0.86rem; font-weight: 700;
    min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.kv-wait-hint  { margin: 0 0 16px; color: var(--text-muted); font-size: 0.82rem; line-height: 1.5; }
/* 🚨 A PER-THEME PAIR, not one self-tinted colour. Knovli switches themes by
   stamping data-theme on <html> (light is the default and stamps nothing), so
   the bare rule is light and one override handles dark. Writing this as
   `background: rgba(X,0.1); color: X` — the idiom used elsewhere in the app —
   moves the ground with the text and fails WCAG AA on one of the two. */
.kv-wait-note  { margin: 0 0 16px; padding: 10px 12px; border-radius: 10px;
    background: rgba(234, 88, 12, 0.14); color: #7C2D12; font-size: 0.85rem; font-weight: 700; }
:root[data-theme="dark"] .kv-wait-note { color: #FDBA74; }
.kv-wait-note[hidden] { display: none; }
.kv-wait-cancel {
    appearance: none; width: 100%; padding: 13px 18px;
    border: 1px solid var(--border, rgba(15, 23, 42, 0.18)); border-radius: 12px;
    background: transparent; color: var(--text); font: inherit; font-weight: 700; cursor: pointer;
}
.kv-wait-cancel:hover:not(:disabled) { background: rgba(127, 127, 127, 0.12); }
.kv-wait-cancel:disabled { opacity: 0.55; cursor: default; }

/* The hub's reminder that a challenge is still out there. Leaving the waiting
   screen does not cancel it, so without this the wait has no way back. */
.kv-wait-banner {
    display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
    margin: 0 0 20px; padding: 12px 16px;
    border: 1px solid rgba(234, 88, 12, 0.45); border-radius: 12px;
    background: rgba(234, 88, 12, 0.14); color: #FFEDD5;
}
.kv-wait-banner-dot {
    width: 9px; height: 9px; flex: 0 0 auto; border-radius: 50%;
    background: #FB923C; animation: kv-wait-pulse 1.6s ease-in-out infinite;
}
@keyframes kv-wait-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }
@media (prefers-reduced-motion: reduce) { .kv-wait-banner-dot { animation: none; } }
.kv-wait-banner-text { flex: 1; min-width: 180px; font-size: 0.88rem; font-weight: 700; }
/* ⚠️ Orange-600 on white measures 3.56:1 — under the 4.5:1 AA needs for text
   this size. A PALE fill with near-black text gets there and reads better on
   the hub's dark surface anyway. */
.kv-wait-banner-btn {
    appearance: none; padding: 8px 16px; border: none; border-radius: 999px;
    background: #FDBA74; color: #431407; font: inherit; font-weight: 800;
    font-size: 0.82rem; cursor: pointer;
}
.kv-wait-banner-btn:hover { background: #FB923C; }

/* ── Step 3a: choosing an opponent ──────────────────────────────────── */
.kv-opp-random {
    display: flex; align-items: center; gap: 16px; width: 100%;
    padding: 18px 20px; margin-bottom: 18px;
    border: 2px solid var(--accent-blue, #2563EB); border-radius: 16px;
    background: var(--surface, #FFFFFF); color: var(--text);
    font: inherit; text-align: left; cursor: pointer;
    transition: transform 0.18s ease, box-shadow 0.18s ease;
}
.kv-opp-random:hover { transform: translateY(-2px); box-shadow: 0 12px 30px rgba(37, 99, 235, 0.18); }
.kv-opp-random:focus-visible { outline: 2px solid var(--accent-blue, #2563EB); outline-offset: 2px; }
.kv-opp-random-icon  { font-size: 2rem; flex: 0 0 auto; }
.kv-opp-random-text  { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.kv-opp-random-title { font-weight: 800; font-size: 1rem; }
.kv-opp-random-sub   { color: var(--text-muted); font-size: 0.83rem; line-height: 1.45; }

.kv-opp-or {
    display: flex; align-items: center; gap: 12px;
    margin: 0 0 16px; color: var(--text-dim, var(--text-muted)); font-size: 0.78rem;
}
.kv-opp-or::before, .kv-opp-or::after {
    content: ''; flex: 1; height: 1px; background: var(--border, rgba(15, 23, 42, 0.12));
}

.kv-opp-grid {
    display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
    gap: 10px;
}
.kv-opp-card {
    display: flex; align-items: center; gap: 11px;
    padding: 11px 14px;
    border: 1px solid var(--border, rgba(15, 23, 42, 0.12)); border-radius: 14px;
    background: var(--surface, #FFFFFF); color: var(--text);
    font: inherit; text-align: left; cursor: pointer;
    transition: border-color 0.18s ease, transform 0.18s ease;
}
.kv-opp-card:hover { border-color: var(--accent-blue, #2563EB); transform: translateY(-2px); }
.kv-opp-card:focus-visible { outline: 2px solid var(--accent-blue, #2563EB); outline-offset: 2px; }
.kv-opp-avatar {
    width: 38px; height: 38px; border-radius: 50%; flex-shrink: 0; object-fit: cover;
    background: var(--surface-2, rgba(37, 99, 235, 0.12));
}
.kv-opp-avatar--letter {
    display: flex; align-items: center; justify-content: center;
    color: var(--accent-blue, #2563EB); font-weight: 800;
}
.kv-opp-name  { flex: 1; min-width: 0; font-weight: 700; font-size: 0.9rem;
                overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.kv-opp-state { flex: 0 0 auto; font-size: 0.7rem; font-weight: 700; color: var(--text-dim, var(--text-muted)); }
.kv-opp-state.is-online { color: #16A34A; }
.kv-opp-skel  { height: 62px; border-style: dashed; opacity: 0.5; cursor: default; }

/* ── Step 3b: group play setup ──────────────────────────────────────── */
.kv-setup-card {
    max-width: 560px; margin: 0 auto; padding: 26px 24px;
    border: 1px solid var(--border, rgba(15, 23, 42, 0.12)); border-radius: 16px;
    background: var(--surface, #FFFFFF); text-align: center;
}
.kv-setup-recap { margin: 0 0 18px; font-size: 0.95rem; }
.kv-setup-go    { min-width: 220px; }
.kv-setup-note  { margin: 16px 0 0; color: var(--text-muted); font-size: 0.82rem; line-height: 1.5; }

@media (prefers-reduced-motion: reduce) {
    .kv-opp-random:hover, .kv-opp-card:hover { transform: none; }
}

/* Heading row — same balanced layout as the format screen. */
.kv-content-head { display: flex; align-items: center; gap: 12px; margin-bottom: clamp(12px, 2vh, 18px); }
.kv-content-head-titles { flex: 1; min-width: 0; text-align: center; }
.kv-content-head-left,
.kv-content-head-spacer { flex: 0 0 40px; }
@media (max-width: 620px) {
    .kv-content-head-spacer { display: none; }
    .kv-content-head-titles { text-align: left; }
}
.kv-content-head h2 { margin: 0 0 4px; font-size: clamp(1.2rem, 2.6vw, 1.9rem); font-weight: 800; color: var(--text); }
.kv-content-head p  { margin: 0; color: var(--text-muted); font-size: 0.9rem; }

/* Where the quizzes come from — the same three buckets the game's own picker
   offers, so a quiz is found in the same place in both. */
.kv-content-sources { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 14px; }
.kv-src {
    display: inline-flex; align-items: center; gap: 7px;
    padding: 8px 14px; border-radius: 999px;
    border: 1px solid var(--border, rgba(15, 23, 42, 0.14));
    background: var(--surface, #FFFFFF); color: var(--text-muted);
    font: inherit; font-size: 0.82rem; font-weight: 700; cursor: pointer;
    transition: border-color 0.18s ease, color 0.18s ease, background 0.18s ease;
}
.kv-src:hover { border-color: var(--accent-blue, #2563EB); color: var(--text); }
.kv-src.is-active {
    background: var(--accent-blue, #2563EB); border-color: var(--accent-blue, #2563EB); color: #FFFFFF;
}
.kv-src:focus-visible { outline: 2px solid var(--accent-blue, #2563EB); outline-offset: 2px; }
.kv-src-mark { width: 16px; height: 16px; object-fit: contain; display: inline-block; }
/* The count is the useful half of the chip — it says whether the tab is worth
   opening before you open it. */
.kv-src-n {
    min-width: 1.5em; padding: 1px 6px; border-radius: 999px;
    background: var(--surface-2, rgba(15, 23, 42, 0.08));
    font-size: 0.72rem; font-weight: 800; text-align: center;
}
.kv-src.is-active .kv-src-n { background: rgba(255, 255, 255, 0.24); }
@media (max-width: 620px) { .kv-src { padding: 7px 11px; font-size: 0.76rem; } }

.kv-content-search { margin-bottom: 14px; }
.kv-content-search input {
    width: 100%; box-sizing: border-box;
    padding: 14px 18px;
    border-radius: 999px;
    border: 1px solid var(--border);
    background: var(--bg-card, #FFFFFF);
    color: var(--text);
    font-family: inherit; font-size: 0.95rem;
}
.kv-content-search input:focus-visible { outline: 2px solid var(--accent-blue, #2563EB); outline-offset: 2px; }

/* Subject chips — built from what is actually present, so none is a dead end. */
.kv-content-cats { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: clamp(14px, 2.4vh, 22px); }
.kv-cat {
    padding: 8px 15px; border-radius: 999px;
    border: 1px solid var(--border);
    background: var(--bg-card, #FFFFFF);
    color: var(--text-muted);
    font-family: inherit; font-size: 0.8rem; font-weight: 700;
    cursor: pointer;
    transition: background 0.14s ease, color 0.14s ease, border-color 0.14s ease;
}
.kv-cat:hover { border-color: var(--accent-blue, #2563EB); color: var(--text); }
.kv-cat.is-active { background: var(--accent-blue, #2563EB); border-color: var(--accent-blue, #2563EB); color: #FFFFFF; }
.kv-cat:focus-visible { outline: 2px solid var(--accent-blue, #2563EB); outline-offset: 2px; }

/* Subject and topic chips carry their SUBJECT's colour, the same way the
   flashcard library's do, so the two surfaces read as one taxonomy. The hue
   comes in per-chip as --kv-subj-hue ("59,130,246"). */
.kv-subj, .kv-topic {
    display: inline-flex; align-items: center; gap: 6px;
    white-space: nowrap;
    color: var(--text);
    background: rgba(var(--kv-subj-hue), 0.16);
    border-color: rgba(var(--kv-subj-hue), 0.45);
}
.kv-topic { padding: 6px 12px; font-size: 0.76rem; background: rgba(var(--kv-subj-hue), 0.10); border-color: rgba(var(--kv-subj-hue), 0.32); }
.kv-subj.is-active, .kv-topic.is-active {
    background: rgb(var(--kv-subj-hue));
    border-color: rgb(var(--kv-subj-hue));
    color: #FFFFFF;
}
.kv-cat-n { opacity: 0.7; font-weight: 600; }
.kv-cat-clear {
    background: none; border: 0; padding: 0 6px;
    color: var(--text-muted); font: inherit; font-size: 0.76rem;
    text-decoration: underline; cursor: pointer; white-space: nowrap;
}
/* The topic row sits tight under its subject — they are one control. */
.kv-content-topics { margin-top: -6px; }

/* 🚨 On a narrow screen a wrapping chip row can take half the screen before
   the first card, so both rows become ONE scrolling line instead. Chips must
   not shrink, and the row keeps a little end padding so the last chip does
   not look clipped at the edge. */
@media (max-width: 700px) {
    .kv-content-cats {
        flex-wrap: nowrap;
        overflow-x: auto;
        overflow-y: hidden;
        -webkit-overflow-scrolling: touch;
        scrollbar-width: none;                /* the chips themselves are the affordance */
        padding-bottom: 2px;
        scroll-padding-inline: 12px;
    }
    .kv-content-cats::-webkit-scrollbar { display: none; }
    .kv-content-cats > * { flex: 0 0 auto; }
}

/* The quiz grid. */
/* ── Content-first play (/play/mydecks, /play/explore, /play/classes) ──── */
/* The card is the existing quiz card in a clickable wrapper — a second card
   design for the same content would drift the moment either changed. */
.kv-src-card { cursor: pointer; border-radius: 16px; display: block; }
.kv-src-card > * { pointer-events: none; }      /* the wrapper owns the click */
.kv-src-card:focus-visible { outline: 2px solid var(--accent-blue, #2563EB); outline-offset: 3px; }
.kv-src-card:hover { transform: translateY(-2px); transition: transform .15s ease; }

/* Classes are subdivided by class; the other two sources are one list. */
.kv-src-group-head {
    display: flex; align-items: baseline; justify-content: space-between;
    gap: 12px; margin: clamp(18px, 3vh, 26px) 0 10px;
}
.kv-src-group-head h3 { margin: 0; font-size: clamp(1rem, 2vw, 1.2rem); font-weight: 800; color: var(--text); }
.kv-src-group-head span { font-size: 0.82rem; color: var(--text-muted); font-weight: 700; }

/* ── "How do you want to play?" ── asked after the content, not before. */
.kv-mode-modal {
    position: fixed; inset: 0; z-index: 100000;
    display: flex; align-items: center; justify-content: center;
    padding: clamp(16px, 4vw, 40px);
    background: rgba(7, 11, 24, 0.72);
    animation: kvModeIn .18s ease;
}
@keyframes kvModeIn { from { opacity: 0; } to { opacity: 1; } }
.kv-mode-sheet {
    position: relative;
    width: min(560px, 100%);
    max-height: 100%;
    overflow-y: auto;
    padding: clamp(22px, 3vw, 32px);
    border-radius: 20px;
    background: var(--surface-1, #FFFFFF);
    border: 1px solid var(--border, rgba(15, 23, 42, 0.12));
    box-shadow: 0 24px 60px rgba(0, 0, 0, 0.4);
}
.kv-mode-sheet h2 { margin: 0 0 4px; font-size: clamp(1.15rem, 2.6vw, 1.5rem); font-weight: 800; color: var(--text); }
/* Reporting a quiz — quiet, because it is not the thing anyone came here to
   do. Sits under the mode choices, not beside them. */
.kv-mode-report { display: block; margin: 16px auto 0; padding: 7px 14px;
  background: none; border: 0; cursor: pointer; font: inherit; font-size: 12.5px;
  font-weight: 600; color: var(--text-dim); border-radius: 8px; }
.kv-mode-report:hover { color: #DC2626; background: rgba(220,38,38,0.07); }
.kv-mode-sub {
    margin: 0 0 clamp(16px, 2.4vh, 22px);
    color: var(--text-muted); font-size: 0.9rem;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.kv-mode-x {
    position: absolute; top: 12px; right: 12px;
    width: 34px; height: 34px; border-radius: 50%;
    border: 1px solid var(--border, rgba(15, 23, 42, 0.14));
    background: transparent; color: var(--text-muted);
    font-size: 1.3rem; line-height: 1; cursor: pointer;
}
.kv-mode-x:hover { background: var(--surface-2, rgba(15, 23, 42, 0.06)); }
.kv-mode-x:focus-visible { outline: 2px solid var(--accent-blue, #2563EB); outline-offset: 2px; }

.kv-mode-choices { display: flex; flex-direction: column; gap: 10px; }
.kv-mode-choice {
    display: flex; align-items: center; gap: 14px; width: 100%;
    padding: 15px 16px; border-radius: 14px; cursor: pointer; text-align: left;
    border: 2px solid var(--border, rgba(15, 23, 42, 0.14));
    background: transparent; color: var(--text); font: inherit;
    transition: border-color .15s ease, background-color .15s ease;
}
.kv-mode-choice:hover { border-color: var(--accent-blue, #2563EB); background: var(--surface-2, rgba(37, 99, 235, 0.06)); }
.kv-mode-choice:focus-visible { outline: 2px solid var(--accent-blue, #2563EB); outline-offset: 2px; }
.kv-mode-choice-icon { flex: 0 0 auto; display: grid; place-items: center; }
.kv-mode-choice-text { display: flex; flex-direction: column; min-width: 0; }
.kv-mode-choice-text b { font-size: 1rem; font-weight: 800; }
.kv-mode-choice-text span { font-size: 0.82rem; color: var(--text-muted); }

@media (prefers-reduced-motion: reduce) {
    .kv-mode-modal { animation: none; }
    .kv-src-card:hover { transform: none; }
}

/* ── Challenges ── the hub card and the /play/challenges page ───────── */
.kv-chall-card { margin: 0 0 clamp(14px, 2vh, 20px); }
.kv-chall-card[hidden] { display: none; }

/* Dashboard challenge row — AT MOST TWO, side by side, under the two game
   rails. The cards themselves are unchanged; only the track is new.
   🚨 The row carries its own margin, so the cards inside must not also carry
   the stacked one or the two columns sit on an uneven baseline. */
.kv-play-challrow { margin: clamp(18px, 2.4vh, 26px) 0 0; }
.kv-play-challrow[hidden] { display: none; }

/* ── Leaderboards (ranking spec §19): compact on the dashboard, full on
   /play/leaderboards. Rows reuse the class leaderboard's .cc-lb-* pieces. */
/* Under the mode cards and the SAME width: capped at 1200 and centred like
   .kv-play-modes. That row already leaves clearance below its fanned cards, so
   this needs no top margin, only room before the quick links. */
.kv-play-lb { max-width: 1200px; margin: 0 auto clamp(22px, 3.4vh, 36px); }
.kv-play-lb[hidden] { display: none; }
.kv-play-lb .kv-lb-tabs { margin: 0 auto 12px; }
/* Title centred over the tabs; "View all" keeps the right edge. */
.kv-play-lb .kv-play-section-head { display: grid; grid-template-columns: 1fr auto 1fr; }
.kv-play-lb .kv-play-section-head h2 { grid-column: 2; text-align: center; }
.kv-play-lb .kv-lb-all { grid-column: 3; justify-self: end; }
.kv-lb-all {
    appearance: none; border: none; background: transparent; cursor: pointer;
    font: inherit; font-size: 0.85rem; font-weight: 700; color: var(--accent-cyan, #2FB2EA);
    padding: 6px 2px;
}
.kv-lb-all:hover { text-decoration: underline; }
.kv-lb-all:focus-visible { outline: 2px solid var(--accent-cyan, #2FB2EA); outline-offset: 2px; border-radius: 6px; }
.kv-lb-tabs { flex-wrap: wrap; max-width: 100%; }
.kv-lb-periods { display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; margin: 0 0 clamp(14px, 2vh, 20px); }
.kv-lb-periods[hidden] { display: none; }
.kv-lb-periods .kv-src.is-active { border-color: var(--accent-blue, #2563EB); color: var(--text); }
.kv-lb-body--full { max-width: 760px; margin: 0 auto; }
.kv-lb-caption { margin: 0 0 8px; font-size: 0.78rem; font-weight: 700; letter-spacing: 0.02em; color: var(--text-muted); }
.kv-lb-list { list-style: none; margin: 0; }
.kv-lb-list--me { margin-top: 0; }
.kv-lb-rank { width: auto; min-width: 30px; }
.kv-lb-empty, .kv-lb-hint { margin: 8px 0 0; font-size: 0.85rem; color: var(--text-muted); }
.kv-lb-tier {
    display: inline-flex; align-items: center; gap: 6px; flex-shrink: 0;
    min-width: 104px; font-size: 0.8rem; font-weight: 700; color: var(--text);
}
.kv-lb-body--full .cc-lb-xp { min-width: 52px; text-align: right; }
.kv-lb-badge { width: 22px; height: 22px; object-fit: contain; }
.kv-lb-stat { flex-shrink: 0; min-width: 48px; text-align: right; font-size: 0.8rem; color: var(--text-muted); }
.kv-lb-row.is-me .cc-lb-xp { color: var(--text); }
/* A phone has room for rank, name and value; the extras go first. */
@media (max-width: 520px) {
    .kv-lb-stat { display: none; }
    .kv-lb-tier { font-size: 0; gap: 0; min-width: 0; }
    .kv-lb-tabs .kv-chall-tab { padding: 8px 14px; }
}
.kv-chall-row {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: clamp(10px, 1.2vw, 16px);
    align-items: stretch;
}
/* A single card must not stretch the full width and read like a banner. */
.kv-chall-row[data-count="1"] { grid-template-columns: minmax(0, 1fr); max-width: 520px; }
.kv-chall-row .kv-chall-card { margin: 0; }
/* The button is the whole card, so it has to fill the grid cell or two cards
   of different text lengths end up different heights. */
.kv-chall-row .kv-chall-card,
.kv-chall-row .kv-chall-card-btn { height: 100%; }
/* 🚨 minmax(0, 1fr) above is what lets a long challenge name wrap instead of
   forcing the track wider than the page. */
@media (max-width: 640px) {
    .kv-chall-row { grid-template-columns: minmax(0, 1fr); }
}
.kv-chall-card-btn {
    display: block; width: 100%; text-align: left; cursor: pointer;
    padding: clamp(13px, 1.6vw, 17px);
    border-radius: 16px; border: 1px solid rgba(255, 255, 255, 0.14);
    background: rgba(255, 255, 255, 0.06); color: inherit; font: inherit;
    transition: border-color .15s ease, background-color .15s ease;
}
.kv-chall-card-btn:hover { background: rgba(255, 255, 255, 0.1); border-color: rgba(255, 255, 255, 0.26); }
.kv-chall-card-btn:focus-visible { outline: 2px solid var(--accent-cyan, #2FB2EA); outline-offset: 2px; }
.kv-chall-card-head { display: flex; align-items: center; gap: 12px; }
.kv-chall-card-icon { flex: 0 0 auto; font-size: 1.7rem; line-height: 1; width: 40px; height: 40px;
    display: grid; place-items: center; }
.kv-chall-card-icon img { width: 40px; height: 40px; object-fit: contain; border-radius: 8px; }
.kv-chall-card-titles { display: flex; flex-direction: column; min-width: 0; flex: 1 1 auto; }
.kv-chall-card-titles b { font-size: 0.98rem; font-weight: 800; }
.kv-chall-card-titles span { font-size: 0.8rem; color: var(--text-muted, #94A3B8);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.kv-chall-card-when { font-size: 0.75rem; font-weight: 700; color: var(--text-muted, #94A3B8); white-space: nowrap; }
.kv-chall-card-chev { font-size: 1.4rem; color: var(--text-muted, #94A3B8); }
.kv-chall-card-prog { display: flex; align-items: center; gap: 10px; margin-top: 10px; }

/* One bar, used by the card and by every criterion row on the page. */
.kv-chall-bar {
    flex: 1 1 auto; display: block; height: 8px; min-width: 60px;
    border-radius: 99px; overflow: hidden;
    background: rgba(148, 163, 184, 0.28);
}
.kv-chall-bar > span {
    display: block; height: 100%; border-radius: 99px;
    background: linear-gradient(90deg, var(--accent-cyan, #2FB2EA), var(--accent-purple, #8B5CF6));
    transition: width .35s ease;
}
.kv-chall-pct { font-size: 0.82rem; font-weight: 800; white-space: nowrap; }
.kv-chall-more { font-size: 0.75rem; color: var(--text-muted, #94A3B8); white-space: nowrap; }
.kv-chall-done { font-size: 0.85rem; font-weight: 800; color: #16A34A; white-space: nowrap; }

.kv-chall-tabs { display: flex; gap: 6px; padding: 5px; margin: 0 auto clamp(14px, 2vh, 20px);
    width: fit-content; border-radius: 999px; background: var(--surface-2, rgba(15, 23, 42, 0.06)); }
.kv-chall-tab { appearance: none; border: none; background: transparent; cursor: pointer;
    padding: 9px 20px; border-radius: 999px; font: inherit; font-weight: 700; font-size: 0.87rem;
    color: var(--text-muted, #94A3B8); }
.kv-chall-tab.is-active { background: var(--surface-1, #FFF); color: var(--text); box-shadow: 0 1px 3px rgba(0,0,0,0.12); }
.kv-chall-tab:focus-visible { outline: 2px solid var(--accent-blue, #2563EB); outline-offset: 2px; }

.kv-chall {
    padding: clamp(15px, 2vw, 20px); margin-bottom: 14px;
    border: 1px solid var(--border, rgba(15, 23, 42, 0.12)); border-radius: 16px;
    background: var(--surface-1, #FFFFFF);
}
.kv-chall.is-awarded { border-color: rgba(22, 163, 74, 0.5); }
.kv-chall-top { display: flex; align-items: flex-start; gap: 14px; }
.kv-chall-badge { width: 52px; height: 52px; object-fit: contain; border-radius: 10px; flex: 0 0 auto; }
.kv-chall-titles { flex: 1 1 auto; min-width: 0; }
.kv-chall-titles h3 { margin: 0 0 3px; font-size: 1.02rem; font-weight: 800; color: var(--text); }
.kv-chall-titles p { margin: 0; font-size: 0.84rem; color: var(--text-muted); line-height: 1.45; }
.kv-chall-badge-name { margin-top: 4px !important; font-size: 0.78rem !important; }
.kv-chall-state { display: flex; flex-direction: column; align-items: flex-end; gap: 3px; flex: 0 0 auto; }
.kv-chall-pct-big { font-size: 1.25rem; font-weight: 800; color: var(--accent-blue, #2563EB); }
.kv-chall-when { font-size: 0.72rem; color: var(--text-muted); white-space: nowrap; }
.kv-chall-rule { margin: 14px 0 8px; font-size: 0.8rem; color: var(--text-muted); }

.kv-chall-parts { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 9px; }
.kv-chall-part { display: grid; grid-template-columns: minmax(0, 1fr) minmax(70px, 2fr) auto;
    gap: 10px; align-items: center; font-size: 0.82rem; }
.kv-chall-part-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text); }
.kv-chall-part-n { font-weight: 700; color: var(--text-muted); white-space: nowrap; }
.kv-chall-part.is-met .kv-chall-part-n { color: #16A34A; }
/* A criterion this build cannot measure has no bar — a 0% bar would be a
   claim about progress nobody has checked. */
.kv-chall-part.is-unknown { grid-template-columns: minmax(0, 1fr) auto; }
.kv-chall-part.is-unknown .kv-chall-part-n { font-weight: 600; font-style: italic; }

@media (max-width: 640px) {
    .kv-chall-part { grid-template-columns: minmax(0, 1fr) auto; }
    .kv-chall-part .kv-chall-bar { grid-column: 1 / -1; }
}
@media (prefers-reduced-motion: reduce) { .kv-chall-bar > span { transition: none; } }

.kv-quiz-grid {
    display: grid;
    /* 🔑 The SAME track width as `.kv-row-grid > *`, so a card is the same
       size whether it sits in a subject rail or in the full grid (owner,
       2026-09-18: "reduce the size of quiz cards to match the sizing used in
       featured and popular"). auto-fill rather than a fixed column count, so
       a wide screen gets more cards instead of bigger ones. */
    grid-template-columns: repeat(auto-fill, clamp(208px, 23vw, 252px));
    justify-content: start;
    gap: clamp(12px, 1.6vw, 18px);
}
/* One full-width card is friendlier than a 208px one floating in a phone
   column, and it matches what the rails do at this width. */
@media (max-width: 520px)  { .kv-quiz-grid { grid-template-columns: 1fr; } }
.kv-quiz-card {
    display: flex; flex-direction: column;
    border-radius: 16px; overflow: hidden;
    border: 1px solid var(--border);
    background: var(--bg-card, #FFFFFF);
    padding: 0; text-align: left;
    font-family: inherit; color: var(--text);
    cursor: pointer;
    transition: transform 0.14s ease, border-color 0.14s ease, box-shadow 0.14s ease;
}
.kv-quiz-card:hover { transform: translateY(-3px); border-color: var(--accent-blue, #2563EB); box-shadow: var(--shadow-md); }
.kv-quiz-card:focus-visible { outline: 2px solid var(--accent-blue, #2563EB); outline-offset: 2px; }
.kv-quiz-art {
    aspect-ratio: 16 / 10; flex: 0 0 auto;
    display: flex; align-items: center; justify-content: center;
    overflow: hidden;
    position: relative;            /* the kind badge anchors to this */
    background: linear-gradient(135deg, rgba(37,99,235,.14), rgba(139,92,246,.14));
}
.kv-quiz-art img { width: 100%; height: 100%; object-fit: cover; display: block; }
.kv-quiz-art-fallback { font-size: 2.4rem; }
/* Same treatment as the format cards: a dark panel, rounded at the top, lifted
   onto the artwork. */
.kv-quiz-body {
    display: flex; flex-direction: column; gap: 8px;
    flex: 1;
    padding: 13px 14px 14px;
    background: #0F1A2E; color: #E8EEF8;
    border-radius: 14px 14px 0 0;
    margin-top: -14px;
    position: relative; z-index: 1;
}
/* A flashcard set in the quiz grid. It is a different KIND of thing, so it
   says so rather than looking like a quiz with a missing image. */
.kv-quiz-card--vocab .kv-quiz-art {
    background: linear-gradient(135deg, rgba(139, 92, 246, 0.20), rgba(37, 99, 235, 0.14));
}
.kv-quiz-kind {
    position: absolute; top: 10px; right: 10px;
    padding: 4px 10px; border-radius: 999px;
    background: rgba(139, 92, 246, 0.9); color: #FFFFFF;
    font-size: 0.68rem; font-weight: 800; letter-spacing: 0.02em;
}

.kv-quiz-title { font-weight: 800; font-size: 0.92rem; color: #FFFFFF; }
.kv-quiz-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: auto; }
.kv-quiz-chip {
    display: inline-flex; align-items: center; gap: 5px;
    padding: 4px 9px; border-radius: 999px;
    background: rgba(255, 255, 255, 0.08);
    border: 1px solid rgba(255, 255, 255, 0.10);
    color: #C7D6EA;
    font-size: 0.62rem; font-weight: 800; letter-spacing: 0.4px;
    white-space: nowrap;
}
/* Loading skeletons — the shape of the real thing, obviously not it. */
.kv-quiz-skel { cursor: default; pointer-events: none; }
.kv-quiz-skel .kv-quiz-art { background: var(--surface-2, rgba(15, 23, 42, 0.06)); }
.kv-quiz-skel-line { height: 11px; border-radius: 999px; background: rgba(255,255,255,0.10); }
.kv-quiz-skel-line.short { width: 62%; }
/* Says what a pick does today. Quiet — it is a footnote, not a warning. */
.kv-content-foot {
    margin: clamp(16px, 3vh, 26px) 0 0;
    text-align: center; color: var(--text-dim, var(--text-muted));
    font-size: 0.8rem; line-height: 1.5;
}
.kv-content-foot b { font-weight: 700; color: var(--text-muted); }

.kv-content-note {
    margin: 18px 0 0; padding: 18px;
    border-radius: 14px;
    border: 1px dashed var(--border-strong, rgba(15, 23, 42, 0.18));
    text-align: center; color: var(--text-muted);
}

/* ── Competitive profile (ranking spec §18) and the §20 standing strip ── */
.kv-cp[hidden], .kv-cstrip[hidden] { display: none; }
.kv-cp { margin: 0 0 clamp(18px, 3vh, 28px); }
/* Four cards (1v1, 1vWorld, Solo, Group Play): 4 across, 2x2, then 1. */
.kv-cp-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 14px; }
@media (max-width: 1100px) { .kv-cp-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 560px) { .kv-cp-grid { grid-template-columns: 1fr; } }
.kv-cp-card {
    border: 1px solid var(--border); border-radius: 14px; background: var(--bg-card);
    padding: 16px; display: flex; flex-direction: column; gap: 8px; min-width: 0;
}
/* No text-transform: the mode names are "1v1" and "1vWorld", and uppercase
   turns them into "1V1" / "1VWORLD". */
/* design/progressMockup.png — four modes, four colours. Four identical white
   cards read as one undifferentiated block; the tint is what makes the row
   scannable. Kept as a WASH plus a matching border, not a solid fill, so the
   numbers on top keep their contrast in both themes. */
.kv-cp-card.is-solo  { background: linear-gradient(160deg, rgba(59,130,246,0.10), rgba(59,130,246,0.02)); border-color: rgba(59,130,246,0.30); }
.kv-cp-card.is-duel  { background: linear-gradient(160deg, rgba(236,72,153,0.10), rgba(236,72,153,0.02)); border-color: rgba(236,72,153,0.30); }
.kv-cp-card.is-world { background: linear-gradient(160deg, rgba(139,92,246,0.10), rgba(139,92,246,0.02)); border-color: rgba(139,92,246,0.30); }
.kv-cp-card.is-group { background: linear-gradient(160deg, rgba(34,197,94,0.10), rgba(34,197,94,0.02)); border-color: rgba(34,197,94,0.30); }
/* The washes are alpha over --bg-card, so they carry into dark mode on their
   own — only the borders need lifting, or they vanish against a dark surface. */
:root:not([data-theme="light"]) .kv-cp-card.is-solo  { border-color: rgba(59,130,246,0.42); }
:root:not([data-theme="light"]) .kv-cp-card.is-duel  { border-color: rgba(236,72,153,0.42); }
:root:not([data-theme="light"]) .kv-cp-card.is-world { border-color: rgba(139,92,246,0.42); }
:root:not([data-theme="light"]) .kv-cp-card.is-group { border-color: rgba(34,197,94,0.42); }
:root[data-theme="dark"] .kv-cp-card.is-solo  { border-color: rgba(59,130,246,0.42); }
:root[data-theme="dark"] .kv-cp-card.is-duel  { border-color: rgba(236,72,153,0.42); }
:root[data-theme="dark"] .kv-cp-card.is-world { border-color: rgba(139,92,246,0.42); }
:root[data-theme="dark"] .kv-cp-card.is-group { border-color: rgba(34,197,94,0.42); }
.kv-cp-card h3 { margin: 0; font-size: 0.85rem; font-weight: 800; letter-spacing: 0.02em; color: var(--text-muted); }
.kv-cp-rank { display: inline-flex; align-items: center; gap: 8px; font-weight: 800; font-size: 1.05rem; color: var(--text); }
.kv-cp-rank--placement { font-size: 0.95rem; }
.kv-cp-badge { object-fit: contain; }
.kv-cp-big { font-size: 1.9rem; font-weight: 800; line-height: 1.1; color: var(--text); }
.kv-cp-big span { display: block; font-size: 0.75rem; font-weight: 600; color: var(--text-muted); }
.kv-cp-line { display: flex; justify-content: space-between; gap: 10px; font-size: 0.86rem; color: var(--text-muted); }
.kv-cp-line b { color: var(--text); }
.kv-cp-empty { margin: 0; font-size: 0.85rem; color: var(--text-muted); }
.kv-cstrip {
    display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 8px 18px;
    margin: 0 auto 14px; padding: 10px 16px; max-width: 760px;
    border: 1px solid var(--border, rgba(15, 23, 42, 0.12)); border-radius: 16px;
    font-size: 0.85rem; color: var(--text-muted);
}
.kv-cstrip .kv-cp-rank { font-size: 0.9rem; }
.kv-cstrip-item b { color: var(--text); }
.kv-cstrip-lb {
    appearance: none; border: none; background: transparent; cursor: pointer; padding: 4px 2px;
    font: inherit; font-weight: 700; color: var(--accent-blue, #2563EB);
}
.kv-cstrip-lb:hover { text-decoration: underline; }
.kv-cstrip-lb:focus-visible { outline: 2px solid var(--accent-blue, #2563EB); outline-offset: 2px; border-radius: 6px; }

/* ── After a ranked 1v1 match (ranking spec §20) ─────────────────────────
   Above the plugin's full-screen modal (9500), where the match ends. */
.kv-mr {
    position: fixed; right: 16px; bottom: 16px; z-index: 10050;
    width: min(340px, calc(100vw - 32px));
    padding: 14px 16px 16px; border-radius: 16px;
    background: var(--bg-card, #FFFFFF); color: var(--text);
    border: 1px solid var(--border, rgba(15, 23, 42, 0.12));
    box-shadow: 0 18px 40px rgba(2, 6, 23, 0.28);
    display: flex; flex-direction: column; gap: 8px;
    animation: kvMrIn .24s ease;
}
@keyframes kvMrIn { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) { .kv-mr { animation: none; } }
.kv-mr-head { display: flex; align-items: baseline; gap: 10px; }
.kv-mr-head strong { font-size: 1.35rem; font-weight: 900; letter-spacing: 0.02em; }
.kv-mr-head--win strong { color: #16A34A; }
.kv-mr-head--loss strong { color: #DC2626; }
.kv-mr-kicker { order: 2; font-size: 0.72rem; font-weight: 700; color: var(--text-muted); }
.kv-mr-close {
    order: 3; margin-left: auto; appearance: none; border: none; background: transparent; cursor: pointer;
    font-size: 1.4rem; line-height: 1; color: var(--text-muted); padding: 2px 6px; border-radius: 8px;
}
.kv-mr-close:focus-visible, .kv-mr-lb:focus-visible { outline: 2px solid var(--accent-blue, #2563EB); outline-offset: 2px; }
.kv-mr-row { display: flex; align-items: baseline; gap: 8px; font-size: 0.9rem; }
.kv-mr-row span { color: var(--text-muted); min-width: 90px; }
.kv-mr-row b { font-weight: 800; }
.kv-mr-delta { margin-left: auto; font-style: normal; font-weight: 800; }
.kv-mr-delta--up { color: #16A34A; }
.kv-mr-delta--down { color: #DC2626; }
.kv-mr-delta--flat { color: var(--text-muted); }
.kv-mr-rankup b { color: #7C3AED; }
.kv-mr-xp { align-self: flex-start; padding: 4px 10px; border-radius: 999px; font-weight: 800; font-size: 0.85rem;
    background: rgba(245, 158, 11, 0.16); color: var(--text); }
.kv-mr-lb { align-self: flex-start; appearance: none; border: none; background: transparent; cursor: pointer;
    padding: 4px 0; font: inherit; font-weight: 700; color: var(--accent-blue, #2563EB); }
.kv-mr-lb:hover { text-decoration: underline; }
@media (max-width: 520px) { .kv-mr { left: 16px; right: 16px; width: auto; } }

/* Group Play setup: which class the game counts toward. */
.kv-gp-class { display: flex; flex-direction: column; gap: 6px; margin-top: 12px; font-size: 0.85rem; font-weight: 700; color: var(--text); }
.kv-gp-class i { font-style: normal; font-weight: 500; color: var(--text-muted); }
.kv-gp-class select {
    font: inherit; font-weight: 600; padding: 9px 12px; border-radius: 10px; max-width: 100%;
    border: 1px solid var(--border, rgba(15, 23, 42, 0.14)); background: var(--surface, #FFFFFF); color: var(--text);
}
.kv-gp-class select:focus-visible { outline: 2px solid var(--accent-blue, #2563EB); outline-offset: 2px; }

/* Class Group Play board: which class. */
.kv-lb-classpick { display: flex; align-items: center; justify-content: center; gap: 8px; margin: 0 0 12px; font-size: 0.85rem; font-weight: 700; color: var(--text); }
.kv-lb-classpick select {
    font: inherit; font-weight: 600; padding: 7px 10px; border-radius: 10px; max-width: 100%;
    border: 1px solid var(--border, rgba(15, 23, 42, 0.14)); background: var(--surface, #FFFFFF); color: var(--text);
}
.kv-lb-classpick select:focus-visible { outline: 2px solid var(--accent-blue, #2563EB); outline-offset: 2px; }

/* Explore — the heading between the picture hero and the search bar. */
.kv-explore-heading {
    margin: 4px 0 14px;
    font-family: var(--font-display);
    font-size: clamp(1.25rem, 2.4vw, 1.7rem);
    font-weight: 800;
    line-height: 1.2;
    letter-spacing: -0.01em;
    color: var(--text);
}

/* Explore — signed-out picture hero (2159×728 artwork). The link box is placed
   over the "Get Started Free" button drawn into the image, in percentages so it
   follows the picture at every width. */
.kv-explore-hero { position: relative; margin: 0 0 18px; border-radius: 16px; overflow: hidden; line-height: 0; }
.kv-explore-hero img { display: block; width: 100%; height: auto; }
.kv-explore-hero-cta {
    position: absolute; left: 4.1%; top: 71.1%; width: 14.75%; height: 9.8%;
    border-radius: 999px;
}
.kv-explore-hero-cta:focus-visible { outline: 3px solid #FFFFFF; outline-offset: 2px; }
/* On a phone the drawn button shrinks to ~50×11px — far below a tappable size —
   so the WHOLE picture becomes the "Get started free" link. */
@media (max-width: 640px) {
    .kv-explore-hero-cta { left: 0; top: 0; width: 100%; height: 100%; border-radius: 16px; }
    .kv-explore-hero-cta:focus-visible { outline-offset: -3px; }
}

/* ── Public profiles (owner, 2026-09-17) ─────────────────────────────────── */
.kv-profile-link { color: inherit; text-decoration: none; }
.kv-profile-link:hover, .kv-profile-link:focus-visible { text-decoration: underline; }
.kv-profile-id { display: flex; align-items: center; gap: 18px; min-width: 0; flex: 1 1 320px; position: relative; }
.kv-profile-avatar {
    width: 84px; height: 84px; border-radius: 50%; object-fit: cover; flex-shrink: 0;
    border: 3px solid var(--bg-card); box-shadow: 0 4px 16px rgba(15, 23, 42, 0.18);
}
.kv-profile-avatar--blank { display: flex; align-items: center; justify-content: center; color: #fff; font-size: 2rem; font-weight: 800; }
.kv-profile-flag { width: 24px; height: 17px; vertical-align: 2px; }
.kv-profile-hero .cc-hero-sub { margin-bottom: 10px; }
.kv-profile .kv-cp-grid { margin-bottom: 8px; }
.kv-profile-badges { display: grid; grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); gap: 12px; padding: 14px; }
.kv-profile-badge { display: flex; flex-direction: column; align-items: center; gap: 6px; text-align: center; font-size: 0.8rem; font-weight: 700; color: var(--text); }
.kv-profile-badge img { width: 56px; height: 56px; object-fit: contain; }
.kv-profile-badge-blank { font-size: 2.2rem; line-height: 56px; }
.kv-profile-cta { margin: 22px 0; padding: 18px; border: 1px solid var(--border); border-radius: 14px; background: var(--bg-card); display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
.kv-profile-cta p { margin: 0; font-weight: 700; color: var(--text); }
.kv-profile-cta .btn-primary, .kv-psettings-actions .btn-primary, .kv-psettings-actions .btn-secondary, .kv-share .btn-primary { width: auto; display: inline-flex; }
@media (max-width: 560px) {
    .kv-profile-id { gap: 12px; }
    .kv-profile-avatar { width: 64px; height: 64px; }
}

.kv-share { position: relative; display: inline-block; }
.kv-share-menu {
    position: absolute; z-index: 50; top: calc(100% + 6px); left: 0; min-width: min(180px, calc(100vw - 16px)); max-width: calc(100vw - 16px);
    display: flex; flex-direction: column; padding: 6px; border-radius: 12px;
    background: var(--bg-card); border: 1px solid var(--border); box-shadow: 0 12px 30px rgba(15, 23, 42, 0.18);
}
.kv-share-menu[hidden] { display: none; }
.kv-share-opt {
    display: block; width: 100%; text-align: left; padding: 8px 10px; border-radius: 8px; border: 0; background: none;
    color: var(--text); font: inherit; font-size: 0.88rem; font-weight: 600; text-decoration: none; cursor: pointer;
}
.kv-share-opt:hover, .kv-share-opt:focus-visible { background: var(--bg-secondary); }

.kv-psettings { margin-top: 8px; }
.kv-psettings[hidden] { display: none; }
.kv-psettings-body { padding: 16px 18px; }
.kv-psettings-lead { margin: 0 0 12px; color: var(--text-muted); font-size: 0.9rem; }
.kv-psettings-opts { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px; }
.kv-psettings-opt {
    display: flex; gap: 10px; align-items: flex-start; padding: 10px 12px; border: 1px solid var(--border);
    border-radius: 12px; cursor: pointer; background: var(--bg-card);
}
.kv-psettings-opt.is-on { border-color: var(--brand-blue, #3B82F6); background: linear-gradient(135deg, rgba(59, 130, 246, 0.10), rgba(139, 92, 246, 0.10)); }
.kv-psettings-opt input { margin-top: 3px; accent-color: var(--brand-blue, #3B82F6); }
.kv-psettings-opt span { display: flex; flex-direction: column; gap: 2px; }
.kv-psettings-opt b { font-size: 0.9rem; color: var(--text); }
.kv-psettings-opt small { font-size: 0.78rem; color: var(--text-muted); line-height: 1.35; }
.kv-psettings-actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-top: 14px; }
.kv-psettings-hint { font-size: 0.82rem; color: var(--text-muted); }
.kv-psettings-note { margin: 12px 0 0; font-size: 0.85rem; color: var(--text-muted); }
.kv-psettings-err { margin: 10px 0 0; font-size: 0.85rem; color: #DC2626; }

/* "Profile" link on an Arena / opponent card. The card is itself a button, so
   this is a span with role=link and a capture-phase handler. */
.kv-card-profile {
    margin-left: auto;
    align-self: center;
    padding: 4px 10px;
    border-radius: 999px;
    border: 1px solid var(--border);
    background: var(--bg-card);
    color: var(--text-muted);
    font-size: 0.74rem;
    font-weight: 700;
    white-space: nowrap;
    cursor: pointer;
}
.kv-card-profile:hover { color: var(--text); border-color: var(--brand-blue, #3B82F6); }
.kv-card-profile:focus-visible { outline: 2px solid var(--brand-blue, #3B82F6); outline-offset: 2px; }

/* Share menu beside the "Your progress" heading, and the small modals. */
.kv-prog-headline { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.kv-prog-headline .cc-hero-greeting { margin-bottom: 0; }
.kv-prog-share { position: relative; z-index: 60; }
/* Anchored to the button's RIGHT edge: the button sits at the right of the
   hero, so a left-anchored menu ran off the screen on a phone. */
.kv-prog-share .kv-share-menu { right: 0; left: auto; z-index: 70; }
.kv-prog-share[hidden] { display: none; }
.kv-prog-share-btn { white-space: nowrap; }
.kv-smodal-body { padding: 4px 20px 20px; }
.kv-smodal-body .kv-psettings-opts { grid-template-columns: 1fr; }
.kv-psettings-url { margin: 0 0 12px; }
.kv-psettings-url code {
    display: block;
    padding: 8px 10px;
    border-radius: 8px;
    background: var(--bg-secondary);
    font-size: 0.82rem;
    word-break: break-all;
    color: var(--text);
}
.kv-share-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 8px; }
.kv-share-grid .kv-share-opt {
    border: 1px solid var(--border);
    border-radius: 10px;
    text-align: center;
    padding: 10px 8px;
}

/* Ranked / Casual tags on the 1v1 opponent step and the Public Arena. */
.kv-tag {
    display: inline-flex;
    align-items: center;
    padding: 2px 8px;
    border-radius: 999px;
    font-size: 0.68rem;
    font-weight: 800;
    letter-spacing: 0.03em;
    text-transform: uppercase;
    white-space: nowrap;
    vertical-align: 1px;
}
.kv-tag--ranked { background: rgba(37, 99, 235, 0.16); color: #1D4ED8; }
.kv-tag--casual { background: rgba(100, 116, 139, 0.16); color: var(--text-muted); }
[data-theme="dark"] .kv-tag--ranked { background: rgba(96, 165, 250, 0.22); color: #BFDBFE; }
[data-theme="dark"] .kv-tag--casual { background: rgba(148, 163, 184, 0.20); color: #CBD5E1; }
.kv-opp-rules { margin: 8px 0 0; font-size: 0.82rem; line-height: 1.6; color: var(--text-muted); }
/* The Casual/Ranked toggle also sits on the opponent step, so the blurb under
   it needs the same breathing room the format screen gives it. */
.kv-content .kv-arena-tabs { margin: 0 auto 6px; }
.kv-content .kv-arena-lede { margin: 0 0 16px; text-align: center; }
.kv-opp-switch { margin-top: 14px; text-align: center; }
.kv-opp-card .kv-tag { margin-left: 6px; }

/* ── Homepage, rebuilt to design/homepageMockup.png (owner, 2026-09-18) ──── */
.kv-hhero {
    position: relative;
    /* 🚨 FULL-BLEED AND FLUSH UNDER THE HEADER (owner, 2026-10-01): "spread
       full width and align to the bottom of the header like other pages".
       The same treatment .kv-dhero already gives My Decks / Explore / Classes /
       Progress — no radius, no border, and the container's own inset cancelled
       on three sides using the variables .main-panel publishes. The CONTENT
       keeps the page gutter (see .kv-hhero-inner), so only the band runs to
       the edges. */
    border-radius: 0;
    border: 0;
    /* Sides always; the TOP only when the band really is the first thing in
       its container (.kv-hhero:first-child below) — the same guard .kv-dhero
       needs, because pulling a band up that has something above it covers it. */
    margin: 0 calc(-1 * var(--kv-bleed-x, 0px)) 0;
    /* 🔑 THE BAND NOW CARRIES ITS OWN DARK BASE. The artwork used to be the
       only thing behind the white text; it is masked to fade out toward the
       bottom-left now, so without a base the text would sit on the page
       background and become unreadable exactly where it lives. */
    background: linear-gradient(135deg, #0A1430 0%, #122048 52%, #0C1838 100%);
    min-height: 200px;
    /* Safe here, unlike .kv-dhero: this band holds two buttons and two stat
       tiles — nothing position:absolute that opens past its edge. */
    overflow: hidden;
    isolation: isolate;
}
/* Flush with the header bar. Only as the first child, for the reason above. */
.kv-hhero:first-child { margin-top: calc(-1 * var(--kv-bleed-top, 0px)); }
.kv-hhero-bg {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    /* Framed from the RIGHT: the artwork's handwriting and window sit on that
       side, and centring the crop put the writing behind the welcome text. */
    object-position: right center;
    /* 🚨 FADES OUT TOWARD THE BOTTOM-LEFT (owner, 2026-10-01: "with it fading
       bottom left"). Same direction and stops as .kv-dhero::before, so the
       homepage reads as one family with the other bands: opaque at the
       top-right where the illustration is, dissolving into the dark base
       diagonally across the band — which is also where the avatar, the
       greeting and the buttons sit. */
    -webkit-mask-image: linear-gradient(to top right,
        rgba(0, 0, 0, 0) 0%,
        rgba(0, 0, 0, 0) 18%,
        rgba(0, 0, 0, 0.32) 44%,
        rgba(0, 0, 0, 0.78) 72%,
        rgba(0, 0, 0, 1) 100%);
    mask-image: linear-gradient(to top right,
        rgba(0, 0, 0, 0) 0%,
        rgba(0, 0, 0, 0) 18%,
        rgba(0, 0, 0, 0.32) 44%,
        rgba(0, 0, 0, 0.78) 72%,
        rgba(0, 0, 0, 1) 100%);
    z-index: 0;
}
.kv-hhero::after {
    content: '';
    position: absolute;
    inset: 0;
    /* Retuned with the mask + dark base: the artwork no longer reaches the left
       at all, so a near-opaque wash there was darkening a dark gradient into
       mud, and it still dimmed the top-right where the picture now lives.
       Leans left for the text, clears entirely before the illustration. */
    background: linear-gradient(100deg,
        rgba(8, 15, 35, 0.82) 0%,
        rgba(8, 15, 35, 0.52) 42%,
        rgba(8, 15, 35, 0.14) 72%,
        rgba(8, 15, 35, 0) 100%);
    z-index: 1;
}
.kv-hhero-inner {
    position: relative;
    z-index: 2;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 22px;
    flex-wrap: wrap;
    /* 🔑 The band is full-bleed but its TEXT is not: put back exactly the inset
       the band cancelled, so the greeting sits on the same gutter as the
       modules below it (the same rule .kv-dhero-inner follows). */
    padding-block: 24px;
    padding-inline: max(var(--kv-bleed-x, 22px), 16px);
    color: #F8FAFC;
}
.kv-hhero-who { display: flex; align-items: center; gap: 18px; min-width: 0; flex: 1 1 340px; }
.kv-hhero-avatar {
    width: 72px; height: 72px; border-radius: 50%; flex-shrink: 0;
    border: 3px solid rgba(255, 255, 255, 0.85);
    box-shadow: 0 6px 18px rgba(2, 6, 23, 0.45);
    font-size: 1.7rem;
}
.kv-hhero-text { min-width: 0; }
.kv-hhero-eyebrow { margin: 0; font-size: 0.86rem; color: rgba(248, 250, 252, 0.82); }
.kv-hhero-name {
    margin: 2px 0 6px;
    font-family: var(--font-display, inherit);
    font-size: clamp(1.5rem, 3.2vw, 2rem);
    font-weight: 800;
    line-height: 1.1;
    color: #FFFFFF;
}
.kv-hhero-lead { margin: 0; font-weight: 700; font-size: 0.95rem; color: rgba(248, 250, 252, 0.95); }
.kv-hhero-sub { margin: 2px 0 0; font-size: 0.86rem; color: rgba(248, 250, 252, 0.78); }
.kv-hhero-sub:empty { display: none; }
.kv-hhero-cta { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 14px; }
.kv-hhero-cta .btn-primary, .kv-hhero-cta .btn-secondary { width: auto; display: inline-flex; }
.kv-hhero-cta .btn-secondary {
    background: rgba(15, 23, 42, 0.55);
    color: #F8FAFC;
    border: 1px solid rgba(248, 250, 252, 0.45);
}
.kv-hhero-stats { display: flex; gap: 12px; flex-wrap: wrap; }
.kv-hstat {
    display: flex; align-items: center; gap: 12px;
    padding: 12px 16px;
    border-radius: 14px;
    background: rgba(9, 14, 32, 0.72);
    border: 1px solid rgba(248, 250, 252, 0.16);
    backdrop-filter: blur(6px);
    min-width: 168px;
}
/* 🔑 Its own font-size. `.cc-rank-art` (same element, same specificity,
   declared earlier) sets 2.4rem for the 🏆 placeholder glyph, sized for that
   card's larger art slot — in this 44px one the glyph's advance ran 2px past
   the box. Only shows when a tier has no badge image. */
.kv-hstat-art { width: 44px; height: 44px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; font-size: 1.75rem; }
.kv-hstat-art img { width: 44px; height: 44px; object-fit: contain; }
.kv-hstat-body { min-width: 0; }   /* load-bearing: lets the bar's max-width clamp */
.kv-hstat-kicker { font-size: 0.7rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; color: rgba(248, 250, 252, 0.72); }
/* 🚨 The tier name is ADMIN-AUTHORED (the `ranks` collection), so its
   length is not bounded by anything here. “Grandmaster Scholar” is one
   unbreakable 11-character word wider than the 78px body a phone leaves, and
   it spilled 40px out of the card toward the streak card — the same symptom as
   the bar above, from a different child. Breaking inside the word is worse
   typography and far better than text sitting outside its own card. */
.kv-hstat-name { font-size: 1rem; font-weight: 800; color: #FFFFFF; overflow-wrap: anywhere; }
/* 🚨 FLUID, NOT A FIXED 150px. It was `width: 150px; max-width: 42vw`,
   and on a phone the level card's body is only ~78px wide — so the bar ran
   55px PAST its own card and 43px INTO the day-streak card beside it, which
   paints later and covered the overspill (owner, on mobile). 42vw never
   clamped it: 42vw at 360px is 151px, wider than the bar already was.
   → `width: 150px; max-width: 100%`, NOT `width: 100%; max-width: 150px`.
   The percentage-first version fixed the overflow but SHRANK THE DESKTOP: a
   percentage contributes nothing to intrinsic width, so the card collapsed to
   its 168px min-width and the bar with it (240→168px card, 150→78px bar,
   measured). Keeping the 150px as the WIDTH preserves that intrinsic pull;
   `max-width: 100%` is what clamps it to the body on a phone. */
.kv-hstat-bar { width: 150px; max-width: 100%; height: 6px; border-radius: 999px; background: rgba(248, 250, 252, 0.22); margin: 6px 0 4px; overflow: hidden; }
.kv-hstat-bar span { display: block; height: 100%; border-radius: 999px; background: linear-gradient(90deg, #3B82F6, #8B5CF6); transition: width 0.4s ease; }
.kv-hstat-xp { font-size: 0.74rem; color: rgba(248, 250, 252, 0.78); }
.kv-hstat-count { font-size: 1.5rem; font-weight: 800; line-height: 1; color: #FFFFFF; }
.kv-hstat-caption { font-size: 0.74rem; color: rgba(248, 250, 252, 0.78); }
@media (max-width: 860px) {
    /* 🚨 NOT `padding: 20px` — that hardcoded gutter stopped matching the
       page once the band went full-bleed, pulling the greeting out of line
       with every module under it. Only the BLOCK padding changes here. */
    .kv-hhero-inner { padding-block: 20px; }
    .kv-hhero-stats { width: 100%; }
    .kv-hstat { flex: 1 1 150px; min-width: 0; }
    /* 🔑 THE LEVEL CARD IS THE BIGGER OF THE TWO (owner, on mobile).
       They were forced to exactly 1:1 here, which is the odd one out: ABOVE
       this breakpoint the pair is content-sized and already lands at ~1.43:1,
       because the level card holds a badge, a name, a progress bar and an XP
       line while the streak card holds a number and one word. Squeezing them
       equal cost the XP bar most — 63px at 360px wide.

       Measured with these values: 360px → 184/122 (bar 94px), 390px → 204/132
       (bar 114px), 414px → 220/140. Desktop is untouched.

       🚨 THE BASIS, NOT JUST THE GROW FACTOR, IS LOAD-BEARING. With
       `flex-basis: 0` the ratio would apply cleanly — and the pair would stop
       WRAPPING, so a 320px phone would show two cramped cards side by side
       instead of stacking them full width. The 180/120 bases keep the wrap at
       320px and only then does the 2:1 grow divide what is left. */
    .kv-hstat--level  { flex: 2 1 180px; }
    .kv-hstat--streak { flex: 1 1 120px; }
}

/* The two feature bands. */
.kv-hbands { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; margin: 18px 0; }
@media (max-width: 860px) { .kv-hbands { grid-template-columns: 1fr; } }
.kv-hband {
    position: relative;
    overflow: hidden;
    border-radius: var(--radius-lg, 16px);
    border: 1px solid var(--surface-glass-border, var(--border));
    min-height: 190px;
    display: flex;
    isolation: isolate;
}
/* Same reason as the hero: each band's wordmark lives on the right. */
.kv-hband-bg { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: right center; z-index: 0; }
.kv-hband::after {
    content: '';
    position: absolute; inset: 0; z-index: 1;
    background: linear-gradient(90deg, rgba(4, 9, 26, 0.92) 0%, rgba(4, 9, 26, 0.62) 55%, rgba(4, 9, 26, 0.12) 100%);
}
.kv-hband-inner { position: relative; z-index: 2; padding: 22px 24px; display: flex; flex-direction: column; justify-content: center; gap: 8px; max-width: 62%; }
.kv-hband-title { margin: 0; font-family: var(--font-display, inherit); font-size: clamp(1.3rem, 2.4vw, 1.75rem); font-weight: 800; color: #FFFFFF; }
.kv-hband-sub { margin: 0; font-size: 0.88rem; color: rgba(248, 250, 252, 0.85); }
.kv-hband-cta { width: auto; display: inline-flex; margin-top: 8px; align-self: flex-start; }
@media (max-width: 560px) { .kv-hband-inner { max-width: 80%; padding: 18px; } }

/* Creator recruitment beside the store badges. */
.kv-home-footrow { display: grid; grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr); gap: 16px; align-items: start; margin-top: 8px; }
@media (max-width: 860px) { .kv-home-footrow { grid-template-columns: 1fr; } }
/* No leaderboard above, and no creator card beside it: the store badges are
   alone in the row, so they take the whole width and sit in the middle rather
   than off to the right of an empty column (owner, 2026-10-07).
   Both conditions are read from the page itself — the leaderboard section
   carries `hidden` until it has rows, and the creator host is empty unless a
   teacher has an open ad — so no script has to keep a class in step.
   With a leaderboard, or with the creator card, the two-column row stands. */
.cc-wrap:has(#ccHomeLeaderboardSection[hidden]) .kv-home-footrow:has(#ccHomeCreatorAd:empty) { grid-template-columns: minmax(0, 1fr); }
.cc-wrap:has(#ccHomeLeaderboardSection[hidden]) .kv-home-footrow:has(#ccHomeCreatorAd:empty) #ccHomeCreatorAd { display: none; }
.kv-home-proghead { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }

/* Trending on the DASHBOARD is a RAIL: four smaller cards visible, the rest a
   sideways scroll away (owner, 2026-09-18). Explore keeps the wrapping grid.
   🚨 Declared AFTER .kvSetGrid--tight and its phone rule, both of which set
   grid-template-columns — the rail has to win, so it clears that and sizes its
   own columns instead. */
.kvSetGrid--rail {
    grid-template-columns: none;
    grid-auto-flow: column;
    /* Exactly four across, accounting for the three 10px gaps between them. */
    grid-auto-columns: calc((100% - 30px) / 4);
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scroll-snap-type: x proximity;
    /* Room for the scrollbar so it never sits on top of a card. */
    padding-bottom: 8px;
}
.kvSetGrid--rail > * { scroll-snap-align: start; min-width: 0; }
.kvSetGrid--rail::-webkit-scrollbar { height: 8px; }
.kvSetGrid--rail::-webkit-scrollbar-thumb { background: var(--border); border-radius: 999px; }
.kvSetGrid--rail:focus-visible { outline: 2px solid var(--brand-blue, #3B82F6); outline-offset: 3px; }
/* A phone cannot read four — show one and a bit, so the cut-off card is the
   hint that the row scrolls. */
@media (max-width: 640px) {
    .kvSetGrid--rail { grid-auto-columns: 74%; }
}

/* ── My Decks, rebuilt to design/myDecksMockup.png (owner, 2026-09-18) ───── */
/* ── The SET view hero (design/setViewMockup.png) ──────────────────────
   🔑 ROUNDED AND INSET on purpose. Every other hero in Knovli is full-bleed
   and flush with the app header; the owner confirmed (2026-09-19) that the
   set page is meant to differ, so this one keeps its corners and sits inside
   the content column.
   Same four-layer shape as .kv-dhero below: artwork on ::before, a scrim on
   ::after, and one z-indexed inner layer, so the title stays readable. */
/* 🚨 NO `overflow: hidden` HERE (owner, 2026-09-19: "the teacher tools
   dropdown gets cutoff at the bottom of the hero"). It was clipping the Teach
   menu to the band. The radius still has to clip the ARTWORK, so that job
   moves onto the two pseudo-elements: a background-image is clipped by its own
   element's border-radius, so `border-radius: inherit` on ::before/::after
   does everything `overflow: hidden` was doing and clips nothing else. */
.kv-sethero { position: relative; isolation: isolate;
  border-radius: 18px; margin-bottom: 18px;
  background: linear-gradient(120deg,
    color-mix(in srgb, var(--accent-blue, #2563EB) 16%, var(--bg-card, #fff)),
    var(--bg-card, #fff) 62%);
  border: 1px solid var(--border, rgba(15,23,42,.10)); }
.kv-sethero::before { content: ""; position: absolute; inset: 0; z-index: 0;
  border-radius: inherit; overflow: hidden;
  background-image: var(--kv-sethero-art); background-size: cover;
  background-position: right center; background-repeat: no-repeat;
  /* Clears the art off the words, like the dashboard bands. */
  -webkit-mask-image: linear-gradient(to left, rgba(0,0,0,1) 0%, rgba(0,0,0,.85) 26%, rgba(0,0,0,0) 62%);
  mask-image: linear-gradient(to left, rgba(0,0,0,1) 0%, rgba(0,0,0,.85) 26%, rgba(0,0,0,0) 62%); }
.kv-sethero::after { content: ""; position: absolute; inset: 0; z-index: 0;
  border-radius: inherit;
  background: linear-gradient(100deg,
    color-mix(in srgb, var(--bg-card, #fff) 82%, transparent) 0%,
    color-mix(in srgb, var(--bg-card, #fff) 40%, transparent) 52%,
    transparent 78%); }
/* 🚨 The ONLY z-indexed layer — anything interactive must live in here, or
   the artwork paints over it and it cannot be clicked. */
.kv-sethero__inner { position: relative; z-index: 1; display: flex; gap: 20px;
  align-items: flex-start; padding: 22px 24px; }
/* 🚨 …and the menu has to clear the FILTER CHIPS below, which come later in
   the DOM. The hero is `isolation: isolate`, so a z-index inside it cannot
   escape — the whole band has to be raised while a menu inside it is open. */
.kv-sethero:has(.kvMenu:not([hidden])) { z-index: 30; }
.kv-sethero__img { flex: none; width: 120px; height: 120px; border-radius: 18px;
  overflow: hidden; background: var(--surface-2, rgba(15,23,42,.05));
  box-shadow: 0 8px 22px rgba(15,23,42,.18); }
.kv-sethero__img img { width: 100%; height: 100%; object-fit: cover; display: block; }
.kv-sethero__body { flex: 1; min-width: 0; }
.kv-sethero__eyebrow { display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  margin-bottom: 8px; }
.kv-sethero__kind { font-size: 11.5px; font-weight: 800; letter-spacing: .08em;
  text-transform: uppercase; color: var(--text-muted); }
.kv-sethero__chip { font-size: 11.5px; font-weight: 700; padding: 3px 10px;
  border-radius: 999px;
  background: color-mix(in srgb, var(--accent-purple, #8B5CF6) 13%, transparent);
  color: var(--accent-purple, #8B5CF6); }
.kv-sethero__titlerow { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.kv-sethero__title { margin: 0; color: var(--text); font-size: 28px; font-weight: 800;
  letter-spacing: -.01em; line-height: 1.15; }
.kv-sethero__count { color: var(--text-dim); font-size: 13px; margin-top: 6px; }
.kv-sethero__desc { color: var(--text-muted); font-size: 14px; line-height: 1.55;
  margin: 10px 0 0; }
@media (max-width: 720px) {
  /* The picture drops above the copy rather than squeezing the title. */
  .kv-sethero__inner { flex-direction: column; gap: 14px; padding: 18px; }
  .kv-sethero__img { width: 88px; height: 88px; border-radius: 14px; }
  .kv-sethero__title { font-size: 22px; }
  /* Copy is full width here, so fade the art back further or it sits behind. */
  .kv-sethero::before {
    -webkit-mask-image: linear-gradient(to left, rgba(0,0,0,.9) 0%, rgba(0,0,0,0) 55%);
    mask-image: linear-gradient(to left, rgba(0,0,0,.9) 0%, rgba(0,0,0,0) 55%); }
}

/* ── Set EDITOR (design/setEditMockup.png) ────────────────────
   A page header with its own actions, a collapsible details card, a cards
   toolbar, a real table, and a sticky save bar. */
.kv-setedit { padding-bottom: 96px; }   /* room for the sticky bar */

.kv-sehead { display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
  margin: 0 0 18px; }
.kv-sehead__text { flex: 1 1 260px; min-width: 0; }
.kv-sehead__text h2 { margin: 0; color: var(--text); font-size: 24px;
  font-weight: 800; letter-spacing: -.01em; }
.kv-sehead__text p { margin: 3px 0 0; color: var(--text-muted); font-size: 13px; }
.kv-sehead__actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.kv-sewarn { color: #B45309; font-weight: 600; }

.kv-sebtn { display: inline-flex; align-items: center; gap: 7px; cursor: pointer;
  font: inherit; font-size: 13.5px; font-weight: 600; padding: 9px 16px;
  border-radius: 10px; border: 1px solid transparent; white-space: nowrap; }
.kv-sebtn--sm { padding: 7px 13px; font-size: 13px; }
.kv-sebtn--ghost { background: var(--bg-card); color: var(--text);
  border-color: var(--border-strong); }
.kv-sebtn--ghost:hover { background: rgba(15,23,42,0.04); }
.kv-sebtn--primary { background: linear-gradient(135deg, #2563EB, #8B5CF6);
  color: #fff; font-weight: 700; }
.kv-sebtn--primary:hover { filter: brightness(1.05); }
.kv-sebtn--ai { background: rgba(139,92,246,0.10); color: #8B5CF6;
  border-color: rgba(139,92,246,0.35); align-self: flex-start; }
.kv-selink { background: none; border: 0; padding: 0; cursor: pointer;
  font: inherit; font-size: 13px; font-weight: 700; color: var(--accent-blue, #2563EB); }
.kv-selink:hover { text-decoration: underline; }
.kv-selink--danger { color: #DC2626; }

.kv-secard { background: var(--bg-card); border: 1px solid rgba(15,23,42,0.10);
  border-radius: 14px; padding: 18px 20px; margin-bottom: 18px; }
.kv-secard__head { display: flex; align-items: center; justify-content: space-between;
  gap: 12px; flex-wrap: wrap; margin-bottom: 16px; }
.kv-secard__head h3 { display: flex; align-items: center; gap: 8px; margin: 0;
  color: var(--text); font-size: 16px; font-weight: 800; }
.kv-secount { color: var(--text-muted); font-size: 13px; font-weight: 600; }
/* 🚨 flow-root, because .kv-secover FLOATS. A float does not contribute to a
   plain block parent's height, so the cover hung out through the bottom border
   of the card whenever the field grid beside it was shorter than the picture.
   The deck editor hid this by accident — it has six fields, so its grid is
   always taller than the 150px cover; the quiz editor has four, and the
   overrun was immediate.
   ⚠️ flow-root, NOT overflow:hidden — both contain the float, but overflow
   would also CLIP the ⋯ menus that open out of this card.
   Specificity keeps the collapse working: [hidden] below is (0,2,0). */
.kv-secard__body { display: flow-root; }
.kv-secard__body[hidden] { display: none; }

.kv-secover { display: flex; flex-direction: column; align-items: center; gap: 8px;
  float: left; margin: 0 20px 12px 0; }
.kv-secover__frame { width: 150px; height: 150px; border-radius: 12px;
  overflow: hidden; display: grid; place-items: center;
  background: rgba(15,23,42,0.04); border: 1px solid rgba(15,23,42,0.10); }
.kv-secover__frame img { width: 100%; height: 100%; object-fit: cover; display: block; }
.kv-secover__empty { opacity: .45; }

.kv-segrid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 14px 18px; }
.kv-sefield { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.kv-sefield > label { color: var(--text-muted); font-size: 11.5px;
  text-transform: uppercase; letter-spacing: .05em; font-weight: 700; }
.kv-sereq { color: #DC2626; }
.kv-seinput { width: 100%; box-sizing: border-box; padding: 10px 12px;
  background: var(--surface-2); border: 1px solid var(--border-strong);
  border-radius: 10px; color: var(--text); font-size: 14px; font-family: inherit; }
.kv-seinput--area { resize: vertical; min-height: 84px; }

/* The build line under the sidebar level. Deliberately quiet: it is a
   diagnostic the owner reads on purpose, not something a student needs to
   notice. `user-select: text` so it can be read out or copied into a bug
   report, which is the whole reason it exists. */
.sidebar-build {
  padding: 2px 2px 0;
  color: var(--text-dim);
  font-size: 10.5px;
  line-height: 1.4;
  letter-spacing: 0.01em;
  user-select: text;
  -webkit-user-select: text;
}
.sidebar-build[hidden] { display: none !important; }
.kv-sehint, .kv-sefield small { color: var(--text-dim); font-size: 11.5px; }
/* One row, and it scrolls sideways on a phone instead of wrapping into a
   tall stack. The button is last and never shrinks, so it stays reachable at
   the end of the scroll. */
.kv-setagrow { display: flex; align-items: center; gap: 10px; min-width: 0;
  overflow-x: auto; overflow-y: hidden; padding-bottom: 2px;
  scrollbar-width: thin; -webkit-overflow-scrolling: touch; }
.kv-setagrow > .kv-sebtn { flex: 0 0 auto; align-self: center; }
.kv-setags { display: flex; flex-wrap: nowrap; gap: 6px; color: var(--text-muted);
  font-size: 13px; line-height: 1.5; min-height: 22px; white-space: nowrap; }

.kv-secard__head--cards { margin-bottom: 12px; }
.kv-setools { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.kv-seinput--search { width: 220px; padding: 8px 12px; font-size: 13px; }
.kv-seinput--sort { width: auto; padding: 8px 12px; font-size: 13px; cursor: pointer; }
.kv-setip { background: rgba(37,99,235,0.06); border: 1px solid rgba(37,99,235,0.20);
  border-radius: 10px; padding: 10px 12px; margin-bottom: 14px;
  color: var(--text-muted); font-size: 12.5px; line-height: 1.5; }
.kv-setip code { background: rgba(37,99,235,0.10); padding: 1px 5px;
  border-radius: 4px; color: #3B82F6; }

/* The table. The head row and every card row share one grid template, which
   is what keeps the headings over their columns. */
.kv-serow { display: grid; grid-template-columns: 56px minmax(0, 1fr) minmax(0, 1.7fr) 116px;
  gap: 10px; align-items: start; padding: 10px 12px;
  border: 1px solid rgba(15,23,42,0.06); border-radius: 10px;
  background: var(--surface-2); margin-bottom: 8px; }
.kv-serow--head { background: transparent; border: 0; padding: 0 12px 6px;
  margin-bottom: 4px; color: var(--text-muted); font-size: 11px;
  text-transform: uppercase; letter-spacing: .06em; font-weight: 700; }
.kv-serow--head span:last-child { text-align: center; }
.kv-serow__n { display: flex; align-items: center; gap: 7px; padding-top: 9px;
  color: var(--text-dim); font-size: 12px; font-weight: 700;
  font-variant-numeric: tabular-nums; }
.kv-segrip { color: var(--text-dim); opacity: .5; font-size: 14px; line-height: 1; }
.kv-seacts { display: flex; align-items: center; justify-content: center; gap: 4px;
  padding-top: 4px; }
/* ⚠️ 36px, because the image slot holds a 34px thumbnail that another
   test pins at that size — the box accommodates the thumb rather than the
   thumb being shrunk to fit the box. */
.kv-seact, .kv-seact-slot { display: inline-flex; align-items: center;
  justify-content: center; width: 36px; height: 36px; flex: 0 0 auto; padding: 0;
  background: transparent; border: 0; border-radius: 8px; cursor: pointer;
  color: var(--text-dim); font-size: 17px; line-height: 1; }
.kv-seact:hover { background: rgba(37,99,235,0.08); }
.kv-seact.is-on { color: #F59E0B; }
.kv-seact--danger:hover { background: rgba(220,38,38,0.08); color: #DC2626; }
.kv-seempty { padding: 26px 14px; text-align: center; color: var(--text-dim);
  font-size: 13px; }
.kv-seempty[hidden] { display: none; }
.kv-seaddwrap { display: flex; justify-content: center; margin-top: 14px; }
.kv-seadd { background: rgba(37,99,235,0.08); color: #2563EB;
  border: 1px dashed rgba(37,99,235,0.45); padding: 10px 24px; border-radius: 10px;
  cursor: pointer; font: inherit; font-size: 13px; font-weight: 700; }
.kv-sestatus { font-size: 13px; min-height: 18px; margin-bottom: 10px; }

/* The save bar. Sticks to the BOTTOM of the viewport while the form scrolls.
   ⚠️ sticky, not fixed: fixed would escape .main-panel and sit over the
   sidebar on desktop. */
.kv-sebar { position: sticky; bottom: 0; z-index: 20;
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; flex-wrap: wrap;
  margin: 0 calc(-1 * var(--kv-bleed-x, 0px));
  padding: 12px calc(var(--kv-bleed-x, 0px) + 4px)
           calc(12px + env(safe-area-inset-bottom, 0px));
  background: color-mix(in srgb, var(--bg-card) 92%, transparent);
  -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px);
  border-top: 1px solid var(--border-strong); }
.kv-sebar__meta { display: flex; align-items: center; gap: 12px;
  color: var(--text-muted); font-size: 13px; font-weight: 600; }
.kv-sedirty { color: #EA580C; font-weight: 700; }
.kv-sedirty[hidden] { display: none; }
.kv-sebar__actions { display: flex; gap: 10px; }

@media (max-width: 860px) {
  /* The definition needs the width more than the columns need to line up.
     🚨 (2026-10-03) The actions are PLACED, not left to auto-placement: with
     the definition spanning the row, auto-placement dropped them into a THIRD
     row's 40px number column, where 116px of buttons spilled out both sides.
     Row 1 is number · term · actions; row 2 is the definition, full width. */
  .kv-serow { grid-template-columns: 40px minmax(0, 1fr) auto; row-gap: 8px; }
  .kv-serow--head { display: none; }
  .kv-serow .kv-serow__n { grid-column: 1; grid-row: 1; }
  .kv-serow .mmEduTermWrap { grid-column: 2; grid-row: 1; min-width: 0; }
  .kv-serow .kv-seacts { grid-column: 3; grid-row: 1; }
  .kv-serow .mmEduDefWrap { grid-column: 1 / -1; grid-row: 2; }
  .kv-secover { float: none; margin: 0 0 14px; align-items: flex-start; }
  .kv-seinput--search { width: 100%; }
  .kv-setools { width: 100%; }
}

/* ── Deck + quiz editors on a PHONE (2026-10-03) ─────────────────────
   Owner: "can you update the deck creator to better render on mobile".
   Everything here is layout only; no field, id or control changes. */
@media (max-width: 640px) {
  /* Room for a two-line save bar, so the last card is never under it. */
  .kv-setedit { padding-bottom: 150px; }
  .kv-sehead { gap: 10px; margin-bottom: 14px; }
  .kv-sehead__text { flex-basis: 100%; }
  .kv-sehead__text h2 { font-size: 20px; }
  /* The sticky bar at the bottom already holds Save, always in reach; a
     second Save in the header just crowds the first screen. */
  #mmEduSaveTopBtn, #mmEduQuizSaveTopBtn,
  #kvCreatorSubmitTopBtn, #kvCreatorQuizSubmitTopBtn { display: none; }
  .kv-secard { padding: 14px; border-radius: 12px; margin-bottom: 14px; }
  .kv-segrid { grid-template-columns: minmax(0, 1fr); gap: 12px; }
  .kv-secover__frame { width: 112px; height: 112px; }

  /* Card rows: narrower number column and tighter actions give the term
     field the width. */
  .kv-serow { grid-template-columns: 26px minmax(0, 1fr) auto; gap: 8px;
    padding: 10px; }
  .kv-serow .kv-segrip { display: none; }
  .kv-seacts { gap: 0; padding-top: 2px; }
  /* Search on its own line; sort and Add card share the next one. */
  .kv-setools > .kv-seinput--sort, .kv-setools > .kv-sebtn { flex: 1 1 0; justify-content: center; }
  /* The formatting toolbar: six equal, finger-sized buttons across the
     full width (their size is set inline, hence !important). */
  .mmEduCardToolbar .mmEduMdBtn { flex: 1 1 0 !important; padding: 9px 0 !important; }

  /* The save bar: count on one line, then Cancel and Save side by side,
     full width, instead of wrapping unpredictably. */
  .kv-sebar { gap: 8px; }
  .kv-sebar__meta { width: 100%; justify-content: space-between; font-size: 12px; }
  .kv-sebar__actions { width: 100%; }
  .kv-sebar__actions > .kv-sebtn { flex: 1 1 0; justify-content: center; }

  /* Quiz answer choices: "Correct", the picture and remove on the first
     line, the answer text full width under them (the row's grid is set
     inline, hence !important). */
  .mmEduChoiceRow { grid-template-columns: auto minmax(0, 1fr) auto auto !important; row-gap: 8px !important; }
  .mmEduChoiceRow > label { grid-column: 1; grid-row: 1; }
  .mmEduChoiceRow > .mmEduCardImg { grid-column: 3; grid-row: 1; }
  .mmEduChoiceRow > .mmEduRemoveChoiceBtn { grid-column: 4; grid-row: 1; }
  .mmEduChoiceRow > .mmEduChoiceText { grid-column: 1 / -1; grid-row: 2; }
}
/* The website's "Get the Knovli app" bar is fixed to the bottom of a phone
   screen, exactly where the editor's sticky Save bar lives — it hid Save.
   Not while a deck or quiz editor is open; it comes back on every other page.
   (The bar's own CSS is injected by app.js, so this targets its id.) */
body:has(.kv-setedit) #mmEduInstallBanner { display: none !important; }

/* The Progress page's streak block: heading OUTSIDE, card is the dots — the
   same shape .cc-xpchart uses, so the two cards' top edges line up. The
   8px matches .cc-xpchart > .cc-section-head exactly; any other value and the
   two cards start at different heights again. */
.cc-streakblock > .cc-section-head { margin-bottom: 8px; }
/* The dots card carries the padding the section used to. 16px top matches
   .cc-xpchart-bars, so the first row of content sits on the same line. */
.cc-streakblock .cc-streakcard { padding: 16px 16px 12px; }

/* XP this week and Streak, side by side (design/progressMockup.png). Two
   INDEPENDENT sections in one row, so either can hide on its own without
   leaving a hole — the chart hides when there is no data and the streak card
   is deliberately shown even at zero. */
.cc-progrow {
    display: grid;
    grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr);
    gap: 16px;
    align-items: start;
    margin-bottom: 18px;
}
/* The children carry their own margins for the single-column case; inside the
   row the grid gap owns the spacing. */
.cc-progrow > * { margin-bottom: 0; }
@media (max-width: 900px) {
    /* Stacked: at this width the seven day circles and the seven bars both
       need the full column, and side by side each becomes unreadable. */
    .cc-progrow { grid-template-columns: 1fr; }
}

/* A challenge that names a quiz shows it, so the player can start it from
   here. The card itself is .kv-feat, shared with the Play dashboard's rails —
   only the slot around it is local. */
.kv-chall-play:empty { display: none; }
.kv-chall-play { margin-top: 12px; }
.kv-chall-playlead {
    margin: 0 0 8px; color: var(--text-muted); font-size: 12px; font-weight: 700;
    text-transform: uppercase; letter-spacing: .05em;
}
/* One card, so it must not stretch the full width of the challenge and read
   like a banner — the same reservation .kv-chall-row makes for a lone card. */
.kv-chall-play .kv-feat { max-width: 320px; }

/* ── Progress page (design/progressMockup.png) ────────────────
   The band itself is .kv-dhero, shared with My Decks / Explore / Classes —
   nothing is re-declared here. Only what this page adds. */

/* The level + rank chips became their own CARD under the band, where the
   mockup has them. .kv-standing on its own right-aligns into a hero corner;
   this variant is a full-width surface that reads as a row. */
.kv-standing--row {
    justify-content: flex-start;
    background: var(--bg-card);
    border: 1px solid rgba(15,23,42,0.10);
    border-radius: 14px;
    padding: 14px 16px;
    margin: 0 0 16px;
}

.kv-mastery .cc-section-title { display: flex; align-items: center; gap: 8px; }
.kv-mastery-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
    gap: 12px;
}
.kv-mastery-card {
    display: flex; align-items: stretch; gap: 0; text-align: left; width: 100%;
    background: var(--bg-card); border: 1px solid rgba(15,23,42,0.10);
    border-radius: 12px; overflow: hidden; cursor: pointer;
    font: inherit; color: inherit; padding: 0;
}
.kv-mastery-card:hover { border-color: rgba(37,99,235,0.45); }
.kv-mastery-card:focus-visible { outline: 3px solid var(--accent-blue, #2563EB); outline-offset: 2px; }
/* A fixed cover column, so a grid of these lines up whatever the artwork is. */
.kv-mastery-cover { flex: 0 0 74px; line-height: 0; background: rgba(15,23,42,0.04); }
.kv-mastery-cover img { width: 74px; height: 100%; object-fit: cover; display: block; }
.kv-mastery-body { display: flex; flex-direction: column; gap: 4px; padding: 10px 12px; min-width: 0; flex: 1 1 auto; }
.kv-mastery-name {
    color: var(--text); font-weight: 700; font-size: 13.5px;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.kv-mastery-count { color: var(--text-muted); font-size: 12px; }
.kv-mastery-barrow { display: flex; align-items: center; gap: 8px; margin-top: 2px; }
.kv-mastery-bar {
    flex: 1 1 auto; height: 6px; border-radius: 999px;
    background: rgba(15,23,42,0.10); overflow: hidden;
}
.kv-mastery-bar > span {
    display: block; height: 100%; border-radius: 999px;
    background: linear-gradient(90deg, #2563EB, #8B5CF6);
}
.kv-mastery-pct { color: var(--text-muted); font-size: 12px; font-weight: 700; flex: 0 0 auto; }
:root:not([data-theme="light"]) .kv-mastery-bar { background: rgba(148,163,184,0.22); }
:root[data-theme="dark"] .kv-mastery-bar { background: rgba(148,163,184,0.22); }

@media (max-width: 640px) {
    /* One per row: at 230px minimum two columns squeeze the deck name to
       nothing, and the name is the only thing identifying the card. */
    .kv-mastery-grid { grid-template-columns: 1fr; }
}

/* ── Dragging a deck into a folder ────────────────────────────
   The drop targets only announce themselves WHILE a deck is being dragged —
   a dashed outline on every folder card at rest would read as an error
   state. body.kv-dragging-deck is the switch. */
.kv-dragging { opacity: .45; }
body.kv-dragging-deck .kv-droptarget {
  outline: 2px dashed rgba(37,99,235,0.45); outline-offset: 2px;
}
body.kv-dragging-deck .kv-droptarget.kv-dropover {
  outline: 2px solid #2563EB; outline-offset: 2px;
  background: rgba(37,99,235,0.10);
}
/* A folder card is the drop target itself, so its children must not swallow
   the dragover — an <img>, a <span> or the Share button inside it would
   otherwise flicker the highlight as the pointer crosses them. */
body.kv-dragging-deck .kv-droptarget * { pointer-events: none; }

/* ── Quiz EDITOR (design/quizEditMockUp.png) ──────────────────
   The quiz editor wears the SET editor's shell — .kv-setedit and every kv-se*
   rule above dress both, so the two pages cannot drift apart. Only what a
   question card needs beyond a card row lives here. */

/* The cover sits in the details card exactly where the deck's does; it is the
   same _renderImgCell host, so it styles the control rather than replacing it. */
.kv-quizedit .kv-secover .mmEduCardImg img,
.kv-quizedit .kv-secover .mmEduCardImgBtn { border-radius: 12px; }
.kv-quizedit .kv-secover { align-items: flex-start; }

/* Class-share checkboxes, revealed only for "class" visibility. */
.kv-seclasses { display: flex; flex-direction: column; gap: 8px; margin-top: 8px; }
.kv-seclasses[hidden] { display: none; }

.kv-qrows { display: flex; flex-direction: column; gap: 14px; }
.kv-qcard { background: var(--surface-2); border: 1px solid rgba(15,23,42,0.08);
  border-radius: 12px; padding: 14px; }
.kv-qcard[hidden] { display: none; }
.kv-qhead { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; }
.kv-qnum { color: var(--text-dim); font-size: 12px; font-weight: 700;
  font-variant-numeric: tabular-nums; flex: 0 0 auto; }
.kv-qmore { background: transparent; border: 0; padding: 0 6px; cursor: pointer;
  color: var(--text-dim); font-size: 18px; line-height: 1; font-family: inherit; }
.kv-qmore:hover { color: var(--text); }
/* The ⋯ is pushed to the far end of the header row. */
.kv-qhead > .kvMenuWrap { margin-left: auto; }

/* The type control: two halves of ONE pill, per the mockup. */
.kv-qseg { display: inline-flex; background: var(--bg-card);
  border: 1px solid var(--border-strong); border-radius: 10px; padding: 3px; gap: 3px; }
.kv-qseg__btn { background: transparent; border: 0; border-radius: 8px;
  padding: 6px 14px; cursor: pointer; font: inherit; font-size: 12.5px;
  font-weight: 600; color: var(--text-muted); white-space: nowrap; }
.kv-qseg__btn:hover { color: var(--text); }
.kv-qseg__btn.is-on { background: linear-gradient(135deg, #2563EB, #8B5CF6);
  color: #fff; font-weight: 700; }

.kv-qlabel { display: block; color: var(--text-muted); font-size: 11.5px;
  text-transform: uppercase; letter-spacing: .05em; font-weight: 700;
  margin: 0 0 6px; }
/* Prompt + its image control, side by side. */
.kv-qprompt { display: grid; grid-template-columns: minmax(0, 1fr) auto;
  gap: 10px; align-items: start; margin-bottom: 12px; }
.kv-qprompt .kv-seinput--area { min-height: 66px; }
.kv-quizedit .mmEduMcArea, .kv-quizedit .mmEduTfArea { margin-top: 2px; }

@media (max-width: 640px) {
  /* The prompt needs the width more than the picker needs to sit beside it. */
  .kv-qprompt { grid-template-columns: minmax(0, 1fr); }
  .kv-qseg { width: 100%; }
  .kv-qseg__btn { flex: 1 1 0; text-align: center; }
}

/* ── 🎓 Creator mode of the deck + quiz editors (Creator Studio) ──────────
   The SAME editors as everyone else, plus an AI bar above the details card,
   a "Creator Studio" eyebrow and Save draft / Submit for review. Folder and
   visibility are hidden with [hidden] — the save handler still reads them —
   and .kv-sefield's display:flex would otherwise beat the UA [hidden] rule. */
.kv-sefield[hidden] { display: none; }
.kv-seeyebrow { color: #7C3AED; font-size: 11.5px; font-weight: 800;
  letter-spacing: .06em; text-transform: uppercase; margin: 0 0 2px; }
.kv-creator-aibar { display: flex; align-items: center; justify-content: space-between;
  gap: 14px; flex-wrap: wrap;
  background: linear-gradient(135deg, rgba(37,99,235,0.07), rgba(139,92,246,0.10));
  border-color: rgba(139,92,246,0.30); }
.kv-creator-aibar__text { flex: 1 1 260px; min-width: 0; }
.kv-creator-aibar__text h3 { margin: 0; color: var(--text); font-size: 16px; font-weight: 800; }
.kv-creator-aibar__text p { margin: 3px 0 0; color: var(--text-muted); font-size: 13px;
  line-height: 1.45; }
/* Three buttons (Cancel, Save draft, Submit) must WRAP on a phone rather
   than push the page sideways. */
.kv-creator-mode .kv-sebar__actions,
.kv-creator-mode .kv-sehead__actions { flex-wrap: wrap; justify-content: flex-end; }
@media (max-width: 640px) {
  .kv-creator-aibar .kv-sebtn { width: 100%; justify-content: center; white-space: normal; }
  .kv-creator-mode .kv-sebar__actions { width: 100%; }
  .kv-creator-mode .kv-sebar__actions .kv-sebtn { flex: 1 1 auto; justify-content: center; }
}
/* The Create tab's "New deck" / "New quiz" choices. */
.kv-cs-newgrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 12px; margin-bottom: 12px; }
.kv-cs-newtile { display: flex; flex-direction: column; align-items: flex-start; gap: 4px;
  text-align: left; cursor: pointer; font: inherit; padding: 16px;
  background: var(--bg-card); border: 1px solid var(--border-strong); border-radius: 12px; }
.kv-cs-newtile:hover { border-color: #8B5CF6; box-shadow: 0 6px 18px rgba(15,23,42,0.08); }
.kv-cs-newtile__icon { font-size: 26px; line-height: 1; }
.kv-cs-newtile__title { color: var(--text); font-weight: 800; font-size: 15px; }
.kv-cs-newtile__sub { color: var(--text-muted); font-size: 12.5px; line-height: 1.4; }

/* ── Set page: "Study your way" + Mastery plan (design/setViewMockup.png) ── */
.kv-secardplain { background: var(--bg-card); border: 1px solid rgba(15,23,42,0.10);
  border-radius: 14px; padding: 18px 20px; margin-bottom: 18px; }
.kv-modes__title { margin: 0; color: var(--text); font-size: 20px; font-weight: 800;
  letter-spacing: -.01em; }
.kv-modes__sub { margin: 4px 0 16px; color: var(--text-muted); font-size: 13.5px; }
.kv-modes { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 14px; }

.kv-mode { display: flex; flex-direction: column; align-items: center; text-align: center;
  gap: 8px; padding: 22px 16px 18px; border-radius: 14px; cursor: pointer;
  border: 1px solid rgba(15,23,42,0.06);
  /* The sampled tint at the top, clearing to the card beneath. */
  background: linear-gradient(180deg, var(--kv-mode-tint, #EEF2FF) 0%,
    color-mix(in srgb, var(--kv-mode-tint, #EEF2FF) 22%, var(--bg-card)) 100%);
  transition: transform 140ms ease, box-shadow 140ms ease; }
.kv-mode:hover { transform: translateY(-2px); box-shadow: 0 10px 26px rgba(15,23,42,0.10); }
.kv-mode.is-soon { cursor: default; opacity: .6; }
.kv-mode.is-soon:hover { transform: none; box-shadow: none; }
.kv-mode__art { width: 46px; height: 46px; object-fit: contain; border-radius: 10px; }
.kv-mode__emoji { font-size: 38px; line-height: 1; }
.kv-mode__name { color: var(--text); font-weight: 800; font-size: 15.5px; }
.kv-mode__desc { color: var(--text-muted); font-size: 12.5px; line-height: 1.45;
  flex: 1 1 auto; }
.kv-mode__count { color: var(--text-dim); font-size: 12px; font-weight: 600; }
.kv-mode__start { display: block; width: 100%; margin-top: 4px; padding: 9px 12px;
  border-radius: 9px; color: #fff; font-size: 13.5px; font-weight: 700; }
.kv-mode__soon { color: var(--text-dim); font-size: 12px; font-weight: 600;
  padding: 9px 0 0; }

/* 📱 PHONE: two tiles per screen, swipe sideways for the rest.
   Owner, 2026-10-02. The auto-fit grid collapses to ONE tile per row under
   ~420px, so a deck with six study modes pushed the terms list most of a
   screen down. A rail keeps the whole set reachable in one gesture.

   🚨 THIS BLOCK MUST STAY BELOW THE .kv-mode__* RULES ABOVE. A media
   query carries no extra specificity, so when it sat higher in the file the
   plain `.kv-mode__start { margin-top: 4px }` below it won and the CTA pin
   silently did nothing — measured as an 18px step between two tiles' Start
   buttons, not visible enough to catch by eye. */
@media (max-width: 720px) {
  /* `grid-auto-flow: column` + explicit `grid-auto-columns` is what makes it
     a RAIL; auto-fit alone would still wrap. Each tile is just under half the
     width so the next one is clipped — the standard "there is more" cue. */
  .kv-modes { grid-auto-flow: column;
    grid-template-columns: none;
    /* Two tiles plus a SLIVER of the third. Sized to exactly half and the
       third sits fully off-screen, so nothing tells you the rail scrolls —
       measured 1.92 tiles visible, i.e. two and nothing else. */
    grid-auto-columns: calc((100% - 14px) / 2 - 14px);
    overflow-x: auto; overflow-y: hidden;
    scroll-snap-type: x proximity;
    padding-bottom: 4px;
    scrollbar-width: none; }
  .kv-modes::-webkit-scrollbar { display: none; }
  .kv-modes > * { scroll-snap-align: start; }
  .kv-mode { padding: 18px 12px 14px; }
  /* At ~145px wide a description runs to five or six lines and buries the
     Start button. The name and the term count carry the tile here. */
  .kv-mode__desc { display: none; }
  /* 🔑 WITHOUT THIS THE BUTTONS DO NOT LINE UP: `flex:1 1 auto` lived on
     the description, so hiding it removed the flexible spacer and tiles
     packed to the top — a two-line name like "Multiple Choice" dropped its
     Start a row below its neighbour's. */
  .kv-mode__start, .kv-mode__soon { margin-top: auto; }
}

/* ── "Application questions" is a <details>, collapsed by default ──────
   Owner, 2026-10-02. The <summary> carries the existing h3 so the heading
   is unchanged when open; the marker is replaced by our own chevron
   because Safari and Chrome draw the native one differently. */
.kv-apply > summary { list-style: none; cursor: pointer; display: flex;
  align-items: center; gap: 10px; }
.kv-apply > summary::-webkit-details-marker { display: none; }
.kv-apply__chev { margin-left: auto; color: var(--text-muted); font-size: 15px;
  line-height: 1; transition: transform 160ms ease; }
.kv-apply[open] > summary .kv-apply__chev { transform: rotate(180deg); }
/* A closed <details> has no gap under the heading; an open one needs the
   same breathing room the section had before. */
.kv-apply[open] > summary { margin-bottom: 2px; }

/* Mastery plan rows in the right rail. */
.kv-side__more { background: none; border: 0; margin-left: auto; padding: 0 2px;
  cursor: pointer; color: var(--text-muted); font-size: 20px; line-height: 1; }
.kv-side__more:hover { color: var(--accent-blue, #2563EB); }
.kv-side__empty { margin: 0 0 12px; color: var(--text-muted); font-size: 13px;
  line-height: 1.5; }
.kv-side__cta { width: 100%; padding: 9px 12px; border-radius: 9px; cursor: pointer;
  font: inherit; font-size: 13px; font-weight: 700; color: #fff; border: 0;
  background: linear-gradient(135deg, #2563EB, #8B5CF6); }
.kv-mrow { display: flex; align-items: flex-start; gap: 11px; padding: 11px 12px;
  border-radius: 11px; background: color-mix(in srgb, var(--text-muted) 6%, transparent);
  margin-bottom: 8px; }
.kv-mrow:last-child { margin-bottom: 0; }
.kv-mrow.is-warn { background: rgba(249,115,22,0.10); }
.kv-mrow.is-good { background: rgba(16,185,129,0.10); }
.kv-mrow__ico { flex: 0 0 auto; font-size: 20px; line-height: 1.1; }
.kv-mrow__txt { min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.kv-mrow__txt b { color: var(--text); font-size: 13.5px; font-weight: 700; }
.kv-mrow__txt small { color: var(--text-muted); font-size: 12px; line-height: 1.45; }
.kv-mrow.is-warn b { color: #C2410C; }

/* ── EVERY dropdown stays on screen ───────────────────────────────────
   Owner, 2026-10-02: "double check drop downs to make sure they render on
   screen whether mobile or not", after the deck picker and the Progress
   class picker both ran off the edge.

   🔑 THE BUG IS ALWAYS THE SAME TWO LINES. A <select> sizes itself to its
   LONGEST OPTION, and as a flex item its default `min-width:auto` refuses
   to shrink below that — so it pushes out of its container instead of
   narrowing, and the container's own overflow decides whether you get a
   clipped control or a sideways-scrolling page. `max-width:100%` alone is
   not enough: it caps the box, while `min-width:0` is what actually lets
   the flex algorithm shrink it.

   Belt and braces, applied to every select in the app rather than per
   control, because the next one added would have the same bug and nobody
   would think to ask. dropdown-overflow.test.js derives the list of selects
   FROM THE SOURCE so a new one cannot escape this. */
select {
    max-width: 100%;
    min-width: 0;
    box-sizing: border-box;
    /* ── A dropdown is never meant to be see-through ──────────────
       🚨 Owner, 2026-10-03: "make the dropdowns not be transparent like we
          had to do for the buttons … on mobile there is too much bleed
          through." Same cause as #3846, one control type along: 18 `<select>`
          elements in app.js declare NO background at all — not inline, not
          through any class — so they fall back to the browser's default,
          which on a phone sits over whatever artwork or card is behind them.

       🔑 UNLIKE `.btn-secondary`, THIS ONE IS SAFE TO DO GLOBALLY. That fix
          had to be scoped to the hero bands because a transparent button is
          CORRECT on the dark topbar. No select in this app asks to be
          see-through — audited, and none declares `background: transparent`
          — so the default belongs on the element itself.

       ⚠️ `background-color`, not `background`: a future select that wants a
          gradient or an arrow image keeps it and only gains a solid base.
       ⚠️ `color` comes with it. Painting the box dark in dark mode and
          leaving the text at the UA default is how you trade a see-through
          control for an unreadable one. */
    background-color: var(--bg-card);
    color: var(--text);
}
/* …and the flex/grid parents that hold one, for the same reason: a flex
   ITEM that cannot shrink pushes its parent wide even when the select
   itself would have obliged. */
.kv-lb-classpick,
.kv-deckopt { min-width: 0; }
/* 🚨 `:has()` IS ON ITS OWN LINE DELIBERATELY. An unsupported selector
   anywhere in a comma list invalidates the WHOLE RULE, so putting this with
   the two above would have silently dropped them too on any engine without
   `:has()` — the named selectors are the ones that must not depend on it.
   This is the catch-all for labels nobody has named yet. */
label:has(> select) { min-width: 0; }

/* ── Deck page: the two mastery pickers ───────────────────────────────
   "How well do you want to know it?" and "Which kinds must count?".
   These were inline-styled `display:inline-flex`; the layout moved here so
   a media query can reach it at all (an inline declaration outranks any
   stylesheet rule short of !important). Desktop is unchanged. */
.kv-deckopt {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    font-size: 12px;
    color: var(--text-muted);
    /* Without this the label refuses to shrink below its content and pushes
       the select past the card's edge instead of narrowing. */
    min-width: 0;
    max-width: 100%;
}
.kv-deckopt__sel {
    background: var(--bg-card);
    border: 1px solid var(--border-strong);
    color: var(--text);
    border-radius: 8px;
    padding: 4px 8px;
    font-family: inherit;
    font-size: 12px;
    font-weight: 600;
    cursor: pointer;
    max-width: 100%;
    min-width: 0;
}

/* 📱 PHONE: the question on its own line, the dropdown under it, both
   inside the card. Owner, 2026-10-02: the select ran off the screen —
   its longest option ("Proven — …") is wider than a phone, and on one
   line with the label there was nowhere for it to go.
   🚨 This block must stay BELOW the two rules above: a media query adds no
   specificity of its own, so higher up they would simply win. */
@media (max-width: 720px) {
    .kv-deckopt {
        display: flex;
        flex-direction: column;
        align-items: stretch;
        gap: 4px;
        width: 100%;
    }
    .kv-deckopt__lbl { line-height: 1.3; }
    /* width, not just max-width: the select should fill the card rather than
       size itself to its longest option, which is what overflowed. */
    .kv-deckopt__sel { width: 100%; }
}

/* ── Set page: the terms list (design/setViewMockup.png) ────────────
   Index · term · definition · bucket pill. No per-term menu — owner,
   2026-09-19: "not sure what that would be for". */
.kv-terms { display: flex; flex-direction: column; gap: 8px; }
.kv-term { display: grid; grid-template-columns: 34px minmax(0, 1fr) minmax(0, 1.55fr) auto;
  gap: 14px; align-items: center; padding: 12px 16px;
  background: var(--bg-card); border: 1px solid rgba(15,23,42,0.10);
  border-radius: 12px; }
.kv-term.is-important { background: rgba(245,158,11,0.07);
  border-color: rgba(245,158,11,0.45); }
.kv-term__n { color: var(--text-dim); font-size: 12px; font-weight: 700;
  font-variant-numeric: tabular-nums; }
.kv-term__t { display: flex; align-items: center; gap: 6px; min-width: 0;
  color: var(--text); font-weight: 700; font-size: 14px; line-height: 1.4; }
.kv-term__d { display: flex; align-items: flex-start; gap: 10px; min-width: 0; }
.kv-term__dtext { flex: 1 1 auto; min-width: 0; color: var(--text-muted);
  font-size: 13.5px; line-height: 1.5;
  /* Two lines, like the mockup; the full text is on the card itself. */
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden; }
/* A definition with a picture in it is not clamped — two lines would cut the
   picture, not the words. */
.kv-term__dtext.has-img { display: block; overflow: visible; }
/* The pill IS the mastered toggle for a signed-in reader, so it is a button
   and has to look pressable without shouting. */
.kv-term__pill { display: inline-flex; align-items: center; gap: 7px;
  padding: 6px 13px; border-radius: 999px; white-space: nowrap;
  font: inherit; font-size: 12.5px; font-weight: 700; cursor: pointer;
  background: color-mix(in srgb, var(--text-muted) 8%, transparent);
  border: 1px solid transparent; color: var(--text-muted);
  transition: background 120ms ease, color 120ms ease; }
span.kv-term__pill { cursor: default; }
.kv-term__pill:hover { background: color-mix(in srgb, var(--text-muted) 15%, transparent); }
.kv-term__dot { width: 9px; height: 9px; border-radius: 50%; background: currentColor;
  flex: 0 0 auto; }
.kv-term__pill.is-notStarted { color: #94A3B8; }
.kv-term__pill.is-learning { color: #3B82F6;
  background: color-mix(in srgb, #3B82F6 10%, transparent); }
.kv-term__pill.is-familiar { color: #8B5CF6;
  background: color-mix(in srgb, #8B5CF6 10%, transparent); }
.kv-term__pill.is-mastered { color: #10B981;
  background: color-mix(in srgb, #10B981 12%, transparent); }
/* Read aloud. */
.kv-speak { flex: 0 0 auto; display: inline-flex; align-items: center;
  justify-content: center; width: 26px; height: 26px; padding: 0; cursor: pointer;
  background: transparent; border: 0; border-radius: 7px; opacity: .55;
  transition: opacity 120ms ease, background 120ms ease; }
.kv-speak:hover { opacity: 1; background: rgba(37,99,235,0.08); }
.kv-speak.is-speaking { opacity: 1; background: rgba(37,99,235,0.14); }
@media (max-width: 720px) {
  /* One column per row: the definition under the term, the pill on its own
     line. A 4-column grid at phone width clamps the definition to nothing. */
  .kv-term { grid-template-columns: 30px minmax(0, 1fr); row-gap: 8px; }
  .kv-term__d { grid-column: 2; }
  .kv-term__pill { grid-column: 2; justify-self: start; }
}

/* ── Set page: two columns + the right rail ─────────────────────────────
   design/setViewMockup.png. Study modes and the term list on the left,
   Your Progress and Set Actions beside them. */
.kv-setgrid { display: grid; grid-template-columns: minmax(0,1fr) 320px;
  gap: 20px; align-items: start; }
.kv-setgrid__main { min-width: 0; }
/* 🚨 STICKS BELOW THE TOPBAR, not to the viewport top. The topbar is
   `position: sticky; top: 0; z-index: 70`, so a rail pinned at `top: 16px`
   slid UNDER it and its heading was clipped away on scroll (owner,
   2026-09-19: "Your Progress (header) gets cutoff on scroll"). The bar's own
   height is published as --topbar-height; env() covers the iOS notch, which
   the bar also adds to itself. */
.kv-setgrid__side { display: flex; flex-direction: column; gap: 16px;
  position: sticky;
  top: calc(var(--topbar-height, 64px) + env(safe-area-inset-top, 0px) + 16px);
  /* 🔑 A rail TALLER than the gap below the bar cannot be scrolled to its
     end while it is pinned — the bottom would simply be unreachable. Bound it
     to what is actually visible and let it scroll itself instead. */
  max-height: calc(100vh - var(--topbar-height, 64px) - env(safe-area-inset-top, 0px) - 32px);
  overflow-y: auto; }
@media (max-width: 1040px) {
  /* 🚨 ONE column, and the rail goes FIRST. Left where it sits in source
     order it would land under the whole term list, which on a phone is the
     one place nobody scrolls to. */
  .kv-setgrid { grid-template-columns: minmax(0,1fr); }
  /* 🚨 UNDOING `position: sticky` IS NOT ENOUGH — `max-height` AND `overflow`
     BELONG TO IT AND WERE LEFT BEHIND (owner, 2026-10-02: "the page extends
     outside of the window causing the page to horizontally scroll on mobile").
     Both exist only to make a PINNED desktop rail scrollable. Once the rail is
     static they are wrong twice over:
       • it stayed a fixed ~748px-tall box on a phone, so the panels scrolled
         inside a window instead of flowing down the page;
       • and — the real bite — `overflow-y: auto` makes the computed
         `overflow-x` resolve from `visible` to `auto`, because a box cannot
         clip one axis and overflow the other. Measured on knovli.com at 390px:
         `overflow-x` came back `auto` with nothing in the CSS saying so.
         That turned the rail into its OWN horizontal scroller, so Progress,
         Mastery plan, Application questions and the actions were cut off at
         the right edge with no page-level overflow to point at.
     🔑 Reset a sticky rail's max-height and overflow together with its
     position, every time. `min-width: 0` so a wide child shrinks instead of
     forcing the column open. */
  .kv-setgrid__side {
    position: static; order: -1;
    max-height: none;
    overflow: visible;
    min-width: 0;
  }
}

.kv-side { background: var(--bg-card); border: 1px solid rgba(15,23,42,0.10);
  border-radius: 14px; padding: 16px 16px 14px; }
.kv-side__head { display: flex; align-items: center; margin-bottom: 12px; }
.kv-side__head h3 { display: flex; align-items: center; gap: 8px; margin: 0;
  color: var(--text); font-size: 15px; font-weight: 800; }

/* Progress: a conic-gradient ring driven by --pct, plus the four rows. */
.kv-prog { display: flex; align-items: center; gap: 14px; }
.kv-prog__dial { --pct: 0; flex: 0 0 auto; width: 96px; height: 96px;
  border-radius: 50%; display: grid; place-items: center;
  background: conic-gradient(#10B981 calc(var(--pct) * 1%), rgba(15,23,42,0.10) 0); }
.kv-prog__dial > span { width: 74px; height: 74px; border-radius: 50%;
  background: var(--bg-card); display: grid; place-items: center; text-align: center;
  line-height: 1.1; }
.kv-prog__dial b { display: block; color: var(--text); font-size: 18px; font-weight: 800; }
.kv-prog__dial small { display: block; color: var(--text-muted); font-size: 10.5px;
  font-weight: 600; }
.kv-prog__rows { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column;
  gap: 7px; }
.kv-prog__row { display: flex; align-items: center; gap: 8px; }
.kv-prog__dot { width: 10px; height: 10px; border-radius: 50%; flex: 0 0 auto; }
.kv-prog__dot.is-none { background: #CBD5E1; }
.kv-prog__dot.is-learning { background: #3B82F6; }
.kv-prog__dot.is-familiar { background: #8B5CF6; }
.kv-prog__dot.is-mastered { background: #10B981; }
.kv-prog__label { flex: 1 1 auto; color: var(--text-muted); font-size: 13px;
  font-weight: 600; }
.kv-prog__n { color: var(--text); font-size: 13px; font-weight: 700; }
.kv-prog__foot { margin-top: 12px; text-align: center; color: var(--text-dim);
  font-size: 12px; font-weight: 600; }

/* Set Actions: plain full-width rows, not buttons-that-look-like-buttons. */
.kv-setacts { display: flex; flex-direction: column; }
.kv-setact { display: flex; align-items: center; gap: 10px; width: 100%;
  background: transparent; border: 0; border-radius: 8px; padding: 9px 8px;
  cursor: pointer; font-family: inherit; font-size: 13.5px; font-weight: 600;
  color: var(--text); text-align: left; transition: background 120ms ease; }
.kv-setact svg { flex: 0 0 auto; }
.kv-setact:hover:not(:disabled) { background: rgba(37,99,235,0.07); }
.kv-setact:disabled { opacity: 0.55; cursor: default; }
.kv-setact.is-danger { color: #DC2626; }
.kv-setact.is-danger:hover:not(:disabled) { background: rgba(220,38,38,0.08); }

.kv-dhero {
    position: relative;
    /* 🚨 FULL-BLEED, not a card (owner, 2026-09-18): "it looks like they are to
       position just below the header bar and go full width with no rounded
       corners". So no radius, no border, and the container's own inset is
       cancelled on three sides — top, left and right — using the variables the
       container publishes. The CONTENT keeps the page gutter (see
       .kv-dhero-inner), so only the background runs to the edges. */
    border-radius: 0;
    border: 0;
    /* 🚨 SIDES always; the TOP only when the band is genuinely the first thing
       in its container. On My Decks the Flashcard Sets / Quizzes tab bar sits
       above it, and pulling the band up covered those buttons (owner,
       2026-09-18: "not aligned under the header bar… at a higher z index than
       the buttons"). */
    margin: 0 calc(-1 * var(--kv-bleed-x, 0px)) 22px;
    background-color: var(--bg-card);
    /* 🚨 THE BAND MUST READ AS A HERO. The scrim used to be a near-WHITE wash
       (rgba(248,250,255,…)) over pale artwork, so the whole band came out the
       same colour as the page behind it and the hero vanished — "aside from
       the homepage, there is no hero… the background image blends into the
       background" (owner, 2026-09-18).
       Three layers, top to bottom:
         1. the readability scrim, now TINTED blue rather than white and
            clearing to nothing on the right so the artwork still shows;
         2. a brand gradient across the WHOLE band — this is what makes it a
            distinct surface at the pale end of the artwork;
         3. the artwork itself.
       `--kv-dhero-art` is set inline where the band is rendered. */
    /* 🔑 Only the brand gradient lives on the element itself. The ARTWORK is a
       pseudo-element (::before) so it can carry its own MASK, and the
       readability scrim is another (::after) so it still paints over the
       picture. A single background-image list cannot mask one layer. */
    background-image:
        linear-gradient(135deg,
            rgba(37, 99, 235, 0.16) 0%,
            rgba(139, 92, 246, 0.15) 52%,
            rgba(6, 182, 212, 0.12) 100%);
    background-size: cover;
    /* Only the VERTICAL value does anything on the picture: a 3:1 image
       covering a ~6:1 band is scaled to the band's width, so it overflows
       downwards, not sideways. (`center right` was silently invalid — the
       order is horizontal then vertical.) */
    background-position: center;
    background-repeat: no-repeat;
    /* No shadow: a band that meets the page edge-to-edge does not float above
       it, and a shadow under a full-bleed strip reads as a seam. */
    /* 🚨 NO `overflow: hidden` HERE. The Create and Options menus are
       position:absolute and open BELOW their triggers, so hiding overflow
       CLIPPED BOTH OF THEM (owner, 2026-09-18) — the same trap as .cc-hero.
       Nothing needs it: border-radius already clips a background. */
}
/* THE ARTWORK, masked. Owner, 2026-09-18: "make the background image so that
   it goes from completely transparent bottom-left and then decreases
   transparency as it moves up and out" — so the picture fades out exactly
   where the words are and is fullest at the far top corner. */
.kv-dhero:first-child { margin-top: calc(-1 * var(--kv-bleed-top, 0px)); }
/* And when it is not first, it still needs clear air above it. */
.kv-dhero:not(:first-child) { margin-top: 6px; }

/* 🚨 THE CREATE MENU WAS PAINTING *UNDER* THE DECK CARDS, and it reads as a
   see-through menu rather than as a stacking bug — which is why three rounds of
   BACKGROUND fixes did nothing. The panel was always solid
   (`.kvMenu` → `var(--bg-card)`, measured opaque in both themes). Owner,
   2026-10-03, with a screenshot: the deck card's badge and title were drawn on
   top of the open "+ Flashcard Deck" panel.

   🔑 THE CAUSE IS ONE LINE ELSEWHERE: `.kv-dhero-inner` is
      `position: relative; z-index: 1`, which CREATES A STACKING CONTEXT. The
      menu's `z-index: 60` is therefore trapped inside the hero and can only
      compete with the cards as the HERO's level — and `.kvDeckTitle` is
      `position: relative; z-index: 1` later in the document, so it wins.
      A z-index only ever competes inside its own stacking context.

   🔑 Same shape, same fix as `.kv-sethero:has(.kvMenu:not([hidden]))` above:
      raise the WHOLE BAND while one of its menus is open, so the trapped
      context is lifted with it. Only while open, so the hero never sits above
      anything it should not. */
.kv-dhero:has(.kvMenu:not([hidden])) { z-index: 30; }
.kv-dhero::before,
.kv-dhero::after {
    content: '';
    position: absolute;
    inset: 0;
    pointer-events: none;
    border-radius: inherit;
}
.kv-dhero::before {
    background-image: var(--kv-dhero-art, none);
    background-size: cover;
    background-position: center;
    background-repeat: no-repeat;
    -webkit-mask-image: linear-gradient(to top right,
        rgba(0, 0, 0, 0) 0%,
        rgba(0, 0, 0, 0) 18%,
        rgba(0, 0, 0, 0.32) 44%,
        rgba(0, 0, 0, 0.78) 72%,
        rgba(0, 0, 0, 1) 100%);
    mask-image: linear-gradient(to top right,
        rgba(0, 0, 0, 0) 0%,
        rgba(0, 0, 0, 0) 18%,
        rgba(0, 0, 0, 0.32) 44%,
        rgba(0, 0, 0, 0.78) 72%,
        rgba(0, 0, 0, 1) 100%);
}
/* The readability scrim, ABOVE the picture — ::after paints after ::before.
   Lighter than before: the mask already clears the artwork off the text. */
.kv-dhero::after {
    background-image: linear-gradient(90deg,
        rgba(226, 236, 255, 0.72) 0%,
        rgba(228, 234, 255, 0.46) 38%,
        rgba(232, 234, 255, 0.12) 62%,
        rgba(237, 233, 254, 0) 80%);
}
/* 🚨 Above both pseudo-elements, or the picture covers the words. */
.kv-dhero-inner { position: relative; z-index: 1; }

/* Dark mode: the artwork is pale, so the veil has to carry across the whole
   band or light-on-light text and buttons become unreadable over it. */
[data-theme="dark"] .kv-dhero {
    background-image:
        linear-gradient(135deg,
            rgba(37, 99, 235, 0.30) 0%,
            rgba(139, 92, 246, 0.26) 52%,
            rgba(6, 182, 212, 0.20) 100%);
}
[data-theme="dark"] .kv-dhero::after {
    background-image: linear-gradient(90deg,
        rgba(7, 15, 40, 0.86) 0%,
        rgba(9, 17, 44, 0.62) 38%,
        rgba(12, 20, 52, 0.26) 62%,
        rgba(16, 22, 58, 0) 80%);
}
.kv-dhero-inner {
    /* 🔑 The band is full-bleed but its TEXT is not: put back EXACTLY the inset
       the band cancelled, so the title sits on the same gutter as the content
       below it. (Adding 22px on top pushed the title 22px further in than
       everything else — measured.) The floor covers a container that has not
       published a value. */
    padding-inline: max(var(--kv-bleed-x, 22px), 16px);
    /* Banner proportions, so the artwork has room to read as one. */
    min-height: clamp(150px, 15vw, 200px);
    position: relative;
    z-index: 1;
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 18px;
    padding-block: 20px;
    flex-wrap: wrap;
}
.kv-dhero-text { min-width: 0; flex: 1 1 320px; }
/* The Flashcard Sets / Quizzes pills ride ON the band, above the title — in the
   mockup they sit on the hero's background, which is why the band has to start
   at the header bar rather than below them. */
.kv-dhero-tabs { margin: 0 0 12px; }
.kv-dhero-tabs > * { margin-bottom: 0 !important; }
.kv-dhero-title { margin: 0; font-family: var(--font-display, inherit); font-size: clamp(1.4rem, 3vw, 1.9rem); font-weight: 800; color: var(--text); }
.kv-dhero-sub { margin: 4px 0 10px; color: var(--text-muted); font-size: 0.92rem; }
.kv-dhero-cap { margin-top: 8px; color: var(--text-muted); font-size: 0.8rem; }
.kv-dhero-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; position: relative; z-index: 2; }
.kv-dstats { display: flex; gap: 16px; flex-wrap: wrap; align-items: center; }
.kv-dstat { display: inline-flex; align-items: center; gap: 6px; color: var(--text); font-size: 0.86rem; font-weight: 700; }
.kv-dstat svg, .kv-dstat img { flex-shrink: 0; }
@media (max-width: 760px) {
    /* The artwork used to be dropped entirely here. The owner wants the RIGHT
       side of it — the side the illustration is actually on — to keep showing
       (2026-09-18). Anchoring the picture right does that: at this width the
       image is scaled to the band's HEIGHT and ends up far wider than the
       band, so the horizontal anchor is what decides which part you see, and
       `center` was showing the empty middle. The scrim leans harder to the
       left, where every line of text sits, and clears by the right edge. */
    /* The brand gradient stays (it is what makes the band a band); the
       picture anchors right, where the illustration is, and its mask is
       steeper so the narrow column of text stays clear of it. */
    .kv-dhero::before {
        background-position: right center;
        -webkit-mask-image: linear-gradient(to top right,
            rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0) 30%,
            rgba(0, 0, 0, 0.40) 62%, rgba(0, 0, 0, 1) 100%);
        mask-image: linear-gradient(to top right,
            rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0) 30%,
            rgba(0, 0, 0, 0.40) 62%, rgba(0, 0, 0, 1) 100%);
    }
    .kv-dhero-inner { padding-block: 18px; }
}

/* The scan / AI banner that closes the page. The ARTWORK is the banner — it
   carries its own heading, copy and button — so this is one picture-shaped
   button and nothing is drawn on top of it. */
.kv-dscan {
    display: block;
    width: 100%;
    margin-top: 22px;
    padding: 0;
    /* 🚨 NOT a card — just the picture (owner, 2026-09-18, twice: first the
       border went, then the rest of the container). No frame, no rounding, no
       hover lift, nothing behind it. The focus ring stays: it is still a
       button, and a keyboard user has to be able to see where they are. */
    border: 0;
    border-radius: 0;
    overflow: visible;
    background: none;
    box-shadow: none;
    cursor: pointer;
    /* 🚨 `appearance: none` as well as our own resets. A <button> carries the
       BROWSER's chrome — Safari in particular draws its own border, background
       and padding, which reads as a card with the picture floating inside it
       (owner, 2026-09-18, after our border was already gone). font-size/
       line-height 0 removes the text-node leading under the image. */
    -webkit-appearance: none;
    appearance: none;
    margin-bottom: 0;
    font-size: 0;
    line-height: 0;
    text-align: left;
    color: inherit;
    /* ✅ No crop here any more. The first file was 2172×724 with ~70% white
       padding baked in, and we cropped to its banner strip to hide it; the
       owner re-cropped the artwork instead (2168×234, tight, and 540KB
       smaller), so the workaround is gone. Left in, its 10.06:1 box would
       have clipped the new art's own 9.26:1 edges. */
}
.kv-dscan:focus-visible { outline: 2px solid var(--brand-blue, #3B82F6); outline-offset: 3px; }
.kv-dscan-bg { display: block; width: 100%; height: auto; }
/* The picture failed to load — most likely its Image Library URL was replaced.
   Show the wording instead of a blank space, so the entry point survives. */
.kv-dscan-text {
    position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
    overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.kv-dscan--broken {
    background: linear-gradient(135deg, #2563EB, #8B5CF6);
    border-radius: var(--radius-lg, 16px);
}
.kv-dscan--broken .kv-dscan-text {
    position: static; width: auto; height: auto; margin: 0; overflow: visible;
    clip: auto; white-space: normal;
    display: block; padding: 18px 22px;
    color: #fff; font-family: inherit; font-size: 15px; font-weight: 700;
    line-height: 1.4; text-align: left;
}

/* ── My Quizzes cards (owner, 2026-09-18) ────────────────────────────────
   The quiz's picture, which the editor has always saved but these cards
   never showed. Fixed band so a grid of cards lines up whatever shape the
   uploaded image is. */
.kv-qcard-cover {
    display: block;
    height: 124px;
    background: rgba(15, 23, 42, 0.04);
    line-height: 0;
}
.kv-qcard-cover img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

/* ── The quiz picker's filters, pinned (owner, 2026-09-18) ────────────────
   Source tabs, search, subjects and topics travel together so they can be
   used from anywhere in a long list. Pinned BELOW the topbar, which is itself
   sticky at 0 and 64px tall.
   🚨 This only works because `.main-panel` no longer sets `overflow-y: auto`
   — a scrolling ancestor becomes the sticky containing block and the bar
   would just scroll away. */
.kv-content-filters {
    position: sticky;
    top: calc(var(--topbar-height, 64px) + env(safe-area-inset-top, 0px));
    z-index: 40;
    /* A panel in its own right (owner, 2026-09-18): padded, rounded, and set
       off from the results below it. 🚨 The background must be OPAQUE — cards
       scroll underneath it while it is pinned. */
    background: var(--bg-card, #FFFFFF);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg, 16px);
    padding: clamp(12px, 1.5vw, 18px);
    /* 🚨 NO negative side margins. A full-bleed backing would have to guess
       the container's padding — `.kv-content` has none of its own, so the
       guess came from `.main-panel`, and at 1280 it overflowed the page by
       6px horizontally (measured). The bar spans its container instead. */
    margin: 0 0 clamp(18px, 2.6vh, 28px);
    /* 🔑 Column + gap rather than the children's own bottom margins: the topic
       row is `hidden` most of the time, and a margin on a display:none element
       still leaves the gap above it — `gap` skips it. */
    display: flex;
    flex-direction: column;
    gap: 10px;
    transition: box-shadow 0.15s ease;
}
/* Their own spacing would fight the gap above, and leave a dead strip at the
   bottom of the panel. */
.kv-content-filters > * { margin-bottom: 0; }
/* The results sit a little INSIDE the pinned bar's width, so nothing scrolling
   underneath shows an edge beside it (owner, 2026-09-18).
   🚨 The rails are 4px wider than their container — `.kv-row-grid` carries
   `margin: 0 -4px` so `overflow-x: auto` cannot clip the cards' hover lift and
   focus ring — so the inset has to clear that as well as the gap it adds. */
.kv-content-results { padding-inline: clamp(12px, 1.2vw, 18px); }

/* Only once it is actually pinned — an always-on shadow reads as a seam. */
/* Lifts only while pinned — an always-on shadow reads as a permanent seam.
   The border is always there now, so only the shadow changes. */
.kv-content-filters.is-stuck {
    box-shadow: 0 10px 24px rgba(15, 23, 42, 0.10);
}
[data-theme="dark"] .kv-content-filters.is-stuck { box-shadow: 0 6px 18px rgba(0, 0, 0, 0.35); }
/* The bar is tall on a phone; keep it from eating the screen. */
@media (max-width: 640px) {
    .kv-content-filters { max-height: 52vh; overflow-y: auto; }
}

/* ── Explore shelves (owner mockup design/exploreMockup.png, 2026-09-18) ──
   The rails reuse `.kv-row` / `.kv-row-grid` from the quiz picker, so the two
   discovery surfaces read as one design and there is one thing to maintain.
   Only the subject tiles are new. */
.kv-row-head h3 { display: flex; align-items: center; gap: 8px; }

/* A subject tile is not a card: it is a chunky, tappable label, so the rail
   sizes these by their own width rather than the card track. */
.kv-subjrail > * { flex: 0 0 auto; }
.kv-subjtile {
    display: flex; align-items: center; gap: 10px;
    padding: 14px 18px;
    border-radius: 14px;
    /* `--tile` is the subject's own hue, set inline from SUBJECT_STYLE. The
       fallback keeps an unclassified subject looking deliberate. */
    border: 1px solid rgba(var(--tile, 100, 116, 139), 0.30);
    background: linear-gradient(135deg, rgba(var(--tile, 100, 116, 139), 0.14),
                                        rgba(var(--tile, 100, 116, 139), 0.05));
    color: var(--text);
    font: inherit; font-weight: 700; font-size: 0.92rem;
    cursor: pointer; white-space: nowrap;
    transition: transform 0.14s ease, border-color 0.14s ease;
}
.kv-subjtile:hover { transform: translateY(-2px); border-color: rgba(var(--tile, 100, 116, 139), 0.65); }
.kv-subjtile:focus-visible { outline: 2px solid var(--accent-blue, #2563EB); outline-offset: 2px; }
.kv-subjtile-ico { font-size: 1.15rem; line-height: 1; }
.kv-subjtile-n {
    margin-left: 2px; padding: 2px 8px; border-radius: 999px;
    background: rgba(var(--tile, 100, 116, 139), 0.20);
    font-size: 0.78rem; font-weight: 800;
}

/* A shelf that has nothing to show says WHY — loading, failed, or genuinely
   empty — rather than rendering as a blank gap (owner, 2026-09-18: "the bottom
   half of the page doesn't load"). */
.kv-empty-note {
    display: flex; align-items: center; justify-content: space-between;
    gap: 14px; flex-wrap: wrap;
    padding: 18px 20px;
    border: 1px solid var(--border);
    border-radius: var(--radius-lg, 16px);
    background: var(--bg-card);
    color: var(--text-muted);
}
.kv-empty-note p { margin: 0; line-height: 1.5; }
.kv-empty-note b { color: var(--text); }

/* ── rewards Phase 4b: what a student holds (plan §6b) ────────────────── */
.kv-rws { list-style: none; margin: 0; padding: 0; display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 14px; }
.kv-rw { border: 1px solid var(--border, #E5E7EB); border-radius: 12px;
  padding: 14px 16px; background: var(--surface, #fff); }
.kv-rw-top { display: flex; align-items: center; justify-content: space-between;
  gap: 10px; }
.kv-rw-name { font-weight: 700; }
.kv-rw-state { font-size: .7rem; font-weight: 700; letter-spacing: .05em;
  text-transform: uppercase; padding: 3px 9px; border-radius: 999px;
  background: var(--border, #E5E7EB); color: var(--text-muted, #6B7280);
  white-space: nowrap; }
.kv-rw-state--earned { background: color-mix(in srgb, var(--success, #16A34A) 15%, transparent);
  color: var(--success, #16A34A); }
.kv-rw-state--requested { background: color-mix(in srgb, var(--accent-blue, #2563EB) 15%, transparent);
  color: var(--accent-blue, #2563EB); }
.kv-rw-goal { margin-top: 6px; font-size: .85rem; font-style: italic;
  color: var(--text-muted, #6B7280); }
.kv-rw-hint { margin-top: 6px; font-size: .84rem; color: var(--text-muted, #6B7280); }
.kv-rw-note { margin-top: 8px; font-size: .84rem; color: var(--text-muted, #6B7280);
  border-left: 3px solid var(--border, #E5E7EB); padding-left: 10px; }
.kv-rw-acts { margin-top: 12px; display: flex; gap: 8px; flex-wrap: wrap; }
.kv-rw-btn { font-size: .85rem; padding: 7px 13px; }
.kv-smodal-lead { margin: 0 0 12px; color: var(--text-muted, #6B7280); font-size: .9rem; }
.kv-smodal-acts { margin-top: 14px; display: flex; gap: 10px; justify-content: flex-end; }

/* ── goals a student is working toward (owner, 2026-09-21) ───────────── */
.kv-goals { list-style: none; margin: 0 0 18px; padding: 0; display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 14px; }
.kv-goal { border: 1px solid var(--border, #E5E7EB); border-radius: 12px;
  padding: 14px 16px; background: var(--surface, #fff); }
.kv-goal.is-done { border-color: var(--success, #16A34A); }
.kv-goal-top { display: flex; align-items: baseline; justify-content: space-between;
  gap: 10px; }
.kv-goal-name { font-weight: 700; }
.kv-goal-when { font-size: .78rem; color: var(--text-muted, #6B7280); white-space: nowrap; }
.kv-goal-earn { margin-top: 6px; font-size: .86rem; color: var(--text-muted, #6B7280); }
.kv-goal-bar { margin-top: 10px; height: 8px; border-radius: 999px;
  background: var(--border, #E5E7EB); overflow: hidden; }
.kv-goal-bar > span { display: block; height: 100%; border-radius: 999px;
  background: var(--accent-blue, #2563EB); transition: width .3s ease; }
.kv-goal.is-done .kv-goal-bar > span { background: var(--success, #16A34A); }
.kv-goal-crits { list-style: none; margin: 10px 0 0; padding: 0; display: flex;
  flex-direction: column; gap: 4px; }
.kv-goal-crit { font-size: .83rem; color: var(--text-muted, #6B7280); }
.kv-goal-crit.is-met { color: var(--success, #16A34A); }
.kv-goal-q { font-style: italic; }
.kv-goal-done { margin-top: 10px; font-size: .85rem; font-weight: 700;
  color: var(--success, #16A34A); }

/* ── Knovli Learn, read inside the app ─────────────────────────────────
   The same articles the site publishes at /learn/<slug>, rendered in the
   content panel. Uses the app's own tokens so it belongs to Knovli rather
   than looking like an embedded web page. */
/* 🔑 Full width (owner, 2026-09-22). The content panel supplies its own
   padding, so this just stops the 720px measure — the reader fills whatever
   the panel gives it, beside the sidebar. */
.kvLearnWrap { max-width: none; margin: 0; }
.kvLearnWrap h1 { font-family: var(--font-display); font-size: 1.6rem; margin: 0 0 6px; color: var(--text); }
.kvLearnMuted { color: var(--text-muted); font-size: 0.92rem; margin: 0 0 18px; }
.kvLearnList { list-style: none; margin: 0; padding: 0; }
.kvLearnList li { border-bottom: 1px solid var(--border); }
.kvLearnItem { display: flex; flex-direction: column; gap: 3px; width: 100%; text-align: left;
    background: none; border: 0; padding: 14px 2px; cursor: pointer; font-family: inherit; }
.kvLearnItem:hover { background: var(--surface-2); }
.kvLearnItemTitle { font-weight: 700; font-size: 1.02rem; color: var(--text); }
.kvLearnItemSum { color: var(--text-dim); font-size: 0.9rem; line-height: 1.45; }
.kvLearnItemMeta { color: var(--text-muted); font-size: 0.78rem; }
.kvLearnBack { background: none; border: 0; color: var(--text-muted); cursor: pointer;
    font-family: inherit; font-size: 0.86rem; padding: 2px 0 10px; }
.kvLearnBack:hover { color: var(--text); }
/* 🔑 On an article, "← All articles" stays in reach while reading (owner,
   2026-09-30: "when scrolling down … have the ← All Articles link be sticky
   under the header"). It sticks BELOW the topbar — the bar is itself
   `position: sticky; top: 0; z-index: 70`, so `top: 0` would slide it under
   the bar (the same trap the set page's rail hit, see .kv-setgrid__side). A
   full-width strip with the PAGE background, so the article scrolling
   beneath does not show through its text. */
.kvLearnArticle .kvLearnBack {
    position: sticky;
    top: calc(var(--topbar-height, 64px) + env(safe-area-inset-top, 0px));
    z-index: 5;
    display: block;
    width: 100%;
    text-align: left;
    background: var(--bg);
    padding: 10px 0;
    margin-bottom: 8px;
    box-shadow: 0 1px 0 var(--border);
}
.kvLearnByline { display: flex; align-items: center; gap: 10px; color: var(--text-muted);
    font-size: 0.86rem; margin: 0 0 18px; }
.kvLearnByline img { width: 40px; height: 40px; border-radius: 50%; object-fit: cover;
    flex: 0 0 40px; background: var(--surface-2); }
.kvLearnBody { color: var(--text); font-size: 1rem; line-height: 1.65; }
.kvLearnBody h2 { font-size: 1.25rem; margin: 1.5em 0 .4em; color: var(--text); }
.kvLearnBody h3 { font-size: 1.06rem; margin: 1.3em 0 .35em; color: var(--text); }
.kvLearnBody p { margin: 0 0 1em; }
.kvLearnBody ul, .kvLearnBody ol { margin: 0 0 1em; padding-left: 1.4em; }
.kvLearnBody li { margin: .3em 0; }
.kvLearnBody a { color: var(--accent, #2563EB); }
.kvLearnBody img { max-width: 100%; height: auto; border-radius: 10px; }
.kvLearnBody blockquote { margin: 0 0 1em; padding: .2em 0 .2em 1em;
    border-left: 3px solid var(--border-strong); color: var(--text-dim); }

/* Knovli Learn's in-app index reads like the published one: cards with covers
   and summaries, and a side menu of everything. One column on a phone, menu
   last — the writing comes before a list of links. */
/* 🚨 minmax(0,1fr), NOT 1fr — on the PHONE track too.
   Owner, 2026-10-02, with a screenshot: the featured card's image and summary
   ran to the right edge of a 440pt phone while the left kept its 16pt gutter.

   🔑 `1fr` is shorthand for `minmax(AUTO, 1fr)`, and an `auto` MINIMUM means
   the track may not shrink below its content's min-content width — which for
   a block containing a wide raster image is that image's intrinsic width. The
   column then grows past its container and the text rides out with it, losing
   the gutter on the overflow side only. That is exactly the shape in the
   screenshot.

   The tell that this was already known: BOTH wide breakpoints below use
   `minmax(0,1fr)` and the base did not — the desktop layout was fixed and the
   phone one left behind.

   ⚠️ NOT REPRODUCED LOCALLY. Measured at 390 and 440pt with the shell, the
   bleeding hero and a real raster, the card fit every time; the in-app Learn
   index needs sign-in, which is out of reach here. This is the cause the
   evidence points at and the change is strictly safer than `1fr`, but it wants
   confirming on a device. */
.kvLearnBlog { display: grid; grid-template-columns: minmax(0, 1fr); gap: 26px; }
/* …and the column's contents must be allowed to shrink inside it. A grid ITEM
   carries the same `min-width:auto` default that the track just lost. */
.kvLearnMain { min-width: 0; }
@media (min-width: 900px) { .kvLearnBlog { grid-template-columns: minmax(0,1fr) 200px; gap: 34px; } }
.kvLearnSec { font-size: 0.74rem; text-transform: uppercase; letter-spacing: .06em;
    color: var(--text-muted); margin: 0 0 8px; font-weight: 700; }
.kvLearnMain .kvLearnSec:not(:first-child) { margin-top: 26px; }
.kvLearnCards { list-style: none; margin: 0; padding: 0; }
.kvLearnCard { border-bottom: 1px solid var(--border); }
.kvLearnCard .kvLearnItem { flex-direction: row; gap: 14px; align-items: flex-start; }
.kvLearnThumb { flex: 0 0 116px; }
.kvLearnThumb img { width: 116px; height: 78px; object-fit: cover; border-radius: 10px;
    display: block; background: var(--surface-2); }
.kvLearnCardBody { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
/* 📱 The featured card must not out-grow the column it sits in.
   Owner, 2026-10-02: "the feature card is too wide … not sure if it's content
   or image". NOT REPRODUCED in desktop Chromium — measured at 390px, both the
   lifted card renderer in isolation and the live public /learn page fit (page
   scrollWidth == viewport, no element past the right edge). So this is the
   same DEFENSIVE pair applied everywhere else in this pass rather than a fix
   for a diagnosed overflow: a flex item's default `min-width: auto` is what
   lets long unbroken content — a title with no spaces, a wide image — push a
   card past its container instead of wrapping inside it.
   If it persists, the next step is a screenshot or the Safari inspector on the
   element that actually overhangs. */
.kvLearnCard--lead .kvLearnItem { flex-direction: column; min-width: 0; max-width: 100%; }
.kvLearnCard--lead .kvLearnCardBody { min-width: 0; }
.kvLearnCard--lead .kvLearnItemTitle,
.kvLearnCard--lead .kvLearnItemSum { overflow-wrap: anywhere; }
.kvLearnCard--lead .kvLearnThumb img { max-width: 100%; }
.kvLearnCard--lead { border-bottom: 0; }
.kvLearnCard--lead .kvLearnThumb { flex: none; width: 100%; }
.kvLearnCard--lead .kvLearnThumb img { width: 100%; height: 200px; object-fit: cover; }
.kvLearnCard--lead .kvLearnItemTitle { font-size: 1.25rem; line-height: 1.25; }
.kvLearnSide ul { list-style: none; margin: 0; padding: 0; }
.kvLearnSide li { border-bottom: 1px solid var(--border); }
.kvLearnSideLink { padding: 8px 2px; font-size: 0.9rem; color: var(--text); }
/* 🔥 Trending / Recently added — 4 small cards above the search box: 4 across
   on a wide screen, 2×2 in between, a swipeable row on a phone. */
.kvLearnTrendHead { margin-top: 26px; }
.kvLearnTrend { list-style: none; margin: 0; padding: 0; display: grid; gap: 14px;
    grid-template-columns: repeat(4, minmax(0, 1fr)); }
@media (max-width: 1100px) { .kvLearnTrend { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 600px) {
    .kvLearnTrend { display: flex; overflow-x: auto; scroll-snap-type: x mandatory; gap: 12px; padding-bottom: 6px; }
    .kvLearnTrend li { flex: 0 0 72%; scroll-snap-align: start; }
}
.kvLearnTrendCard { flex-direction: column; gap: 6px; height: 100%; padding: 0 0 10px; border-radius: 12px;
    background: var(--bg-card); border: 1px solid var(--border); overflow: hidden; }
.kvLearnTrendCard:hover { background: var(--bg-card); border-color: var(--brand-blue); }
.kvLearnTrendImg { display: block; width: 100%; aspect-ratio: 16 / 9; background: var(--surface-2); }
.kvLearnTrendImg img { width: 100%; height: 100%; object-fit: cover; display: block; }
.kvLearnTrendCard .kvLearnItemTitle { font-size: 0.92rem; line-height: 1.3; padding: 0 10px;
    display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
.kvLearnTrendCard .kvLearnItemMeta { padding: 0 10px; margin-top: auto; }
/* 🔍 Search under the featured article; results take "More to read"'s place.
   🏷️ The Topics cloud replaced the "All articles" list (owner, 2026-09-30). */
.kvLearnSearch { margin: 22px 0 6px; }
.kvLearnSearch input { width: 100%; padding: 12px 14px 12px 40px; font: inherit; font-size: 0.98rem;
    color: var(--text); background: var(--bg-input) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 24 24' fill='none' stroke='%2394A3B8' stroke-width='2.2' stroke-linecap='round'%3E%3Ccircle cx='11' cy='11' r='7'/%3E%3Cpath d='m20 20-3.5-3.5'/%3E%3C/svg%3E") no-repeat 13px 50%;
    border: 1px solid var(--border); border-radius: 12px; }
.kvLearnSearch input:focus { outline: none; border-color: var(--brand-blue); box-shadow: 0 0 0 3px rgba(37,99,235,.15); }
.kvLearnResHead { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; margin-top: 22px; }
.kvLearnResHead .kvLearnSec { margin: 0; }
.kvLearnResHead .kvLearnMuted { margin: 0; }
.kvLearnClear { margin-left: auto; background: none; border: 1px solid var(--border); border-radius: 999px;
    padding: 4px 12px; font: inherit; font-size: 0.82rem; color: var(--text-muted); cursor: pointer; }
.kvLearnClear:hover { color: var(--text); border-color: var(--text-muted); }
.kvLearnTags { display: flex; flex-wrap: wrap; gap: 6px 8px; align-items: baseline; }
.kvLearnTag { background: var(--surface-2); border: 1px solid transparent; border-radius: 999px; padding: 4px 11px;
    font: inherit; color: var(--text); cursor: pointer; line-height: 1.3; }
.kvLearnTag:hover { border-color: var(--brand-blue); }
.kvLearnTag--1 { font-size: 0.8rem; }
.kvLearnTag--2 { font-size: 0.9rem; }
.kvLearnTag--3 { font-size: 1.02rem; font-weight: 600; }
.kvLearnTag--4 { font-size: 1.15rem; font-weight: 700; }
.kvLearnTag.is-on { background: var(--brand-blue); color: #fff; border-color: var(--brand-blue); }
/* 🚨 The sticky menu must clear the FIXED TOPBAR. `top: 12px` sticks it 12px
   from the top of the VIEWPORT, which is underneath the 64px bar — the first
   few entries disappear behind it as you scroll. Offset by the bar's own
   variable (plus the notch), and cap the height so a long list scrolls
   itself instead of running off the bottom of the screen. */
@media (min-width: 900px) {
    .kvLearnSide {
        position: sticky;
        top: calc(var(--topbar-height) + env(safe-area-inset-top, 0px) + 12px);
        align-self: start;
        max-height: calc(100vh - var(--topbar-height) - env(safe-area-inset-top, 0px) - 32px);
        overflow: auto;
    }
}
/* 🚨 Capped — full width made a 16:9 cover a wall of image before the first
   sentence. `cover` crops to the cap instead of squashing the picture. */
.kvLearnHero { width: 100%; max-height: 200px; object-fit: cover; border-radius: 12px;
    margin: 0 0 18px; display: block; }

@media (min-width: 1200px) { .kvLearnBlog { grid-template-columns: minmax(0,1fr) 240px; gap: 44px; } }
@media (min-width: 1600px) {
    .kvLearnMain .kvLearnCards { display: grid; grid-template-columns: 1fr 1fr; gap: 0 28px; }
    .kvLearnMain .kvLearnCards .kvLearnCard--lead { grid-column: 1 / -1; }
}

/* ── A creator's public decks (/c/<handle>) ───────────────────────────
   The decks themselves render through Explore's card template
   (_renderCommunityGrid), so there is no second set of card rules here —
   the owner asked for these to look like the decks on Explore, and one
   template is the only way that stays true. Only the page furniture lives
   here. */
.kv-creator-empty { opacity: 0.75; margin: 18px 0; }
.kv-creator-foot { margin: 14px 0 0; }
.kv-linklike {
    background: none; border: 0; padding: 0; cursor: pointer; font: inherit;
    color: #4F46E5; text-decoration: underline;
}
:root:not([data-theme="light"]) .kv-linklike { color: #A5B4FC; }
@media (prefers-color-scheme: light) {
    :root:not([data-theme="dark"]) .kv-linklike { color: #4F46E5; }
}
:root[data-theme="dark"] .kv-linklike { color: #A5B4FC; }

/* The creator byline on a deck card and under a deck title. */
.kv-creator-by { display: block; font-size: 11.5px; color: var(--text-muted); margin: 2px 0 6px; }
.kv-creator-link {
    background: none; border: 0; padding: 0; font: inherit; cursor: pointer;
    color: #4F46E5; text-decoration: underline; text-underline-offset: 2px;
}
.kv-creator-link:hover { text-decoration-thickness: 2px; }
.kv-creator-link:focus-visible { outline: 2px solid #6366F1; outline-offset: 2px; border-radius: 3px; }
/* Under a deck title the byline sits with the card count, so it reads larger. */
.kv-sethero__by { font-size: 13px; margin-top: 4px; }
:root:not([data-theme="light"]) .kv-creator-link { color: #A5B4FC; }
@media (prefers-color-scheme: light) {
    :root:not([data-theme="dark"]) .kv-creator-link { color: #4F46E5; }
}
:root[data-theme="dark"] .kv-creator-link { color: #A5B4FC; }

/* ── Username editor, in Share settings ──────────────────────────────── */
.kv-uname { margin-top: 18px; padding-top: 16px; border-top: 1px solid rgba(148, 163, 184, 0.25); }
.kv-uname-label { display: block; font-weight: 700; font-size: 0.92rem; margin-bottom: 2px; }
.kv-uname-value { margin: 2px 0 4px; font-size: 1rem; font-weight: 600; }
.kv-uname-row { display: flex; gap: 8px; align-items: center; margin-top: 8px; }
.kv-uname-input {
    flex: 1 1 auto; min-width: 0; font: inherit; padding: 8px 10px;
    border-radius: 9px; border: 1px solid rgba(148, 163, 184, 0.45);
    background: var(--kv-surface, #fff); color: var(--kv-text, #0F172A);
}
.kv-uname-input:focus-visible { outline: 3px solid #6366F1; outline-offset: 1px; }
/* The verdict line keeps its height so the row does not jump as you type. */
.kv-uname-msg { min-height: 1.2em; margin: 6px 0 0; font-size: 0.85rem; }
.kv-uname-msg.is-ok { color: #059669; }
.kv-uname-msg.is-err { color: #DC2626; }
.kv-uname-msg.is-busy { opacity: 0.7; }
:root:not([data-theme="light"]) .kv-uname-input {
    background: rgba(15, 23, 42, 0.35); border-color: rgba(148, 163, 184, 0.35);
    color: var(--kv-text, #E2E8F0);
}
@media (prefers-color-scheme: light) {
    :root:not([data-theme="dark"]) .kv-uname-input { background: #fff; color: #0F172A; }
}
:root[data-theme="dark"] .kv-uname-input {
    background: rgba(15, 23, 42, 0.35); border-color: rgba(148, 163, 184, 0.35);
}


/* ── SUBJECTS & EXAMS inside Knovli Learn ────────────────────────
   The second section of Learn: an index of subject cards (the Featured
   reading card, reused), and a subject's own page — its write-up beside its
   units, each unit opening to the decks and quizzes for it. The same page
   that is published at /subjects/<slug>, drawn from the data stored with it. */
.kvLearnTabs { display: flex; gap: 6px; flex-wrap: wrap; margin: 18px 0 20px; border-bottom: 1px solid var(--border); }
.kvLearnTab {
    padding: 10px 16px; margin: 0 0 -1px; border: 0; border-bottom: 3px solid transparent; background: none;
    font: inherit; font-weight: 700; font-size: 0.95rem; color: var(--text-muted); cursor: pointer;
}
.kvLearnTab:hover { color: var(--text); }
.kvLearnTab.is-on { color: var(--brand-blue); border-bottom-color: var(--brand-blue); }
.kvLearnTab:focus-visible { outline: 3px solid var(--brand-blue); outline-offset: 2px; border-radius: 6px; }
.kvSubjIntro { margin: 0 0 6px; }
.kvSubjArea { margin: 22px 0 10px; overflow-wrap: anywhere; }
/* "Your progress" on a subject page: how many of its concepts the learner
   has proven, by unit or by era. Drawn only when the subject has a knowledge
   map; #kvGoalBox is empty (no height, no margin) otherwise. */
.kvGoal { margin: 18px 0 4px; padding: 16px 18px; border: 1px solid var(--border); border-radius: 14px; background: var(--bg-card); }
.kvGoalHead { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8px 14px; }
.kvGoal h2 { margin: 0; font-family: var(--font-display); font-size: 1.15rem; color: var(--text); }
.kvGoalTabs { display: inline-flex; border: 1px solid var(--border); border-radius: 999px; overflow: hidden; }
.kvGoalTab { padding: 5px 13px; border: 0; background: none; font: inherit; font-size: 0.82rem; font-weight: 700; color: var(--text-muted); cursor: pointer; }
.kvGoalTab.is-on { background: var(--brand-blue); color: #FFFFFF; }
.kvGoalTab:focus-visible { outline: 3px solid var(--brand-blue); outline-offset: -3px; }
.kvGoalSum { margin: 8px 0 10px; color: var(--text-dim); font-size: 0.95rem; line-height: 1.45; }
.kvGoalSum strong { color: var(--text); }
.kvGoalBar { height: 8px; border-radius: 999px; background: var(--surface-2); overflow: hidden; margin: 0 0 12px; }
.kvGoalBar > span { display: block; height: 100%; border-radius: 999px; background: #16A34A; }
.kvGoalGroup { border-top: 1px solid var(--border); }
.kvGoalGroup > summary { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 0; cursor: pointer; font-weight: 700; color: var(--text); list-style: none; }
.kvGoalGroup > summary::-webkit-details-marker { display: none; }
.kvGoalGroup > summary > span:first-child { min-width: 0; overflow-wrap: anywhere; }
.kvGoalGroup > summary:focus-visible { outline: 3px solid var(--brand-blue); outline-offset: 2px; }
.kvGoalCount { flex: none; font-weight: 600; font-size: 0.8rem; color: var(--text-muted); text-align: right; }
.kvGoalList { list-style: none; margin: 0 0 10px; padding: 0; }
.kvGoalC { display: flex; align-items: baseline; flex-wrap: wrap; gap: 2px 10px; padding: 6px 0; }
.kvGoalDot { flex: none; width: 10px; height: 10px; border-radius: 50%; align-self: center; background: var(--surface-2); border: 1px solid var(--border); }
.kvGoalC--proven .kvGoalDot { background: #16A34A; border-color: #16A34A; }
.kvGoalC--learning .kvGoalDot { background: #F59E0B; border-color: #F59E0B; }
.kvGoalC--thin .kvGoalDot, .kvGoalC--nocards .kvGoalDot { background: none; border-style: dashed; }
.kvGoalName { flex: 1 1 180px; min-width: 0; font-weight: 600; color: var(--text); overflow-wrap: anywhere; }
.kvGoalMeta { font-size: 0.8rem; color: var(--text-muted); }
.kvGoalNote { margin: 10px 0 0; padding-top: 10px; border-top: 1px solid var(--border); font-size: 0.8rem; color: var(--text-muted); line-height: 1.45; }

/* Filtering the list by the subject hierarchy: a trail of where you are, and
   chips for the next level down. Shown only once there are enough subjects. */
.kvSubjFilter { margin: 0 0 6px; }
.kvSubjSearch { margin: 10px 0 12px; }
.kvSubjTrail { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 8px; margin: 0 0 10px; font-size: 0.9rem; color: var(--text-muted); }
.kvSubjCrumbBtn { padding: 2px 0; border: 0; background: none; font: inherit; color: var(--accent, #2563EB); cursor: pointer; }
.kvSubjCrumbBtn:hover { text-decoration: underline; }
.kvSubjHere { font-weight: 700; color: var(--text); overflow-wrap: anywhere; }
.kvSubjChips { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 6px; }
.kvSubjChip {
    /* inline-block, not flex: bare text inside a flex box will not break, so a
       long level name ran off a phone screen. */
    display: inline-block; max-width: 100%;
    padding: 7px 13px; border: 1px solid var(--border); border-radius: 999px; background: var(--bg-card);
    font: inherit; font-size: 0.88rem; font-weight: 600; color: var(--text); cursor: pointer; text-align: left; overflow-wrap: anywhere;
}
.kvSubjChip:hover { border-color: var(--brand-blue); }
.kvSubjChip:focus-visible, .kvSubjCrumbBtn:focus-visible { outline: 3px solid var(--brand-blue); outline-offset: 2px; }
.kvSubjChipN { margin-left: 4px; font-size: 0.74rem; font-weight: 700; color: var(--text-muted); }
/* A subject with no picture gets a slim tinted band, not a 16:9 blank: an
   index of mostly picture-less subjects was a wall of empty boxes. */
.kvSubjGrid .kv-fa-art--blank {
    aspect-ratio: auto; height: 72px; font-size: 1.7rem;
    background: linear-gradient(120deg, rgba(37, 99, 235, 0.14), rgba(124, 58, 237, 0.12));
}
.kvSubjCrumb { margin: 0 0 4px; color: var(--text-muted); font-size: 0.84rem; }
.kvSubjLead { margin: 0 0 16px; color: var(--text-dim); font-size: 1.05rem; line-height: 1.5; }
.kvSubjCols { display: grid; grid-template-columns: minmax(0, 1fr); gap: 26px; margin-top: 18px; align-items: start; }
@media (min-width: 1000px) { .kvSubjCols.has-side { grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr); } }
.kvSubjSide { min-width: 0; }
.kvSubjSide h2 { font-family: var(--font-display); font-size: 1.1rem; margin: 0 0 10px; color: var(--text); }
.kvSubjSide h2:not(:first-child) { margin-top: 22px; }
.kvSubjUnit { border: 1px solid var(--border); border-radius: 12px; background: var(--bg-card); margin: 0 0 8px; overflow: hidden; }
.kvSubjUnit > summary {
    display: flex; align-items: center; justify-content: space-between; gap: 12px;
    padding: 12px 14px; cursor: pointer; font-weight: 700; color: var(--text); list-style: none;
}
.kvSubjUnit > summary::-webkit-details-marker { display: none; }
.kvSubjUnit > summary > span:first-child { min-width: 0; overflow-wrap: anywhere; }
.kvSubjUnit > summary:focus-visible { outline: 3px solid var(--brand-blue); outline-offset: -3px; }
.kvSubjCount { flex: none; font-weight: 600; font-size: 0.8rem; color: var(--text-muted); }
.kvSubjUnitIn { padding: 0 14px 12px; }
.kvSubjUnitIn > p { margin: 0 0 10px; color: var(--text-dim); font-size: 0.9rem; line-height: 1.45; }
.kvSubjList { list-style: none; margin: 0; padding: 0; }
.kvSubjRow { display: flex; align-items: center; flex-wrap: wrap; gap: 4px 10px; padding: 8px 0; border-top: 1px solid var(--border); }
.kvSubjRow:first-child { border-top: 0; }
.kvSubjOpen {
    flex: 1 1 200px; min-width: 0; padding: 4px 0; border: 0; background: none; text-align: left; cursor: pointer;
    font: inherit; font-weight: 600; color: var(--accent, #2563EB); overflow-wrap: anywhere;
}
.kvSubjOpen:hover { text-decoration: underline; }
.kvSubjOpen:focus-visible { outline: 3px solid var(--brand-blue); outline-offset: 2px; border-radius: 4px; }
.kvSubjName { flex: 1 1 200px; min-width: 0; font-weight: 600; color: var(--text); overflow-wrap: anywhere; }
.kvSubjSize { font-size: 0.78rem; color: var(--text-muted); white-space: nowrap; }
.kvSubjTag { font-size: 0.7rem; font-weight: 700; padding: 2px 8px; border-radius: 999px; white-space: nowrap;
    background: var(--surface-2); color: var(--text-muted); }
.kvSubjTag--0 { background: rgba(37, 99, 235, 0.12); color: var(--brand-blue); }
.kvSubjTag--1 { background: rgba(22, 163, 74, 0.14); color: #15803D; }
.kvSubjNote { margin: 26px 0 0; font-size: 0.8rem; }

/* ── FEATURED ARTICLES (home, below the leaderboard) ─────────────
   Knovli Learn articles and subject pages picked in Studio Admin → Featured.
   Three across, then two, then one. 🚨 No `display` on #ccHomeFaSection: it
   is toggled with the hidden attribute. A card is a <button> (Learn article,
   opens the in-app reader) or an <a> (subject page), so both are reset to
   the same box here. */
.kv-fa { margin-top: 26px; }
.kv-fa-row {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: clamp(10px, 1.2vw, 16px);
    align-items: stretch;
}
@media (max-width: 900px) { .kv-fa-row { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 560px) { .kv-fa-row { grid-template-columns: minmax(0, 1fr); } }
.kv-fa-card {
    display: flex; flex-direction: column; min-width: 0;
    margin: 0; padding: 0; overflow: hidden;
    font: inherit; text-align: left; text-decoration: none; cursor: pointer;
    color: var(--text); background: var(--bg-card);
    border: 1px solid var(--border); border-radius: 14px;
    transition: transform .15s ease, box-shadow .15s ease;
}
.kv-fa-card:hover { transform: translateY(-2px); box-shadow: 0 8px 22px rgba(15, 23, 42, 0.12); }
.kv-fa-card:focus-visible { outline: 3px solid var(--brand-blue); outline-offset: 2px; }
.kv-fa-art { display: block; width: 100%; aspect-ratio: 16 / 9; overflow: hidden; background: var(--surface-2); }
.kv-fa-art img { display: block; width: 100%; height: 100%; object-fit: cover; }
.kv-fa-art--blank { display: flex; align-items: center; justify-content: center; font-size: 2.2rem; }
.kv-fa-body { display: flex; flex-direction: column; gap: 6px; min-width: 0; padding: 14px 16px 16px; }
.kv-fa-eyebrow { font-size: 0.72rem; font-weight: 800; letter-spacing: 0.05em; text-transform: uppercase; color: var(--brand-blue); }
.kv-fa-title { font-weight: 800; font-size: 1.05rem; line-height: 1.3; overflow-wrap: anywhere; }
.kv-fa-sum { color: var(--text-dim); font-size: 0.9rem; line-height: 1.45; overflow-wrap: anywhere; }
@media (prefers-reduced-motion: reduce) {
    .kv-fa-card { transition: none; }
    .kv-fa-card:hover { transform: none; }
}

/* ── FEATURED QUIZZES (home) + /inseason ─────────────────────────
   The row is 3 or 4 across on a full screen, set by the ACTIVE configuration's
   layout and written onto the row as data-layout. 🚨 No `display` on the
   section itself — `#ccHomeFqSection` is toggled with the hidden attribute,
   and any display rule here would beat it. */
.kv-fq-row {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: clamp(10px, 1.2vw, 16px);
    align-items: stretch;
}
.kv-fq-row[data-layout="4"] { grid-template-columns: repeat(4, minmax(0, 1fr)); }
/* minmax(0, 1fr) is what lets a long quiz title wrap instead of forcing the
   track wider than the page. */
@media (max-width: 1100px) { .kv-fq-row[data-layout="4"] { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 900px)  { .kv-fq-row { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 560px)  {
    .kv-fq-row,
    .kv-fq-row[data-layout="4"] { grid-template-columns: minmax(0, 1fr); }
}
.kv-fq-row .kv-quiz-card { margin: 0; height: 100%; }

/* The hero is slot 1 of a 4-slot layout: a group advertisement, not a quiz. */
.kv-fq-hero {
    display: flex; flex-direction: column; height: 100%;
    padding: 0; overflow: hidden; cursor: pointer; text-align: left;
    background: var(--bg-card);
    border: 1.5px solid rgba(37, 99, 235, 0.32);
    border-radius: 14px;
    font-family: inherit;
    transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.kv-fq-hero:hover { transform: translateY(-2px); box-shadow: 0 10px 24px rgba(15, 23, 42, 0.14); }
.kv-fq-hero:focus-visible { outline: 3px solid #2563EB; outline-offset: 2px; }
.kv-fq-hero-art { display: block; width: 100%; aspect-ratio: 3 / 2; overflow: hidden; }
.kv-fq-hero-art img { width: 100%; height: 100%; object-fit: cover; display: block; }
.kv-fq-hero-body { display: block; padding: clamp(11px, 1.4vw, 15px); }
.kv-fq-hero-eyebrow {
    display: block; font-size: 11px; font-weight: 800; letter-spacing: 0.08em;
    text-transform: uppercase; color: #2563EB; margin-bottom: 4px;
}
.kv-fq-hero-title { display: block; font-weight: 700; color: var(--text); line-height: 1.3; }

/* The /inseason header: art with the title and description OVER it, which is
   why the overlay carries its own scrim — an admin image can be any
   brightness, and white-on-white is not readable. */
.kv-inseason-hero {
    position: relative; overflow: hidden; border-radius: 16px;
    margin: 0 0 clamp(16px, 2.4vh, 24px);
    min-height: clamp(180px, 26vh, 300px);
    display: flex; align-items: flex-end;
    background: linear-gradient(135deg, rgba(37, 99, 235, 0.16), rgba(139, 92, 246, 0.16));
}
.kv-inseason-art { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
/* The pasted animated background. Sits over the photo and under the text
   scrim; `pointer-events:none` so it never swallows a click, and it is
   aria-hidden in the markup so it is not announced. */
.kv-inseason-bg {
    position: absolute; inset: 0; width: 100%; height: 100%;
    border: 0; pointer-events: none; background: transparent;
}
@media (prefers-reduced-motion: reduce) { .kv-inseason-bg { display: none; } }
.kv-inseason-over {
    position: relative; width: 100%;
    padding: clamp(16px, 2.4vw, 28px);
    background: linear-gradient(to top, rgba(2, 6, 23, 0.82) 0%, rgba(2, 6, 23, 0.55) 55%, rgba(2, 6, 23, 0) 100%);
    color: #FFFFFF;
}
/* 🚨 The plain variant has no photo to sit on, so the scrim would be a dark
   band over nothing. */
.kv-inseason-hero--plain .kv-inseason-over { background: none; color: var(--text); }
.kv-inseason-eyebrow {
    margin: 0 0 6px; font-size: 12px; font-weight: 800; letter-spacing: 0.1em;
    text-transform: uppercase; opacity: 0.92;
}
.kv-inseason-title { margin: 0; font-size: clamp(22px, 3.4vw, 38px); line-height: 1.15; }
.kv-inseason-desc { margin-top: 10px; max-width: 70ch; line-height: 1.55; }
.kv-inseason-desc p { margin: 0 0 8px; }
.kv-inseason-desc :last-child { margin-bottom: 0; }

/* The collection's own limited challenges, under its quizzes. */
.kv-inseason-chall { margin-top: clamp(22px, 3vh, 34px); }
.kv-inseason-chall .cc-section-head { margin-bottom: 6px; }
.kv-inseason-chall-blurb {
    margin: 0 0 clamp(12px, 1.6vh, 18px);
    max-width: 70ch; line-height: 1.55; color: var(--text-muted);
}
.kv-inseason-chall-blurb b { color: var(--text); }


/* ── SEASONAL THEME PREVIEW BAR ───────────────────────────────
   Shown only when ?theme=<id> renders an UNPUBLISHED theme, so a preview tab
   left open cannot be mistaken for the live site. */
.kv-theme-preview {
    position: fixed; left: 0; right: 0; bottom: 0; z-index: 9999;
    padding: 8px 14px; text-align: center;
    font-size: 13px; font-weight: 700; font-family: inherit;
    color: #111827; background: #FCD34D;
    border-top: 2px solid #B45309;
    pointer-events: none;
}


/* ── A THEMED (seasonal) LOGO ─────────────────────────────────
   🚨 The default logo is a LOCKUP: a square mark plus the word “knovli”.
   A seasonal logo almost always has the name drawn into the artwork, so
   showing the word beside it prints the name TWICE — and a wide image
   crushed into the 30px square mark box is unreadable. When app.js flags the
   container, the word goes and the image takes the whole row at its own
   aspect ratio.
   🔑 HEIGHT is set and WIDTH is auto, so any aspect ratio fits the bar
   without distortion; max-width stops an extreme one pushing the nav. */
.knovli-logo[data-kv-logo="theme"] .knovli-word { display: none; }
.knovli-logo[data-kv-logo="theme"] .knovli-mark {
    width: auto;
    height: 38px;
    max-width: 210px;
    aspect-ratio: auto;
    object-fit: contain;
}
.knovli-logo--lg[data-kv-logo="theme"] .knovli-mark { height: 60px; max-width: 320px; }
.sidebar-logo[data-kv-logo="theme"] .knovli-mark    { height: 42px; max-width: 200px; }
.mmedu-footer-mark[data-kv-logo="theme"] .knovli-mark { height: 28px; max-width: 170px; }
@media (max-width: 420px) {
    .knovli-logo[data-kv-logo="theme"] .knovli-mark { height: 32px; max-width: 150px; }
}


/* ── THE SEASONAL PLAY BACKGROUND, ON EVERY PLAY PAGE ─────────────────
   The dashboard paints its own `.kv-play-bg` inside `.kv-play`. Every OTHER
   play page is a plain `.kv-content` panel, so the artwork stopped at the
   dashboard. This paints it behind the whole page instead, driven by the
   `data-kv-view` hook `_showPanel` writes.

   🚨 The DASHBOARD is excluded: it already has its own layer, and
   painting both would stack two scrims and darken it.
   🔑 Gated on `data-kv-playbg`, which app.js sets only when a theme
   actually supplies an image — `var(--kv-th-play-bg-2, none)` would still
   paint the scrim over an unthemed page.

   🔑 `--kv-th-play-bg-2` is the SECOND image: the Play dashboard has its
   own artwork and every page after it can have a different one. app.js falls
   the second back to the first, so a theme that sets only the dashboard image
   still carries it through the whole flow. */
html[data-kv-playbg][data-kv-view^="play"]:not([data-kv-view="play-dashboard"]) body::before {
    content: '';
    position: fixed;
    inset: 0;
    z-index: 0;
    pointer-events: none;
    background-image:
        linear-gradient(180deg,
            rgba(var(--kv-th-scrim, 17, 24, 43), 0.38) 0%,
            rgba(var(--kv-th-scrim, 17, 24, 43), 0.62) 46%,
            rgba(var(--kv-th-scrim, 17, 24, 43), 0.92) 100%),
        var(--kv-th-play-bg-2, none);
    background-size: cover, cover;
    background-position: center top, center top;
    background-repeat: no-repeat, no-repeat;
}
/* The page content has to sit ABOVE that fixed layer.
   🚨 EITHER background counts. The pasted animation is a fixed frame at
   z-index 0, so an animation-only theme would paint OVER the page without
   this — the image flag alone was not enough. */
html[data-kv-playbg][data-kv-view^="play"] .main-panel,
html[data-kv-playfx][data-kv-view^="play"] .main-panel { position: relative; z-index: 1; }
/* ── THE HOMEPAGE'S OWN BACKGROUND (owner, 2026-10-01) ────────────────
   "change the background of the homepage… image/color/or css/html animated
   background." Three independent layers, authored in Studio Admin → Knovli
   Admin → Themes, each optional:

     --kv-th-home-bg-color   a flat colour
     --kv-th-home-bg         a picture, painted OVER the colour
     (the pasted animation)  the same sandboxed .kv-play-bg-frame the Play
                             pages use, rendered over both

   🔑 Scoped to the home view only, and only when a theme actually sets one:
   `data-kv-homebg` is the switch, because CSS cannot ask whether a custom
   property is set and `var(--x, none)` would paint on every unthemed page. */
html[data-kv-homebg][data-kv-view="home"] .main-panel {
    background-color: var(--kv-th-home-bg-color, transparent);
    background-image: var(--kv-th-home-bg, none);
    background-size: cover;
    background-position: center top;
    background-repeat: no-repeat;
    /* Stays put while the page scrolls, like the Play shell's. */
    background-attachment: fixed;
}
/* 🚨 EITHER background counts — the same trap the Play pages hit. The pasted
   animation is a FIXED frame at z-index 0, so without the content being given
   a layer of its own an animation-only theme paints OVER the homepage. The
   image/colour flag alone is not enough, which is why there are two. */
html[data-kv-homebg][data-kv-view="home"] .main-panel,
html[data-kv-homefx][data-kv-view="home"] .main-panel { position: relative; z-index: 1; }


/* The PASTED animated background. Same containment as the In Season hero's:
   a sandboxed frame that cannot script, navigate or reach the page. It sits
   above the photo layer and below everything else. */
.kv-play-bg-frame {
    position: fixed; inset: 0; width: 100%; height: 100%;
    border: 0; pointer-events: none; background: transparent;
    z-index: 0;
}
@media (prefers-reduced-motion: reduce) { .kv-play-bg-frame { display: none; } }


/* ── THEMED TEXT ON THE PLAY PAGES (not the dashboard) ────────────────
   Only the text that sits DIRECTLY on the seasonal background. A dark photo
   makes the shipped near-black headings unreadable, and the reader cannot
   change it.

   🔑 Each value keeps TODAY'S as its fallback, so a theme that sets no
   page text changes nothing here.

   🚨 Gated on `data-kv-pagetext` — set when the THEME SETS A TEXT COLOUR,
   not when a background image exists. It was gated on `data-kv-playbg`, and a
   theme whose background is the PASTED ANIMATION has no image, so its page
   text was silently dead.

   🚨 Deliberately NOT a blanket `--text` override. That variable is used
   inside cards and panels, which have their own backgrounds; repainting it
   would recolour text that never touches the seasonal artwork. These are the
   specific elements that do. */
/* 🚨 Written FLAT, not nested. Nothing else in this stylesheet uses CSS
   nesting, and a nested block is silently dropped whole by a browser that
   does not support it — taking every rule inside it with it. */
html[data-kv-pagetext][data-kv-view^="play"]:not([data-kv-view="play-dashboard"]) .kv-content-head h2 { color: var(--kv-th-page-text, var(--text)); }
html[data-kv-pagetext][data-kv-view^="play"]:not([data-kv-view="play-dashboard"]) .kv-content-head p  { color: var(--kv-th-page-text-muted, var(--text-muted)); }
html[data-kv-pagetext][data-kv-view^="play"]:not([data-kv-view="play-dashboard"]) .kv-content-note    { color: var(--kv-th-page-text-muted, var(--text-muted)); }
html[data-kv-pagetext][data-kv-view^="play"]:not([data-kv-view="play-dashboard"]) .kv-content-foot    { color: var(--kv-th-page-text-muted, var(--text-dim, var(--text-muted))); }
html[data-kv-pagetext][data-kv-view^="play"]:not([data-kv-view="play-dashboard"]) .kv-content-foot b  { color: var(--kv-th-page-text, var(--text-muted)); }
html[data-kv-pagetext][data-kv-view^="play"]:not([data-kv-view="play-dashboard"]) .kv-steps .kv-step  { color: var(--kv-th-page-text-muted, var(--text-muted)); }


/* ── HOW FAR A THEME'S ACCENT REACHES ────────────────────────────
   `--accent-blue` is the primary action colour: the stylesheet resolves it in
   74 places, and 22 of those are :hover rules. Repointing it is what carries a
   season through buttons, links, focus rings and hover states.

   🔑 `scope: 'play'` (the default) keeps it inside the Play pages.
   `scope: 'site'` carries it everywhere. Both keep TODAY'S colour as the
   fallback, so a theme with no accent changes nothing either way.

   🚨 Specificity: `--accent-blue` is declared once, in `:root`. These use
   `:root[…]` so they match that weight and win on order — an `html[…]`
   selector is LIGHTER and would lose to any future `:root[data-theme=…]`
   redefinition.

   🚨 Only the ACCENT travels site-wide. The background is sized and
   scrimmed for the Play shell; carrying it everywhere would fight every other
   page's layout. */
:root[data-kv-scope="site"] {
    --accent-blue: var(--kv-th-accent, #2563EB);
}
:root[data-kv-scope="play"][data-kv-view^="play"] {
    --accent-blue: var(--kv-th-accent, #2563EB);
}

/* ⏳ "Public status is pending admin approval" — shown to the OWNER on a deck
   whose publish request an automatic check could not clear. Amber, not red: the
   deck saved fine and is theirs to use; only the public shelf is waiting. */
.kv-pending-public {
    margin: 14px 0 0;
    padding: 10px 14px;
    border-radius: 10px;
    background: rgba(180, 83, 9, 0.10);
    border: 1px solid rgba(180, 83, 9, 0.30);
    color: #B45309;
    font-size: 13px;
    line-height: 1.5;
}
