/* ============================================================================
   liquid-glass-core.css
   ----------------------------------------------------------------------------
   Liquid Glass primitives for CollabIQ Schema Explorer.

   Scope: production deploy/index.html (link tag at line 35) AND the
   experimental deploy/liquid_glass_schema/index copy.html.

   Master plan: ~/.claude/plans/zesty-popping-kahn.md
   Reference doc: docs/liquid-glass-reference.md

   Phase 1 deliverable: tokens, .liq-glass primitives, modifiers, keyframes,
   accessibility fallbacks, lens-field gradients.

   2026-04-28: chrome blur reduced from 40px → 24px and saturate 1.6 → 1.8
   to match the DashIQ dashboard's chrome recipe. The two products now share
   the same backdrop-filter values so the floating shells read as siblings.
   ============================================================================ */

/* ----------------------------------------------------------------------------
   1. TOKENS — concentric geometry, easing, durations, registered properties
   ---------------------------------------------------------------------------- */

:root {
  /* Concentric geometry — outer radius MUST equal inner radius + padding.
     24 → 18 → 14 → 10 → 6 with 6/4 px padding cascade. */
  --liq-radius-container: 24px;
  --liq-radius-pane:      18px;
  --liq-radius-element:   14px;
  --liq-radius-chip:      10px;
  --liq-radius-atom:       6px;

  /* Easing — Apple UISpring + fluid overshoot variants */
  --liq-ease:        cubic-bezier(0.32, 0.72, 0, 1);
  --liq-ease-fluid:  cubic-bezier(0.22, 1.08, 0.36, 1);
  --liq-ease-in:     cubic-bezier(0.32, 0, 0.67, 0);

  /* Durations */
  --liq-dur-short:   220ms;
  --liq-dur-med:     380ms;
  --liq-dur-long:    760ms;

  /* Lens / parallax tracking — JS writes to these on pointermove */
  --lens-x: 50%;
  --lens-y: 45%;
  --parallax-x: 0;
  --parallax-y: 0;

  /* Adaptive tint — JS writes the active domain color here */
  --liq-tint: transparent;

  /* Brand color tokens for the lens field. Match the existing accent. */
  --liq-tint-teal:   61, 187, 176;   /* gene/protein, drug, default accent */
  --liq-tint-indigo: 99, 102, 241;   /* mol function, pathway, knowledge */
  --liq-tint-pink:   236, 72, 153;   /* disease, FDA, regulatory */
}

/* @property registrations enable interpolation of CSS custom properties.
   Required for animatable specular angle, opacity, and touch strength.
   Safari ≤ 16.3 lacks support — feature-detect at runtime, fall back to
   static values. */
@supports (background: paint(something)) {
  /* Houdini gate — only register where Paint API exists (rough proxy). */
}
@property --liq-specular-angle {
  syntax: "<angle>";
  initial-value: 135deg;
  inherits: true;
}
@property --liq-specular-opacity {
  syntax: "<number>";
  initial-value: 0.7;
  inherits: true;
}
@property --liq-touch-strength {
  syntax: "<number>";
  initial-value: 0;
  inherits: true;
}
@property --liq-touch-x {
  syntax: "<percentage>";
  initial-value: 50%;
  inherits: true;
}
@property --liq-touch-y {
  syntax: "<percentage>";
  initial-value: 50%;
  inherits: true;
}
/* Parallax registrations — without these, calc(var() * -1px) silently
   resolves to 0 in Chromium because the unregistered token can't be coerced
   to a number inside a length calc. */
@property --parallax-x {
  syntax: "<number>";
  initial-value: 0;
  inherits: true;
}
@property --parallax-y {
  syntax: "<number>";
  initial-value: 0;
  inherits: true;
}

/* ----------------------------------------------------------------------------
   2. KEYFRAMES — materialization, content cascade, idle breathing
   ---------------------------------------------------------------------------- */

@keyframes liqMaterialize {
  0% {
    opacity: 0;
    transform: scale(0.96) translateY(6px);
    filter: blur(14px);
    --liq-specular-opacity: 0;
  }
  45% {
    opacity: 1;
    filter: blur(0);
  }
  100% {
    opacity: 1;
    transform: scale(1) translateY(0);
    filter: blur(0);
    --liq-specular-opacity: 0.7;
  }
}

@keyframes liqMaterializeContent {
  0% {
    opacity: 0;
    transform: translateY(4px);
  }
  100% {
    opacity: 1;
    transform: translateY(0);
  }
}

/* Choreography helpers — applied as classes by liqChoreography() so the
   browser owns the animation lifecycle. We deliberately omit `forwards`
   so the element returns to the cascade after the animation ends, letting
   .hero-receded and other state classes apply normally. The transition on
   #app would otherwise hijack interpolation if we touched transform/opacity
   from JS inline styles directly. */
.liq-materializing {
  animation: liqMaterialize 760ms cubic-bezier(0.32, 0.72, 0, 1) backwards;
}
.liq-content-materializing > * {
  animation: liqMaterializeContent 380ms cubic-bezier(0.32, 0.72, 0, 1) backwards;
}

@keyframes liqBreathe {
  0%, 100% {
    --liq-specular-angle: 135deg;
    --liq-specular-opacity: 0.65;
  }
  50% {
    --liq-specular-angle: 142deg;
    --liq-specular-opacity: 0.78;
  }
}

/* ----------------------------------------------------------------------------
   3. .liq-glass — the base material class
   ---------------------------------------------------------------------------- */

.liq-glass {
  position: relative;
  background:
    linear-gradient(155deg,
      color-mix(in srgb, #0a1730 82%, var(--liq-tint, transparent) 18%) 0%,
      color-mix(in srgb, #0a1730 88%, var(--liq-tint, transparent) 12%) 50%,
      color-mix(in srgb, #0a1730 80%, var(--liq-tint, transparent) 20%) 100%);
  border-radius: var(--liq-radius-container);
  border: 1px solid rgba(255, 255, 255, 0.10);
  backdrop-filter: blur(24px) saturate(1.8);
  -webkit-backdrop-filter: blur(24px) saturate(1.8);
  box-shadow:
    0 40px 120px rgba(0, 0, 0, 0.50),
    0 16px 48px rgba(0, 0, 0, 0.30),
    inset 0 1px 0 rgba(255, 255, 255, 0.22),    /* top rim specular */
    inset 1px 0 0 rgba(255, 255, 255, 0.08),    /* left rim */
    inset 0 -1px 0 rgba(0, 0, 0, 0.40),         /* bottom inner shadow */
    inset 0 0 80px rgba(61, 187, 176, 0.04);    /* lens-field bleed */
  contain: layout paint;
  isolation: isolate;
  animation: liqBreathe 14s ease-in-out infinite;
  transform: translateZ(0); /* force own GPU layer (Safari fix) */
}

/* Specular highlight (animated rim light).
   Uses mask-composite to cut a 1px content-box hole, leaving only the rim. */
.liq-glass::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  padding: 1px;
  background: linear-gradient(
    var(--liq-specular-angle, 135deg),
    rgba(255, 255, 255, 0.30) 0%,
    rgba(255, 255, 255, 0.08) 18%,
    rgba(255, 255, 255, 0.02) 48%,
    rgba(255, 255, 255, 0.08) 82%,
    rgba(255, 255, 255, 0.22) 100%);
  -webkit-mask:
    linear-gradient(#000 0 0) content-box,
    linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask:
    linear-gradient(#000 0 0) content-box,
    linear-gradient(#000 0 0);
          mask-composite: exclude;
  pointer-events: none;
  opacity: var(--liq-specular-opacity, 0.7);
  transition: opacity 400ms var(--liq-ease);
  z-index: 2;
}

/* Touch ripple (fluidity).
   JS writes --liq-touch-x/y on pointerdown, bumps --liq-touch-strength to 1,
   then drops back to 0. @property interpolation produces the bloom. */
.liq-glass::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: radial-gradient(
    circle 200px at var(--liq-touch-x, 50%) var(--liq-touch-y, 50%),
    rgba(255, 255, 255, calc(0.18 * var(--liq-touch-strength, 0))) 0%,
    rgba(255, 255, 255, calc(0.06 * var(--liq-touch-strength, 0))) 28%,
    transparent 60%);
  pointer-events: none;
  mix-blend-mode: screen;
  transition: --liq-touch-strength 700ms var(--liq-ease-fluid);
  z-index: 1;
}

/* ----------------------------------------------------------------------------
   4. MODIFIERS — semantic variants of .liq-glass
   ---------------------------------------------------------------------------- */

/* --container — the outer app shell, the SINGLE shared sampling region.
   This is what every nested pane reads through. */
.liq-glass--container {
  border-radius: var(--liq-radius-container);
  backdrop-filter: blur(24px) saturate(1.8);
  -webkit-backdrop-filter: blur(24px) saturate(1.8);
}

/* --pane — interior children of the container.
   Drops own backdrop-filter (the container does the sampling).
   Just a translucent fill + concentric radius. */
.liq-glass--pane {
  position: relative;
  background:
    linear-gradient(180deg,
      rgba(255, 255, 255, 0.04) 0%,
      rgba(255, 255, 255, 0.01) 100%);
  border-radius: var(--liq-radius-pane);
  border: 1px solid rgba(255, 255, 255, 0.06);
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  animation: none;
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.10),
    inset 0 -1px 0 rgba(0, 0, 0, 0.20);
}
.liq-glass--pane::before,
.liq-glass--pane::after {
  display: none;
}

/* --ornament — floating panels OUTSIDE the shell. Keep own (smaller) blur
   because they're not nested in the shared sampling region. */
.liq-glass--ornament {
  border-radius: var(--liq-radius-element);
  backdrop-filter: blur(24px) saturate(1.5);
  -webkit-backdrop-filter: blur(24px) saturate(1.5);
  background:
    linear-gradient(155deg,
      rgba(255, 255, 255, 0.10) 0%,
      rgba(255, 255, 255, 0.05) 100%);
  border: 1px solid rgba(255, 255, 255, 0.18);
  box-shadow:
    0 24px 64px rgba(0, 0, 0, 0.45),
    0 8px 24px rgba(0, 0, 0, 0.25),
    inset 0 1px 0 rgba(255, 255, 255, 0.18);
}

/* --capsule — pill-shaped buttons, sliders, multi-button containers */
.liq-glass--capsule {
  border-radius: 9999px;
  backdrop-filter: blur(20px) saturate(1.5);
  -webkit-backdrop-filter: blur(20px) saturate(1.5);
  background:
    linear-gradient(155deg,
      rgba(255, 255, 255, 0.10) 0%,
      rgba(255, 255, 255, 0.06) 100%);
  border: 1px solid rgba(255, 255, 255, 0.18);
  box-shadow:
    0 12px 32px rgba(0, 0, 0, 0.35),
    inset 0 1px 0 rgba(255, 255, 255, 0.18);
}

/* --card — IQ/domain dashboard cards. Lighter, more opaque (still readable). */
.liq-glass--card {
  border-radius: var(--liq-radius-pane);
  backdrop-filter: blur(16px) saturate(1.6);
  -webkit-backdrop-filter: blur(16px) saturate(1.6);
  background:
    linear-gradient(155deg,
      rgba(255, 255, 255, 0.14) 0%,
      rgba(255, 255, 255, 0.08) 100%);
  border: 1px solid rgba(255, 255, 255, 0.12);
  box-shadow:
    0 18px 48px rgba(0, 0, 0, 0.30),
    0 4px 16px rgba(0, 0, 0, 0.20),
    inset 0 1px 0 rgba(255, 255, 255, 0.12);
  transition: --liq-touch-strength 360ms var(--liq-ease-fluid),
              transform 280ms var(--liq-ease),
              box-shadow 280ms var(--liq-ease);
}
.liq-glass--card:hover {
  --liq-touch-strength: 0.3;
  transform: translateY(-1px);
}

/* --drawer + --sheet are mobile-only constructs — their declarations live in
   section 8 (MOBILE PASS) inside an `@media (max-width: 768px)` block, scoped
   to #sidebar / #detailPanel so they win against the existing inline mobile
   rules in `index copy.html`. Keeping them out of section 4 prevents the
   ornament look from bleeding onto the desktop pane treatment. */

/* --hero-card — tutorial hero card. Brightest specular, larger blur. */
.liq-glass--hero-card {
  border-radius: var(--liq-radius-container);
  backdrop-filter: blur(28px) saturate(1.6);
  -webkit-backdrop-filter: blur(28px) saturate(1.6);
  background:
    linear-gradient(155deg,
      rgba(255, 255, 255, 0.16) 0%,
      rgba(255, 255, 255, 0.09) 100%);
  border: 1px solid rgba(255, 255, 255, 0.22);
  box-shadow:
    0 48px 120px rgba(0, 0, 0, 0.55),
    0 16px 48px rgba(0, 0, 0, 0.30),
    inset 0 1px 0 rgba(255, 255, 255, 0.30),
    inset 0 -1px 0 rgba(0, 0, 0, 0.40);
  --liq-specular-opacity: 0.85;
}

/* --panel — explore dropdowns and small popouts */
.liq-glass--panel {
  border-radius: var(--liq-radius-element);
  backdrop-filter: blur(20px) saturate(1.5);
  -webkit-backdrop-filter: blur(20px) saturate(1.5);
  background:
    linear-gradient(155deg,
      rgba(255, 255, 255, 0.12) 0%,
      rgba(255, 255, 255, 0.07) 100%);
  border: 1px solid rgba(255, 255, 255, 0.16);
  box-shadow:
    0 16px 48px rgba(0, 0, 0, 0.40),
    inset 0 1px 0 rgba(255, 255, 255, 0.14);
}

/* --tinted — domain-color tinted state (chip active, filter on) */
.liq-glass--tinted {
  background:
    linear-gradient(155deg,
      color-mix(in srgb, rgba(255,255,255,0.14) 60%, var(--liq-tint, rgba(61,187,176,0.6)) 40%) 0%,
      color-mix(in srgb, rgba(255,255,255,0.08) 70%, var(--liq-tint, rgba(61,187,176,0.6)) 30%) 100%);
  border-color: color-mix(in srgb, transparent 60%, var(--liq-tint, rgba(61,187,176,0.4)) 40%);
}

/* --off — disable glass entirely (used for hidden panels to save GPU) */
.liq-glass--off {
  backdrop-filter: none !important;
  -webkit-backdrop-filter: none !important;
  animation: none !important;
}
.liq-glass--off::before,
.liq-glass--off::after {
  display: none !important;
}

/* ----------------------------------------------------------------------------
   5. LENS FIELD — multi-radial gradients on .schema-canvas
   The shell's backdrop-filter picks these up as chromatic tint via lensing.
   ---------------------------------------------------------------------------- */

.schema-canvas {
  background:
    /* Lens-field tints — sit on top so they color the grid area */
    radial-gradient(ellipse 60% 50% at var(--lens-x, 50%) var(--lens-y, 45%),
      rgba(var(--liq-tint-teal), 0.10) 0%,
      transparent 62%),
    radial-gradient(ellipse 55% 40% at 15% 30%,
      rgba(var(--liq-tint-indigo), 0.05) 0%,
      transparent 60%),
    radial-gradient(ellipse 45% 35% at 85% 75%,
      rgba(var(--liq-tint-pink), 0.04) 0%,
      transparent 55%),
    radial-gradient(ellipse 70% 20% at 50% 100%,
      rgba(6, 16, 30, 0.85),
      transparent 55%),
    /* Grid major — 240px spacing, white 0.07 (matches dashboard --grid-color-strong) */
    repeating-linear-gradient(0deg,  rgba(255,255,255,0.07) 0px, rgba(255,255,255,0.07) 1px, transparent 1px, transparent 240px),
    repeating-linear-gradient(90deg, rgba(255,255,255,0.07) 0px, rgba(255,255,255,0.07) 1px, transparent 1px, transparent 240px),
    /* Grid minor — 48px spacing, white 0.04 (matches dashboard --grid-color) */
    repeating-linear-gradient(0deg,  rgba(255,255,255,0.04) 0px, rgba(255,255,255,0.04) 1px, transparent 1px, transparent 48px),
    repeating-linear-gradient(90deg, rgba(255,255,255,0.04) 0px, rgba(255,255,255,0.04) 1px, transparent 1px, transparent 48px);
  /* Canvas base color — matches dashboard's corner dark navy (--color-deep) so
     the schema interior reads at the same depth as the dashboard's outer-edge
     atmosphere. Grid sits above this, lens-field tints sit above the grid. */
  background-color: #06101e;
  /* Parallax: graph translates counter to cursor so chrome reads as a window
     in front. JS already encodes magnitude in px units, this rule just inverts
     the sign so the canvas drifts opposite the cursor. */
  transform: translate3d(
    calc(var(--parallax-x, 0) * -1px),
    calc(var(--parallax-y, 0) * -1px),
    0);
  transition: transform 120ms var(--liq-ease, cubic-bezier(0.32, 0.72, 0, 1));
  will-change: transform;
  contain: paint;
}

/* ----------------------------------------------------------------------------
   6. ACCESSIBILITY FALLBACKS — hard requirements from Apple HIG / WCAG
   ---------------------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  .liq-glass {
    animation: none;
  }
  .liq-glass::before {
    transition: none;
  }
  .liq-glass::after {
    transition: none;
  }
  .schema-canvas {
    transform: none !important;
  }
  :root {
    --parallax-x: 0;
    --parallax-y: 0;
  }
}

@media (prefers-reduced-transparency: reduce) {
  .liq-glass,
  .liq-glass--container,
  .liq-glass--pane,
  .liq-glass--ornament,
  .liq-glass--capsule,
  .liq-glass--card,
  .liq-glass--drawer,
  .liq-glass--sheet,
  .liq-glass--hero-card,
  .liq-glass--panel {
    backdrop-filter: none !important;
    -webkit-backdrop-filter: none !important;
    background: rgba(6, 16, 30, 0.96) !important;
    border-color: rgba(255, 255, 255, 0.16) !important;
  }
  .liq-glass::before,
  .liq-glass::after {
    display: none !important;
  }
}

/* ----------------------------------------------------------------------------
   7. VIEW TRANSITIONS — morphing state changes (Apple `glassEffectID` analog)
   When a parent calls document.startViewTransition(() => mutateDOM()), the
   browser automatically tween-fades these elements between snapshots.
   Browsers without support get the existing CSS transitions as fallback.
   ---------------------------------------------------------------------------- */
@supports (view-transition-name: none) {
  #detailPanel { view-transition-name: detail-panel; }
  #sidebar     { view-transition-name: sidebar-pane; }
  /* NOTE: do NOT add view-transition-name to #app — it causes Chromium to
     ignore inline transform/opacity on the shell, which breaks .hero-receded
     and any future animation that would mutate the container. The plan only
     calls for naming the panes that morph, anyway. */

  /* Customize the morph: smooth crossfade with the app shell timing curve */
  ::view-transition-old(detail-panel),
  ::view-transition-new(detail-panel),
  ::view-transition-old(sidebar-pane),
  ::view-transition-new(sidebar-pane) {
    animation-duration: 380ms;
    animation-timing-function: cubic-bezier(0.32, 0.72, 0, 1);
  }
  /* The default scale-fade looks slightly cleaner than crossfade-only here */
  ::view-transition-old(detail-panel) { animation-name: liqVtFadeOut; }
  ::view-transition-new(detail-panel) { animation-name: liqVtFadeIn; }
  ::view-transition-old(sidebar-pane) { animation-name: liqVtFadeOut; }
  ::view-transition-new(sidebar-pane) { animation-name: liqVtFadeIn; }
}
@keyframes liqVtFadeIn  { from { opacity: 0; transform: translateY(4px); } }
@keyframes liqVtFadeOut { to   { opacity: 0; transform: translateY(-4px); } }
@media (prefers-reduced-motion: reduce) {
  ::view-transition-old(detail-panel),
  ::view-transition-new(detail-panel),
  ::view-transition-old(sidebar-pane),
  ::view-transition-new(sidebar-pane) {
    animation-duration: 0.001ms !important;
  }
}

@media (prefers-contrast: more) {
  .liq-glass,
  .liq-glass--container,
  .liq-glass--pane,
  .liq-glass--ornament,
  .liq-glass--capsule,
  .liq-glass--card,
  .liq-glass--drawer,
  .liq-glass--sheet,
  .liq-glass--hero-card,
  .liq-glass--panel {
    border-color: rgba(255, 255, 255, 0.35) !important;
  }
  .liq-glass::before {
    opacity: 1 !important;
  }
}

/* ----------------------------------------------------------------------------
   8. MOBILE PASS — viewport-aware tuning per master plan Phase 5
   ----------------------------------------------------------------------------
   On phones the floating shell IS the viewport, so its own backdrop-filter is
   meaningless (nothing behind it to sample) and would cost battery for nothing.
   The shell drops its blur and idle breathing animation, the cursor parallax
   handler is replaced by the gyroscope IIFE in index.html, and the drawer +
   bottom sheet detach from the (now non-sampling) shell to become real
   ornaments with their own blur — the last surfaces a finger touches.

   The drawer (#sidebar) and bottom sheet (#detailPanel) get their declarations
   here rather than in section 4 so the ornament look only kicks in at <= 768px.
   Higher specificity (#id.class) is required to win against the existing inline
   mobile #sidebar / #detailPanel rules in index.html.
   ---------------------------------------------------------------------------- */

@media (max-width: 768px) {
  /* Shell becomes the viewport — drop its sampling region cost. */
  #app.liq-glass--container {
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    animation: none; /* idle breathing OFF (battery) */
  }
  /* Specular rim is the only visible artifact left — keep it but quiet. */
  #app.liq-glass--container::before {
    opacity: 0.45;
  }
  /* Touch ripple stays — it's the primary feedback channel on touch devices. */

  /* Cards can ease their blur a touch since the shell isn't sampling. */
  .liq-glass--card {
    backdrop-filter: blur(10px) saturate(1.3);
    -webkit-backdrop-filter: blur(10px) saturate(1.3);
  }

  /* ── DRAWER (mobile sidebar) ────────────────────────────────────────────
     36px blur because it slides outside the (now non-sampling) shell.
     Includes a blur ramp on open: 16px → 36px alongside the translateX
     slide gives the materialize-into-being feel without a separate keyframe. */
  #sidebar.liq-glass--drawer {
    backdrop-filter: blur(16px) saturate(1.3);
    -webkit-backdrop-filter: blur(16px) saturate(1.3);
    background:
      linear-gradient(155deg,
        rgba(6, 16, 30, 0.78) 0%,
        rgba(6, 16, 30, 0.86) 100%);
    border-right: 1px solid rgba(255, 255, 255, 0.12);
    border-radius: 0 var(--liq-radius-container) var(--liq-radius-container) 0;
    box-shadow:
      24px 0 60px rgba(0, 0, 0, 0.55),
      inset 0 1px 0 rgba(255, 255, 255, 0.16);
    /* Layer the blur ramp onto the existing transform slide. */
    transition:
      transform 0.3s cubic-bezier(0.32, 0.72, 0, 1),
      backdrop-filter 0.42s cubic-bezier(0.32, 0.72, 0, 1),
      -webkit-backdrop-filter 0.42s cubic-bezier(0.32, 0.72, 0, 1);
  }
  #sidebar.liq-glass--drawer.mobile-open {
    backdrop-filter: blur(36px) saturate(1.55);
    -webkit-backdrop-filter: blur(36px) saturate(1.55);
  }

  /* ── BOTTOM SHEET (mobile detail panel) ─────────────────────────────────
     36px blur with a pronounced 2px top rim. The inline #detailPanel rules
     in index.html still own position/transform/transition (those have
     !important on them), we only override the look. */
  #detailPanel.liq-glass--sheet {
    backdrop-filter: blur(36px) saturate(1.55);
    -webkit-backdrop-filter: blur(36px) saturate(1.55);
    background:
      linear-gradient(180deg,
        rgba(6, 16, 30, 0.82) 0%,
        rgba(6, 16, 30, 0.92) 100%);
    border-top: 1px solid rgba(255, 255, 255, 0.22);
    border-left: none;
    border-radius: var(--liq-radius-container) var(--liq-radius-container) 0 0;
    box-shadow:
      0 -24px 60px rgba(0, 0, 0, 0.55),
      inset 0 2px 0 rgba(255, 255, 255, 0.22),  /* pronounced top rim */
      inset 0 1px 0 rgba(255, 255, 255, 0.10);
  }
  /* Pronounced glass pill handle — replaces the existing 36×4 grey bar. */
  #detailPanel.liq-glass--sheet .mobile-sheet-handle span {
    width: 44px;
    height: 5px;
    border-radius: 3px;
    background:
      linear-gradient(180deg,
        rgba(255, 255, 255, 0.38) 0%,
        rgba(255, 255, 255, 0.18) 100%);
    box-shadow:
      0 1px 2px rgba(0, 0, 0, 0.40),
      inset 0 1px 0 rgba(255, 255, 255, 0.20);
  }

  /* During sheet animation, freeze canvas parallax to prevent double-frost
     flicker (sheet's blur + canvas transform repaints stacking). JS adds the
     `liq-sheet-transitioning` class on body for the duration of the transition. */
  body.liq-sheet-transitioning .schema-canvas {
    transform: none !important;
    transition: transform 0s !important;
  }
}

/* Phones with prefers-reduced-motion already get a no-op handler in
   liquidGlassGyro(), but also belt-and-braces zero parallax here so any
   stale CSS custom property doesn't leak through. */
@media (max-width: 768px) and (prefers-reduced-motion: reduce) {
  :root {
    --parallax-x: 0;
    --parallax-y: 0;
  }
}
