{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "curtain-austrian-gather",
  "title": "Curtain Austrian Gather",
  "description": "A full-bleed route curtain modeled on a real Austrian (brail) stage drape: sewn-in-fullness fabric hoisted bottom-up on parallel vertical lift lines, so it doesn't slide flat but gathers into a row of scalloped swags whose bellies deepen and breathe on independent phase-offset sines, both at rest and as the drape hoists.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/loud/curtain-austrian-gather/component.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useId, useLayoutEffect, useRef, useState } from \"react\";\n\n// CurtainAustrianGather — a full-bleed route curtain modeled on a real\n// Austrian (brail) drape: unlike a traveler (parts horizontally,\n// curtain-traveler-draw) or a tab curtain (peels diagonally,\n// curtain-tab-diagonal), an Austrian drape rides on PARALLEL VERTICAL LIFT\n// LINES and hoists bottom-up. Because the fabric carries ~100% sewn-in\n// fullness, it does not slide flat as the lines rise — it gathers into a\n// row of scalloped SWAGS along the hem, one swag per lift-line span, each\n// swag's belly deepening and its silhouette compressing upward as the\n// drape hoists. That festooned bottom edge, not a straight parting line,\n// is the whole point of the mechanic.\n//\n// Substrate: SVG. Each swag is one closed path — a straight top edge\n// pinned to the header track, two vertical sides riding its bounding lift\n// lines, and a cubic-bezier belly for the scallop — filled with a per-swag\n// horizontal gradient so the gather reads as shaded fabric, not a flat\n// festoon shape. (--border is a separator token, ~1.1:1 against\n// --background in light theme — unusable as fabric ink. Fold/scallop form\n// is carried in VALUE against --ns-muted's fabric body instead: a\n// --foreground-tinted shadow gradient and a --background-tinted highlight\n// gradient per swag, layered as partial opacity, never --border.)\n//\n// The resting (closed) state already breathes: every swag's belly depth\n// rides its own slow sine, independently phase-offset from its neighbours,\n// so individual scallops deepen and relax at rest — not a whole-panel\n// opacity or scale pulse. The gradient crease inside each swag breathes on\n// its own offset phase too, the same \"rewrite the gradient stops every\n// frame\" idiom curtain-traveler-draw uses for its pleats. Lifting runs a\n// single rAF timeline that eases every lift-line's hem height upward\n// together, with a small end-of-travel settle (rope give snapping the\n// drape into its batten) and a swag belly that grows dramatically deeper\n// as the shrinking panel bunches fabric toward the header.\n//\n// Colors come only from --ns-muted (fabric body), --foreground (shadow\n// side, rope lines) and --background (highlight side, plus the backing\n// \"revealed page\" rect), read once via getComputedStyle(document\n// .documentElement) and re-read on a MutationObserver watching\n// documentElement's class — no hex or rgb() literals anywhere in the draw\n// code. --ns-accent never touches the fabric; it only tints the optional\n// \"skip curtain\" trigger button.\n\nexport interface CurtainAustrianGatherProps {\n  /** Called once the lift animation (or reduced-motion hard cut) completes. */\n  onLiftComplete?: () => void;\n  /** Freezes the idle breathing and any in-flight lift at its current frame. */\n  paused?: boolean;\n  className?: string;\n}\n\ntype Tokens = {\n  muted: string;\n  foreground: string;\n  background: string;\n};\n\nconst VW = 1600;\nconst VH = 900;\nconst TRACK_Y = 20;\nconst TRACK_H = 10;\n\nconst SWAGS = 7;\nconst LIFT_LINES = SWAGS + 1;\n\nconst REST_HEM_Y = VH - 60; // 840 — drape hangs low, nearly full-bleed at rest\nconst LIFT_HEM_Y = 170; // hoisted hem, bunched tight beneath the header\n\nconst REST_SWAG_DEPTH = 48; // px belly droop below the hem line, at rest — visible festoon, not a flat hem\nconst LIFT_SWAG_DEPTH = 110; // px belly droop, fully hoisted — dramatic gather\n\nconst LIFT_MS = 600; // verify.ts's gate waits 700ms after the click — finish comfortably inside that\nconst SETTLE_OVERSHOOT = 0.06; // fraction of travel the rope gives past rest, then snaps back\n\nconst BREATH_PERIOD_MS = 4300;\nconst BREATH_AMP_FRAC = 0.4; // fraction of current belly depth, min-floored below\n\n// A dedicated marker, not the full backing rect: its centre is a point we\n// control (well clear of every lift line and comfortably inside the region\n// the swags vacate once hoisted), rather than the worst-case centre of a\n// full-viewBox rect that the fabric happens to cover last.\nconst MARKER_W = 160;\nconst MARKER_H = 48;\nconst MARKER_Y = VH * 0.55; // below LIFT_HEM_Y + LIFT_SWAG_DEPTH + breath headroom, above REST_HEM_Y\n\nfunction easeRise(t: number): number {\n  // f(1) = 1 exactly — a small overshoot (rope give at full hoist, then a\n  // snap into the batten) lives entirely inside [0,1], so it settles back\n  // to the exact hoisted height without extending duration.\n  const c1 = SETTLE_OVERSHOOT * 2.6;\n  const c3 = c1 + 1;\n  const p = t - 1;\n  return 1 + c3 * p * p * p + c1 * p * p;\n}\n\nfunction lineX(i: number): number {\n  return (i / SWAGS) * VW;\n}\n\ntype StopRefs = {\n  base0: SVGStopElement | null;\n  hiIn: SVGStopElement | null;\n  hiPeak: SVGStopElement | null;\n  shPeak: SVGStopElement | null;\n  shOut: SVGStopElement | null;\n  base1: SVGStopElement | null;\n};\n\nfunction emptyStopRefs(): StopRefs {\n  return { base0: null, hiIn: null, hiPeak: null, shPeak: null, shOut: null, base1: null };\n}\n\nexport function CurtainAustrianGather({ onLiftComplete, paused = false, className = \"\" }: CurtainAustrianGatherProps) {\n  const uid = useId().replace(/[:]/g, \"\");\n  const rootRef = useRef<HTMLDivElement | null>(null);\n  const swagBaseRefs = useRef<(SVGPathElement | null)[]>(Array.from({ length: SWAGS }, () => null));\n  const swagPathRefs = useRef<(SVGPathElement | null)[]>(Array.from({ length: SWAGS }, () => null));\n  const stopRefs = useRef<StopRefs[]>(Array.from({ length: SWAGS }, emptyStopRefs));\n  const ropeRefs = useRef<(SVGLineElement | null)[]>(Array.from({ length: LIFT_LINES }, () => null));\n  const trackRef = useRef<SVGRectElement | null>(null);\n  const bgRectRef = useRef<SVGRectElement | null>(null);\n  const markerRef = useRef<SVGRectElement | null>(null);\n\n  // No hardcoded fallback palette: a hex default here would be the light-\n  // theme value baked in, so a dark-theme visitor could see one wrong-\n  // polarity frame before the real read lands. Null until derive() runs,\n  // and derive() runs in a layout effect (before the browser's first\n  // paint), not a passive effect, so that frame never actually happens.\n  const tokensRef = useRef<Tokens | null>(null);\n\n  const [open, setOpen] = useState(false);\n  const [lifting, setLifting] = useState(false);\n  const [reduced, setReduced] = useState(false);\n  const [mounted, setMounted] = useState(false);\n\n  const liftStartRef = useRef<number | null>(null);\n  const elapsedRef = useRef<number>(0);\n  const lastTsRef = useRef<number>(0);\n  const rafRef = useRef<number | undefined>(undefined);\n  const pausedRef = useRef(paused);\n  const openRef = useRef(false);\n  const liftingRef = useRef(false);\n\n  useEffect(() => {\n    pausedRef.current = paused;\n  }, [paused]);\n\n  useEffect(() => {\n    setMounted(true);\n    const mq = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n    setReduced(mq.matches);\n    const onChange = () => setReduced(mq.matches);\n    mq.addEventListener(\"change\", onChange);\n    return () => mq.removeEventListener(\"change\", onChange);\n  }, []);\n\n  // --- token read + re-read on theme flip ------------------------------\n  const applyStaticColors = () => {\n    const t = tokensRef.current;\n    if (!t) return;\n    // --border is a separator token (~1.1:1 against --background in light\n    // theme) — unusable as fabric ink. The scallop's form is modelled in\n    // VALUE against the --ns-muted body instead: shadow shades toward\n    // --foreground, highlight toward --background, both layered as\n    // partial opacity over the opaque muted base so it reads in both\n    // polarities. Ropes are thin --foreground strokes.\n    if (trackRef.current) trackRef.current.setAttribute(\"fill\", t.muted);\n    if (bgRectRef.current) bgRectRef.current.setAttribute(\"fill\", t.background);\n    if (markerRef.current) markerRef.current.setAttribute(\"fill\", t.muted);\n    for (const base of swagBaseRefs.current) base?.setAttribute(\"fill\", t.muted);\n    for (const rope of ropeRefs.current) rope?.setAttribute(\"stroke\", t.foreground);\n    for (const refs of stopRefs.current) {\n      refs.base0?.setAttribute(\"stop-color\", t.muted);\n      refs.base1?.setAttribute(\"stop-color\", t.muted);\n      refs.hiIn?.setAttribute(\"stop-color\", t.background);\n      refs.hiPeak?.setAttribute(\"stop-color\", t.background);\n      refs.shPeak?.setAttribute(\"stop-color\", t.foreground);\n      refs.shOut?.setAttribute(\"stop-color\", t.foreground);\n    }\n  };\n\n  // useLayoutEffect, not useEffect: runs before the browser's first paint,\n  // so the fabric never has a frame to render in the wrong theme's colors\n  // before the token read lands — no hardcoded fallback palette needed.\n  useLayoutEffect(() => {\n    const derive = () => {\n      const cs = getComputedStyle(document.documentElement);\n      const read = (name: string) => cs.getPropertyValue(name).trim();\n      tokensRef.current = {\n        muted: read(\"--ns-muted\"),\n        foreground: read(\"--foreground\"),\n        background: read(\"--background\"),\n      };\n      applyStaticColors();\n    };\n    derive();\n    const mo = new MutationObserver(derive);\n    mo.observe(document.documentElement, { attributes: true, attributeFilter: [\"class\"] });\n    return () => mo.disconnect();\n  }, [mounted]);\n\n  /** Belly path for swag i between lift lines i and i+1, at hem heights\n   *  (yL, yR) and additional droop depth `depth` beyond their average. */\n  const swagPath = (i: number, yL: number, yR: number, depth: number): string => {\n    const x0 = lineX(i);\n    const x1 = lineX(i + 1);\n    const midY = (yL + yR) / 2 + depth;\n    const c1x = x0 + (x1 - x0) / 3;\n    const c2x = x0 + ((x1 - x0) * 2) / 3;\n    return (\n      `M ${x0} ${TRACK_Y} L ${x1} ${TRACK_Y} L ${x1} ${yR} ` +\n      `C ${c2x} ${midY} ${c1x} ${midY} ${x0} ${yL} Z`\n    );\n  };\n\n  // --- draw one frame (refs-only, no React state on the hot path) -------\n  const draw = (elapsedMs: number) => {\n    const breathPhaseBase = (elapsedMs / BREATH_PERIOD_MS) * Math.PI * 2;\n\n    let progress = 0; // 0 = closed/lowered, 1 = fully hoisted (may briefly exceed 1: rope give)\n    if (liftStartRef.current !== null) {\n      // Clamped on both ends: the reduced-motion hard cut seeds\n      // liftStartRef in the past (peakElapsed - LIFT_MS) so this same\n      // draw() call lands exactly at frac=1, but if reduced motion later\n      // toggles off mid-open, elapsedMs restarts near 0 — an unclamped\n      // low end would read as negative frac and drop the curtain back\n      // closed with the trigger button already unmounted.\n      const frac = Math.max(0, Math.min(1, (elapsedMs - liftStartRef.current) / LIFT_MS));\n      progress = openRef.current ? 1 : easeRise(frac);\n      if (frac >= 1 && !openRef.current) {\n        openRef.current = true;\n        liftingRef.current = false;\n        setOpen(true);\n        setLifting(false);\n        onLiftComplete?.();\n      }\n    }\n    const clamped = Math.max(0, Math.min(1, progress));\n\n    const hemYs: number[] = [];\n    for (let i = 0; i < LIFT_LINES; i++) {\n      // Slight per-line stagger so the hoist reads as several ropes\n      // gathering together rather than one perfectly rigid batten.\n      const stagger = ((i % 3) - 1) * 0.035;\n      const local = Math.max(0, Math.min(1, progress + stagger * clamped));\n      hemYs.push(REST_HEM_Y + (LIFT_HEM_Y - REST_HEM_Y) * local);\n      const rope = ropeRefs.current[i];\n      if (rope) {\n        rope.setAttribute(\"x1\", String(lineX(i)));\n        rope.setAttribute(\"x2\", String(lineX(i)));\n        rope.setAttribute(\"y1\", String(TRACK_Y));\n        rope.setAttribute(\"y2\", String(hemYs[i]));\n      }\n    }\n\n    const depthBase = REST_SWAG_DEPTH + (LIFT_SWAG_DEPTH - REST_SWAG_DEPTH) * clamped;\n    const breathAmpPx = Math.max(8, depthBase * BREATH_AMP_FRAC);\n\n    for (let i = 0; i < SWAGS; i++) {\n      const depthPhase = breathPhaseBase + i * 0.53;\n      const depth = depthBase + breathAmpPx * Math.sin(depthPhase);\n      const d = swagPath(i, hemYs[i], hemYs[i + 1], depth);\n      // Opaque base first, gradient overlay second: the gradient dips to\n      // near-zero opacity at its crease by design (that's what lets the\n      // highlight/shadow band read as a real fold, not a flat wash), which\n      // would otherwise leak whatever sits behind the fabric — the track,\n      // ropes, and the gate's open-state marker — straight through at\n      // every crease, at every height, not just at the hem.\n      const base = swagBaseRefs.current[i];\n      if (base) base.setAttribute(\"d\", d);\n      const path = swagPathRefs.current[i];\n      if (path) path.setAttribute(\"d\", d);\n\n      // Same \"rewrite the gradient stops every frame\" idiom as\n      // curtain-traveler-draw's pleats, on its own phase offset per swag\n      // so the shaded crease shifts independently of the belly droop.\n      const creasePhase = depthPhase + 1.1;\n      const crease = 0.5 + 0.08 * Math.sin(creasePhase);\n      const refs = stopRefs.current[i];\n      refs?.hiIn?.setAttribute(\"offset\", String(crease - 0.22));\n      refs?.hiPeak?.setAttribute(\"offset\", String(crease - 0.03));\n      refs?.shPeak?.setAttribute(\"offset\", String(crease + 0.03));\n      refs?.shOut?.setAttribute(\"offset\", String(crease + 0.22));\n    }\n  };\n\n  // --- reduced motion: one static peak-amplitude frame, no rAF ----------\n  useEffect(() => {\n    if (!mounted || !reduced) return;\n    // Freeze at a quarter-period so the breathing sine sits at its peak,\n    // not the flat t=0 zero-crossing — swags read as gathered fabric with\n    // real belly depth, not flat panels.\n    draw(BREATH_PERIOD_MS / 4);\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [mounted, reduced]);\n\n  // --- normal continuous rAF loop: unconditional idle breathing + lift --\n  useEffect(() => {\n    if (!mounted || reduced) return;\n    lastTsRef.current = performance.now();\n    elapsedRef.current = 0;\n\n    const loop = (ts: number) => {\n      if (!pausedRef.current) {\n        elapsedRef.current += ts - lastTsRef.current;\n        draw(elapsedRef.current);\n      }\n      lastTsRef.current = ts;\n      rafRef.current = requestAnimationFrame(loop);\n    };\n    rafRef.current = requestAnimationFrame(loop);\n    return () => {\n      if (rafRef.current !== undefined) cancelAnimationFrame(rafRef.current);\n    };\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [mounted, reduced]);\n\n  const handleTrigger = () => {\n    if (openRef.current || liftingRef.current) return;\n    if (reduced) {\n      // Hard cut, no tween: jump straight to the hoisted geometry at the\n      // same quarter-period breathing PEAK the resting frozen frame uses,\n      // by reusing draw() itself (same stagger, same per-swag belly and\n      // crease phases) rather than duplicating a flattened, uniform-depth\n      // version that would lose the scallop read entirely.\n      const peakElapsed = BREATH_PERIOD_MS / 4;\n      liftStartRef.current = peakElapsed - LIFT_MS;\n      draw(peakElapsed);\n      return;\n    }\n    liftingRef.current = true;\n    setLifting(true);\n    liftStartRef.current = elapsedRef.current;\n  };\n\n  const gid = (i: number) => `${uid}-swag-${i}`;\n\n  const renderGradients = () =>\n    Array.from({ length: SWAGS }, (_, i) => (\n      <linearGradient key={gid(i)} id={gid(i)} x1={lineX(i)} y1=\"0\" x2={lineX(i + 1)} y2=\"0\" gradientUnits=\"userSpaceOnUse\">\n        <stop\n          offset=\"0\"\n          ref={(el) => {\n            stopRefs.current[i].base0 = el;\n          }}\n        />\n        <stop\n          offset=\"0.28\"\n          stopOpacity=\"0\"\n          ref={(el) => {\n            stopRefs.current[i].hiIn = el;\n          }}\n        />\n        <stop\n          offset=\"0.47\"\n          stopOpacity=\"0.5\"\n          ref={(el) => {\n            stopRefs.current[i].hiPeak = el;\n          }}\n        />\n        <stop\n          offset=\"0.53\"\n          stopOpacity=\"0.45\"\n          ref={(el) => {\n            stopRefs.current[i].shPeak = el;\n          }}\n        />\n        <stop\n          offset=\"0.72\"\n          stopOpacity=\"0\"\n          ref={(el) => {\n            stopRefs.current[i].shOut = el;\n          }}\n        />\n        <stop\n          offset=\"1\"\n          ref={(el) => {\n            stopRefs.current[i].base1 = el;\n          }}\n        />\n      </linearGradient>\n    ));\n\n  const renderSwags = () =>\n    Array.from({ length: SWAGS }, (_, i) => (\n      <g key={i}>\n        <path\n          ref={(el) => {\n            swagBaseRefs.current[i] = el;\n          }}\n          d={swagPath(i, REST_HEM_Y, REST_HEM_Y, REST_SWAG_DEPTH)}\n        />\n        <path\n          ref={(el) => {\n            swagPathRefs.current[i] = el;\n          }}\n          d={swagPath(i, REST_HEM_Y, REST_HEM_Y, REST_SWAG_DEPTH)}\n          fill={`url(#${gid(i)})`}\n        />\n      </g>\n    ));\n\n  const renderRopes = () =>\n    Array.from({ length: LIFT_LINES }, (_, i) => (\n      <line\n        key={i}\n        ref={(el) => {\n          ropeRefs.current[i] = el;\n        }}\n        x1={lineX(i)}\n        y1={TRACK_Y}\n        x2={lineX(i)}\n        y2={REST_HEM_Y}\n        strokeWidth={2}\n        opacity={0.4}\n      />\n    ));\n\n  const showButton = !open;\n\n  return (\n    <div\n      ref={rootRef}\n      className={`absolute inset-0 overflow-hidden ${className}`}\n      role={open ? undefined : \"status\"}\n      aria-live={open ? undefined : \"polite\"}\n      aria-hidden={open ? \"true\" : undefined}\n    >\n      {!open ? <span className=\"sr-only\">Loading</span> : null}\n      <svg viewBox={`0 0 ${VW} ${VH}`} preserveAspectRatio=\"none\" className=\"block h-full w-full\" aria-hidden=\"true\" focusable=\"false\">\n        <defs>{renderGradients()}</defs>\n        <rect ref={bgRectRef} x={0} y={0} width={VW} height={VH} />\n        {/* Dedicated open-state marker, covered by the fabric at rest and\n            exposed once every lift line has risen clear of it — its centre\n            is a point we control, not the worst-case centre of the full\n            backing rect. */}\n        <rect\n          data-curtain-open\n          ref={markerRef}\n          x={VW / 2 - MARKER_W / 2}\n          y={MARKER_Y - MARKER_H / 2}\n          width={MARKER_W}\n          height={MARKER_H}\n          rx={4}\n        />\n        <rect ref={trackRef} x={0} y={TRACK_Y - TRACK_H / 2} width={VW} height={TRACK_H} />\n        {renderRopes()}\n        {renderSwags()}\n      </svg>\n      {showButton ? (\n        <button\n          type=\"button\"\n          data-curtain-trigger\n          onClick={handleTrigger}\n          disabled={lifting}\n          className=\"absolute bottom-6 left-1/2 -translate-x-1/2 inline-flex items-center rounded-sm bg-ns-accent px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-ns-accent-hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ns-accent disabled:opacity-60 disabled:pointer-events-none\"\n        >\n          Skip curtain\n        </button>\n      ) : null}\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/curtain-austrian-gather.tsx"
    }
  ],
  "cssVars": {
    "theme": {
      "color-ns-muted": "var(--ns-muted)",
      "color-ns-accent": "var(--ns-accent)",
      "color-ns-accent-hover": "var(--ns-accent-hover)"
    },
    "light": {
      "ns-muted": "#4d4d4d",
      "ns-accent": "#006bff",
      "ns-accent-hover": "#0059d1"
    },
    "dark": {
      "ns-muted": "#8f8f8f"
    }
  },
  "meta": {
    "collection": "loud",
    "tags": [
      "preloader",
      "curtain",
      "route",
      "svg",
      "physics",
      "loading",
      "reveal"
    ],
    "instruction": "Build <CurtainAustrianGather onLiftComplete? paused? className?> as a full-bleed route curtain modeled on a real Austrian (brail) stage drape — the rigging that hoists BOTTOM-UP on parallel vertical lift lines, distinct from a traveler curtain (curtain-traveler-draw), which parts horizontally on a corded track, and a tab curtain (curtain-tab-diagonal), which peels diagonally toward one wing. Because an Austrian drape carries roughly 100% sewn-in fullness, the fabric does not slide flat as the lift lines rise: it SCALLOPS and BUNCHES upward into a row of swags, one swag per lift-line span, each swag belonging to its own pair of lift lines. CLOSED means the fabric hangs low and full-bleed with only a shallow scallop at each swag's hem; hoisting (via the component's own built-in 'Skip curtain' trigger button, marked data-curtain-trigger) raises every lift line together, and as the panel's height shrinks the swag bellies grow dramatically deeper and the fabric visibly bunches toward the header — the signature scalloped-swag silhouette, not a straight parting edge. A small per-lift-line stagger and a brief rope-give overshoot-and-settle at the end of the hoist (an easeRise timeline whose overshoot resolves back to the exact hoisted height rather than extending duration) read as real rope-and-pulley rigging, not a mechanical slide. SVG, NOT CANVAS: each swag is one closed path — a straight top edge pinned to the header track, two vertical sides riding its bounding lift lines, and a cubic-bezier belly for the scallop curve — filled with a per-swag horizontal gradient (a --foreground-tinted shadow band and a --background-tinted highlight band, both layered as partial opacity over the opaque --ns-muted fabric body, never --border, which is a separator token at ~1.1:1 contrast against --background in light theme and effectively invisible used as fabric ink). Lift-line ropes are thin --foreground strokes from the header track down to each swag boundary's current hem height. PALETTE: fabric body in --ns-muted, swag shadow/highlight gradients shade toward --foreground and --background respectively, blocked/revealed page area in --background — --ns-accent never touches the fabric, reserved only for the built-in skip/dismiss button's interaction chrome. Colors are read once via getComputedStyle(document.documentElement) and re-read on a MutationObserver watching documentElement's class — no hex or rgb() literals anywhere in the draw code. IDLE IS UNCONDITIONAL AND PER-SWAG: even fully lowered, every swag's belly depth breathes independently on its own slow ~4.3s sine, phase-offset from its neighbours by index, and each swag's internal gradient crease breathes on its own separate phase offset — individual scallops visibly deepen and relax, and shading shifts within them, frame to frame; this is not a whole-panel opacity or scale pulse, which is exactly what the no-autoplay verifier grades apart. Because the interesting state (hoisted) is non-resting, a gate descriptor (openBy the trigger button, expect a small dedicated marker rect, centred well below the fully-hoisted belly's maximum reach and well above the resting hem, positioned in paint order beneath the ropes and swags — covered by the fabric while lowered, exposed and hittable at its own centre once every lift line has risen clear of it) triggers it for its own screenshot. prefers-reduced-motion holds a single mid-breath frame at peak swag-belly depth, where scallops read as three-dimensional gathered fabric, and cuts hard (no tween, no rAF) between lowered and hoisted on trigger rather than tweening. role=status/aria-live=polite announces a loading state while lowered; the whole overlay goes aria-hidden once fully hoisted so the revealed route becomes the real focus target, and the trigger button unmounts at that point rather than staying focusable behind an aria-hidden ancestor."
  },
  "type": "registry:ui"
}