{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "aurora-canvas",
  "title": "Aurora canvas",
  "description": "The moving aurora curtain of the site hero and footer, drawn on one canvas in a worker at 24 fps. AuroraCanvas shows a CSS still of the curtain before JavaScript runs and where no canvas can draw. Works without the solar wind.",
  "registryDependencies": [
    "@dwellir/dwellir-theme",
    "@dwellir/field-style"
  ],
  "files": [
    {
      "path": "components/aurora/AuroraCanvas.tsx",
      "content": "import AuroraField from './AuroraField';\nimport type { QuietZone } from './quietMask';\n\nexport interface AuroraCanvasProps {\n  /** Layout classes. The canvas fills its nearest positioned ancestor. */\n  className?: string;\n  /** Where text sits on the curtain; the light thins out there. */\n  quiet?: QuietZone;\n  /** The footer's curtain: its lower edge at the foot and a taller crown. */\n  placement?: 'footer';\n}\n\n/**\n * The moving aurora curtain for app screens: a dashboard header, a page\n * backdrop. It is AuroraField with its CSS still on, so the box shows the\n * curtain before JavaScript runs and where no canvas can draw. Decorative:\n * hidden from assistive technology.\n */\nexport function AuroraCanvas({ className, quiet = 'none', placement }: AuroraCanvasProps) {\n  return <AuroraField className={className} quiet={quiet} placement={placement} still />;\n}\n\nexport default AuroraCanvas;\n",
      "type": "registry:component",
      "target": "components/aurora/AuroraCanvas.tsx"
    },
    {
      "path": "components/aurora/AuroraField.tsx",
      "content": "'use client';\n\nimport { useEffect, useRef } from 'react';\nimport { createFrameTask } from '../../lib/aurora/frameTask';\nimport { AURORA_START_TIMEOUT_MS, whenIdle } from '../../lib/aurora/idle';\nimport { canvasDpr, QUALITY_ATTR, readQuality } from '../../lib/aurora/quality';\nimport { createCurtainRenderer } from './curtainRenderer';\nimport { readPalette } from './fieldStyle';\nimport type { FieldPalette } from './fieldTypes';\nimport type { QuietZone } from './quietMask';\n\n/** Canvas pixels per CSS px, at most: the light is smooth, so a dense screen gains nothing. */\nconst MAX_DPR = 1;\n/** A still frame for reduced motion: late enough that the curtain has formed. */\nconst STILL_T = 40;\n\nexport interface AuroraFieldProps {\n  /** Where the page puts its headline; the curtain thins out there. */\n  quiet?: QuietZone;\n  className?: string;\n  /** The footer's curtain: its lower edge at the foot and a taller crown. */\n  placement?: 'footer';\n  /**\n   * Paints a CSS still of the curtain (styles/aurora-canvas.css) until the\n   * canvas can draw: before JavaScript runs, and for good when it cannot.\n   */\n  still?: boolean;\n}\n\ninterface FieldState {\n  palette: FieldPalette;\n  visible: boolean;\n}\n\n/** Starts the curtain on `host`. Returns the cleanup. */\nfunction startField(host: HTMLElement, quiet: QuietZone, placement: AuroraFieldProps['placement']) {\n  const renderer = createCurtainRenderer(host, STILL_T * Math.random(), (live) =>\n    host.toggleAttribute('data-au-live', live),\n  );\n\n  const reduced = window.matchMedia('(prefers-reduced-motion: reduce)');\n  const state: FieldState = { palette: readPalette(host), visible: false };\n\n  const start = () => {\n    if (!state.visible || document.hidden) renderer.hold();\n    else if (reduced.matches || readQuality() === 'still') renderer.still(STILL_T);\n    else renderer.run();\n  };\n\n  const resize = () => {\n    const rect = host.getBoundingClientRect();\n    renderer.configure({\n      width: rect.width,\n      height: rect.height,\n      dpr: canvasDpr(readQuality(), MAX_DPR),\n      quiet,\n      palette: state.palette,\n      placement,\n    });\n  };\n\n  const resizeObserver = new ResizeObserver(resize);\n  resizeObserver.observe(host);\n  const visibility = new IntersectionObserver(([entry]) => {\n    state.visible = entry.isIntersecting;\n    start();\n  });\n  visibility.observe(host);\n\n  // The theme toggle and the quality governor change attributes, not React state.\n  const restyleTask = createFrameTask(() => {\n    state.palette = readPalette(host, state.palette);\n    // The quality level sets the canvas resolution.\n    resize();\n    start();\n  });\n  const restyle = new MutationObserver(restyleTask.schedule);\n  restyle.observe(document.documentElement, {\n    attributes: true,\n    attributeFilter: [QUALITY_ATTR, 'class'],\n  });\n  restyle.observe(document.body, { attributes: true, attributeFilter: ['class'] });\n  document.addEventListener('visibilitychange', start);\n  reduced.addEventListener('change', start);\n\n  return () => {\n    resizeObserver.disconnect();\n    visibility.disconnect();\n    restyle.disconnect();\n    restyleTask.cancel();\n    document.removeEventListener('visibilitychange', start);\n    reduced.removeEventListener('change', start);\n    renderer.dispose();\n    host.removeAttribute('data-au-live');\n  };\n}\n\n/**\n * The aurora curtain on one canvas (curtainEngine, drawStorm), brightened\n * where the solar wind feeds it (curtainStorm). It renders only while on\n * screen, at 24 fps on the renderer's own clock, and draws a single still\n * frame for prefers-reduced-motion and still quality (lib/aurora/quality.ts).\n * Without the solar wind's governor no quality is set, so it draws at full\n * quality: all motion, one canvas pixel per CSS px. Its field time starts at a\n * random point of the first STILL_T seconds and holds while the field is off\n * screen.\n */\nexport default function AuroraField({\n  quiet = 'top-left',\n  className,\n  placement,\n  still = false,\n}: AuroraFieldProps) {\n  const hostRef = useRef<HTMLDivElement | null>(null);\n\n  useEffect(() => {\n    const host = hostRef.current;\n    if (!host) return;\n    // Hydration is one long task: the canvas starts after it, at the first idle moment.\n    let stop = () => {};\n    const cancel = whenIdle(() => {\n      stop = startField(host, quiet, placement);\n    }, AURORA_START_TIMEOUT_MS);\n    return () => {\n      cancel();\n      stop();\n    };\n  }, [quiet, placement]);\n\n  // .au-field (styles/aurora-canvas.css) fills the stage; the renderer appends the canvas.\n  return (\n    <div\n      ref={hostRef}\n      className={className ? `au-field ${className}` : 'au-field'}\n      aria-hidden=\"true\"\n      data-au-field={placement ?? 'hero'}\n      data-au-still={still ? '' : undefined}\n      data-au-quiet={still ? quiet : undefined}\n    />\n  );\n}\n",
      "type": "registry:component",
      "target": "components/aurora/AuroraField.tsx"
    },
    {
      "path": "components/aurora/curtain.worker.ts",
      "content": "import { createFixedClock } from '../../lib/aurora/fixedClock';\nimport { AURORA_FPS } from '../../lib/aurora/frameGate';\nimport { drawStorm } from './curtainEngine';\nimport type { CurtainConfig, CurtainMessage, CurtainMode, CurtainReply } from './curtainProtocol';\nimport { createStormProfile } from './curtainStorm';\n\n/** The longest step the field time takes between two paints, so a stall does not jump. */\nconst MAX_STEP_S = 0.1;\n\ninterface Curtain {\n  ctx: OffscreenCanvasRenderingContext2D;\n  config: CurtainConfig | null;\n  mode: CurtainMode;\n  t: number;\n  profile: ReturnType<typeof createStormProfile>;\n}\n\nconst curtains = new Map<number, Curtain>();\n\nfunction draw(curtain: Curtain) {\n  const { ctx, config, t, profile } = curtain;\n  if (config && config.width > 0 && config.height > 0) {\n    drawStorm({ ...config, ctx, t }, profile.advance());\n  }\n}\n\n// Only running canvases advance. A held curtain has no timer or per-frame message.\nconst clock = createFixedClock(AURORA_FPS, (dt) => {\n  for (const curtain of curtains.values()) {\n    if (curtain.mode.type !== 'run') continue;\n    curtain.t += Math.min(dt, MAX_STEP_S);\n    draw(curtain);\n  }\n});\n\nfunction updateClock() {\n  if ([...curtains.values()].some((curtain) => curtain.mode.type === 'run')) clock.start();\n  else clock.stop();\n}\n\nfunction configure(curtain: Curtain, next: CurtainConfig) {\n  curtain.config = next;\n  const { ctx } = curtain;\n  const width = Math.round(next.width * next.dpr);\n  const height = Math.round(next.height * next.dpr);\n  if (ctx.canvas.width !== width) ctx.canvas.width = width;\n  if (ctx.canvas.height !== height) ctx.canvas.height = height;\n  ctx.setTransform(next.dpr, 0, 0, next.dpr, 0, 0);\n  if (curtain.mode.type !== 'hold') draw(curtain);\n}\n\nfunction setMode(curtain: Curtain, mode: CurtainMode) {\n  curtain.mode = mode;\n  updateClock();\n  if (mode.type === 'still') {\n    curtain.t = mode.t;\n    draw(curtain);\n  }\n}\n\nfunction handle(data: CurtainMessage) {\n  if (data.type === 'init') {\n    const ctx = data.canvas.getContext('2d');\n    if (!ctx) throw new Error('Offscreen 2D context unavailable');\n    curtains.set(data.id, {\n      ctx,\n      config: null,\n      mode: { type: 'hold' },\n      t: data.t,\n      profile: createStormProfile(),\n    });\n    return;\n  }\n  if (data.type === 'dispose') {\n    curtains.delete(data.id);\n    updateClock();\n    return;\n  }\n  const curtain = curtains.get(data.id);\n  if (!curtain) return;\n  if (data.type === 'configure') configure(curtain, data.config);\n  else if (data.type === 'feed') curtain.profile.feed(data.fx, data.amount);\n  else setMode(curtain, data.mode);\n}\n\nself.onmessage = ({ data }: MessageEvent<CurtainMessage>) => {\n  try {\n    handle(data);\n  } catch {\n    curtains.delete(data.id);\n    updateClock();\n    self.postMessage({ type: 'error', id: data.id } satisfies CurtainReply);\n  }\n};\n\nself.postMessage({ type: 'ready' } satisfies CurtainReply);\n",
      "type": "registry:component",
      "target": "components/aurora/curtain.worker.ts"
    },
    {
      "path": "components/aurora/curtainEngine.ts",
      "content": "/**\n * The hero's aurora curtain, drawn as continuous light.\n * Each column of a small buffer gets a lower edge, a ray height and a\n * brightness (a slow fold times a fine ray texture). Each pixel above the edge\n * then glows by its altitude: green low, violet above, a rose and gold crown.\n * The buffer is half the canvas width, so rays stay fine, and a quarter of its\n * height, because the light is smooth vertically; it is scaled up with\n * smoothing. Every sway cycle is 40 s or longer.\n */\n\nimport { STORM_EDGE, stormAt } from './curtainStorm';\nimport {\n  type EngineInput,\n  type FieldContext,\n  type FieldPalette,\n  fieldCanvas,\n  mix,\n  type Rgb,\n  TAU,\n} from './fieldTypes';\nimport { applyQuiet } from './quietMask';\n\nconst SCALE_X = 2;\nconst SCALE_Y = 4;\nexport const LUT_SIZE = 128;\n/** Light under the lower edge fades over this fraction of the height. */\nconst UNDER = 0.02;\nconst WHITE: Rgb = [255, 255, 255];\n\ninterface CurtainSettings {\n  /** Lower edge, as a fraction of the height. */\n  base: number;\n  /** Highest light above the edge, as a fraction of the height. */\n  reach: number;\n  gain: number;\n  /** How long the upper rays keep their light. */\n  sustain: number;\n}\n\n/**\n * A quiet curtain at rest, so the wind's brightenings read. Its lower edge\n * sits behind the dashboard window, so it shimmers through the frosted glass.\n */\nconst STORM: CurtainSettings = { base: STORM_EDGE, reach: 0.5, gain: 0.82, sustain: 1 };\n/** Keep the base at the foot and fit the crown above the islands. */\nconst FOOTER: CurtainSettings = { base: 0.92, reach: 0.86, gain: 0.82, sustain: 1.6 };\n\n/** Per-column values for one frame. Heights are fractions of the canvas height. */\ninterface Columns {\n  edge: Float32Array;\n  decay: Float32Array;\n  glow: Float32Array;\n}\n\ninterface CurtainState {\n  buffer: HTMLCanvasElement | OffscreenCanvas;\n  bufferCtx: FieldContext;\n  image: ImageData;\n  columns: Columns;\n  /** Colour by altitude: RGB triplets from the edge (0) to the crown (1). */\n  lut: Uint8ClampedArray;\n  lutKey: string;\n}\n\nconst states = new WeakMap<FieldContext, CurtainState>();\n\ninterface Stop {\n  at: number;\n  color: Rgb;\n}\n\nfunction sampleStops(stops: Stop[], at: number): Rgb {\n  let next = 1;\n  while (next < stops.length - 1 && stops[next].at < at) next += 1;\n  const from = stops[next - 1];\n  const to = stops[next];\n  const k = Math.max(0, Math.min(1, (at - from.at) / (to.at - from.at)));\n  return mix(from.color, to.color, k);\n}\n\n/** Green low, violet above, rose and a gold hint at the crown. The edge burns whiter at night. */\nexport function buildLut(palette: FieldPalette) {\n  const [green, violet, rose, gold] = palette.hues;\n  const stops: Stop[] = [\n    { at: 0, color: palette.dark ? mix(green, WHITE, 0.3) : green },\n    { at: 0.07, color: green },\n    { at: 0.26, color: green },\n    { at: 0.5, color: violet },\n    { at: 0.74, color: rose },\n    { at: 0.96, color: gold },\n  ];\n  const lut = new Uint8ClampedArray(LUT_SIZE * 3);\n  for (let i = 0; i < LUT_SIZE; i += 1) lut.set(sampleStops(stops, i / (LUT_SIZE - 1)), i * 3);\n  return lut;\n}\n\nfunction createState(w: number, h: number): CurtainState | null {\n  const buffer = fieldCanvas(w, h);\n  const bufferCtx = buffer.getContext('2d') as FieldContext | null;\n  if (!bufferCtx) return null;\n  return {\n    buffer,\n    bufferCtx,\n    image: bufferCtx.createImageData(w, h),\n    columns: { edge: new Float32Array(w), decay: new Float32Array(w), glow: new Float32Array(w) },\n    lut: new Uint8ClampedArray(LUT_SIZE * 3),\n    lutKey: '',\n  };\n}\n\nfunction getState(ctx: FieldContext, palette: FieldPalette, w: number, h: number) {\n  let state = states.get(ctx);\n  if (!state || state.image.width !== w || state.image.height !== h) {\n    state = createState(w, h) ?? undefined;\n    if (!state) return null;\n    states.set(ctx, state);\n  }\n  const key = `${palette.hues.flat().join(',')}:${palette.dark}`;\n  if (state.lutKey !== key) {\n    state.lut = buildLut(palette);\n    state.lutKey = key;\n  }\n  return state;\n}\n\n/**\n * Fine rays: two warped sine combs in CSS pixels, so a ray keeps its width on\n * any canvas size. The warp slides slowly, so rays drift and merge.\n */\nexport function rayTexture(px: number, t: number) {\n  const slide = (t / 46) * TAU;\n  const a = 0.5 + 0.5 * Math.sin(px / 3.4 + 2.4 * Math.sin(px / 67 + slide));\n  const b = 0.5 + 0.5 * Math.sin(px / 2.1 - 1.8 * Math.sin(px / 41 - slide * 0.7) + 1.3);\n  const cluster = 0.5 + 0.5 * Math.sin(px / 29 + 1.6 * Math.sin(px / 173 - slide * 0.5));\n  return (0.6 * a ** 3 + 0.4 * b ** 4) * (0.3 + 0.7 * cluster ** 2);\n}\n\n/**\n * `energy`: the wind's feed across the width (curtainStorm). Where it is high\n * the curtain brightens, its rays lengthen and its edge dips.\n */\nfunction fillColumns(\n  columns: Columns,\n  settings: CurtainSettings,\n  input: EngineInput,\n  energy: Float32Array,\n) {\n  const { width, height, t } = input;\n  const count = columns.edge.length;\n  const aspect = width / height;\n  const sway = (t / 84) * TAU;\n  const ripple = (t / 56) * TAU;\n  const drift = (t / 70) * TAU;\n  for (let c = 0; c < count; c += 1) {\n    const x = (c + 0.5) / count;\n    const u = x * aspect;\n    // The fold: where the curtain turns towards the viewer it is seen edge-on and glows.\n    const fold = 0.5 + 0.5 * Math.sin(u * 2.1 + drift + 1.3);\n    const rays = rayTexture(x * width, t);\n    const e = stormAt(energy, x);\n    columns.edge[c] =\n      settings.base +\n      0.07 * Math.sin(u * 1.3 + sway) +\n      0.022 * Math.sin(u * 4 - ripple + 0.8) +\n      0.012 * e;\n    columns.glow[c] = (0.24 + 0.76 * fold) * (0.5 + 0.6 * rays) * (1 + 0.5 * e);\n    columns.decay[c] =\n      settings.reach * (0.16 + 0.22 * fold + 0.24 * rays) * (1 + 0.9 * e) * settings.sustain;\n  }\n}\n\n/** Brightness at a height above the edge: bright at the edge, fading up to the crown. */\nfunction glowAt(lift: number, decay: number, reach: number) {\n  if (lift < 0) return Math.exp(lift / UNDER);\n  const relativeLift = lift / reach;\n  const squaredLift = relativeLift * relativeLift;\n  return Math.exp(-lift / decay) * (1 - squaredLift * squaredLift);\n}\n\nfunction paintPixels(state: CurtainState, settings: CurtainSettings, gain: number) {\n  const { image, columns, lut } = state;\n  const { data, width: w, height: h } = image;\n  const { reach } = settings;\n  data.fill(0);\n  for (let row = 0; row < h; row += 1) {\n    const y = (row + 0.5) / h;\n    for (let c = 0; c < w; c += 1) {\n      const lift = columns.edge[c] - y;\n      if (lift < -UNDER * 4 || lift > reach) continue;\n      const i = (row * w + c) * 4;\n      const k = Math.round(Math.max(0, lift / reach) * (LUT_SIZE - 1)) * 3;\n      data[i] = lut[k];\n      data[i + 1] = lut[k + 1];\n      data[i + 2] = lut[k + 2];\n      data[i + 3] = 255 * gain * columns.glow[c] * glowAt(lift, columns.decay[c], reach);\n    }\n  }\n}\n\nfunction paintCurtain(input: EngineInput, settings: CurtainSettings, energy: Float32Array) {\n  const { ctx, width, height, quiet, palette } = input;\n  const w = Math.max(1, Math.ceil(width / SCALE_X));\n  const h = Math.max(1, Math.ceil(height / SCALE_Y));\n  const state = getState(ctx, palette, w, h);\n  if (!state) return;\n  fillColumns(state.columns, settings, input, energy);\n  // On a light ground the same alpha reads paler, so day light gets more of it.\n  paintPixels(state, settings, settings.gain * (palette.dark ? 1 : 1.25));\n  state.bufferCtx.putImageData(state.image, 0, 0);\n  applyQuiet(state.bufferCtx, w, h, quiet);\n\n  ctx.clearRect(0, 0, width, height);\n  ctx.imageSmoothingEnabled = true;\n  ctx.imageSmoothingQuality = 'high';\n  ctx.drawImage(state.buffer, 0, 0, w * SCALE_X, h * SCALE_Y);\n}\n\n/** The curtain, fed by the solar wind (curtainStorm). */\nexport function drawStorm(input: EngineInput, energy: Float32Array) {\n  paintCurtain(input, input.placement === 'footer' ? FOOTER : STORM, energy);\n}\n",
      "type": "registry:component",
      "target": "components/aurora/curtainEngine.ts"
    },
    {
      "path": "components/aurora/curtainProtocol.ts",
      "content": "import type { EngineInput } from './fieldTypes';\n\nexport type CurtainConfig = Omit<EngineInput, 'ctx' | 't'> & { dpr: number };\n\n/** run: paint on the worker's own clock; hold: stop, keeping the time; still: one frame at t. */\nexport type CurtainMode = { type: 'run' } | { type: 'hold' } | { type: 'still'; t: number };\n\nexport type CurtainCommand =\n  | { type: 'init'; canvas: OffscreenCanvas; t: number }\n  | { type: 'configure'; config: CurtainConfig }\n  | { type: 'feed'; fx: number; amount: number }\n  | { type: 'mode'; mode: CurtainMode }\n  | { type: 'dispose' };\n\nexport type CurtainMessage = CurtainCommand & { id: number };\n\nexport type CurtainReply = { type: 'ready' } | { type: 'error'; id: number };\n",
      "type": "registry:component",
      "target": "components/aurora/curtainProtocol.ts"
    },
    {
      "path": "components/aurora/curtainRenderer.ts",
      "content": "import { onAuroraPaint } from '../../lib/aurora/frameLoop';\nimport { drawStorm } from './curtainEngine';\nimport type { CurtainCommand, CurtainConfig, CurtainMode, CurtainReply } from './curtainProtocol';\nimport { createStormProfile, setStormSink } from './curtainStorm';\nimport { connectCurtainWorker } from './curtainWorkerConnection';\n\n/** The longest step the field time takes between two paints, so a pause off screen does not jump. */\nconst MAX_STEP_MS = 100;\n\n/**\n * Owns the curtain's canvas. With a worker, the worker paints on its own clock\n * and the main thread only sends changes: size, mode and the wind's feeds. A\n * main-thread canvas takes over when workers or transfer are unavailable, or\n * when the worker fails. `onLive` hears whether a canvas can draw: true once\n * the worker owns it or the main thread has a 2D context, false when neither.\n */\nexport function createCurtainRenderer(\n  host: HTMLElement,\n  seed: number,\n  onLive: (live: boolean) => void = () => {},\n) {\n  let canvas = document.createElement('canvas');\n  host.append(canvas);\n  let ctx: CanvasRenderingContext2D | null = null;\n  let worker: ReturnType<typeof connectCurtainWorker> | null = null;\n  let ready = false;\n  let transferred = false;\n  let disposed = false;\n  let config: CurtainConfig | null = null;\n  let mode: CurtainMode = { type: 'hold' };\n  let timeout = 0;\n  // The main-thread canvas keeps its own time and light.\n  let t = seed;\n  let last = 0;\n  let stopFrame = () => {};\n  const profile = createStormProfile();\n\n  const post = (message: CurtainCommand, transfer: Transferable[] = []) => {\n    worker?.post(message, transfer);\n  };\n\n  const feed = (fx: number, amount: number) => {\n    if (mode.type !== 'run') return;\n    if (worker) {\n      if (ready) post({ type: 'feed', fx, amount });\n    } else profile.feed(fx, amount);\n  };\n  setStormSink(canvas, feed);\n\n  const sizeFallback = () => {\n    if (!config || !ctx) return;\n    const width = Math.round(config.width * config.dpr);\n    const height = Math.round(config.height * config.dpr);\n    if (canvas.width !== width) canvas.width = width;\n    if (canvas.height !== height) canvas.height = height;\n    ctx.setTransform(config.dpr, 0, 0, config.dpr, 0, 0);\n  };\n\n  const drawFallback = () => {\n    if (ctx && config && config.width > 0 && config.height > 0) {\n      drawStorm({ ...config, ctx, t }, profile.advance());\n    }\n  };\n\n  const tickFallback = (now: number) => {\n    t += Math.max(0, Math.min(now - last, MAX_STEP_MS)) / 1000;\n    last = now;\n    drawFallback();\n  };\n\n  const applyFallbackMode = () => {\n    stopFrame();\n    stopFrame = () => {};\n    if (mode.type === 'run') {\n      last = performance.now();\n      stopFrame = onAuroraPaint(tickFallback);\n      return;\n    }\n    if (mode.type === 'still') t = mode.t;\n    if (mode.type !== 'hold') drawFallback();\n  };\n\n  const fallback = () => {\n    window.clearTimeout(timeout);\n    worker?.dispose();\n    worker = null;\n    ready = false;\n    if (disposed) return;\n    if (transferred) {\n      const replacement = document.createElement('canvas');\n      canvas.replaceWith(replacement);\n      setStormSink(canvas, null);\n      canvas = replacement;\n      setStormSink(canvas, feed);\n    }\n    ctx = canvas.getContext('2d');\n    onLive(ctx !== null);\n    sizeFallback();\n    applyFallbackMode();\n  };\n\n  const connect = () => {\n    const offscreen = canvas.transferControlToOffscreen();\n    transferred = true;\n    post({ type: 'init', canvas: offscreen, t: seed }, [offscreen]);\n    if (config) post({ type: 'configure', config });\n    post({ type: 'mode', mode });\n    ready = true;\n    window.clearTimeout(timeout);\n    onLive(true);\n  };\n\n  const receive = (data: CurtainReply) => {\n    if (disposed || !worker) return;\n    if (data.type === 'error') {\n      fallback();\n      return;\n    }\n    try {\n      connect();\n    } catch {\n      fallback();\n    }\n  };\n\n  try {\n    const disabled = new URLSearchParams(window.location.search).get('auroraWorker') === 'off';\n    if (disabled || !canvas.transferControlToOffscreen || typeof Worker === 'undefined') {\n      fallback();\n    } else {\n      worker = connectCurtainWorker(receive);\n      timeout = window.setTimeout(fallback, 5000);\n    }\n  } catch {\n    fallback();\n  }\n\n  const setMode = (next: CurtainMode) => {\n    if (\n      mode.type === next.type &&\n      (next.type !== 'still' || (mode.type === 'still' && mode.t === next.t))\n    )\n      return;\n    mode = next;\n    if (ready) post({ type: 'mode', mode });\n    else if (!worker) applyFallbackMode();\n  };\n\n  return {\n    get canvas() {\n      return canvas;\n    },\n    configure(next: CurtainConfig) {\n      config = next;\n      if (ready) post({ type: 'configure', config });\n      else if (!worker) {\n        sizeFallback();\n        if (mode.type !== 'hold') drawFallback();\n      }\n    },\n    /** Paints on the renderer's clock until held. */\n    run() {\n      setMode({ type: 'run' });\n    },\n    /** Stops painting; the field time holds. */\n    hold() {\n      setMode({ type: 'hold' });\n    },\n    /** One frame at field time `still`, for reduced motion and still quality. */\n    still(at: number) {\n      setMode({ type: 'still', t: at });\n    },\n    dispose() {\n      disposed = true;\n      stopFrame();\n      window.clearTimeout(timeout);\n      worker?.dispose();\n      setStormSink(canvas, null);\n      canvas.remove();\n    },\n  };\n}\n",
      "type": "registry:component",
      "target": "components/aurora/curtainRenderer.ts"
    },
    {
      "path": "components/aurora/curtainStorm.ts",
      "content": "/**\n * Substorm: the curtain's charge across its width. The solar wind\n * (wind/storm) feeds a hero curtain where its flux lines cross the curtain's\n * lower edge or strike inside the hero; the curtain (curtainEngine,\n * drawStorm) brightens and lengthens its rays there. A feed charges the\n * stretch at once, the light swells towards the charge over about half a\n * second, and the charge fades over a few seconds, so brightenings rise and\n * settle instead of flashing. Without the wind, nothing feeds the curtain and\n * it stays at rest.\n */\n\n/** The curtain's lower edge in a hero canvas, as a fraction of its height (behind the dashboard). */\nexport const STORM_EDGE = 0.64;\nconst BINS = 96;\n/** Seconds for the charge to fall to 1/e, and for the light to close 63% of its gap to the charge. */\nconst FADE_S = 3;\nconst RISE_S = 0.5;\nconst MAX_CHARGE = 2.4;\n/** A feed spreads over this many bins either side (one standard deviation). */\nconst SPREAD = 3.5;\n\n/**\n * One curtain's charge and light across its width. The curtain's renderer\n * owns it (in its worker when it has one) and brings it up to date each paint.\n */\nexport function createStormProfile() {\n  const charge = new Float32Array(BINS);\n  const energy = new Float32Array(BINS);\n  let stamp = performance.now();\n  return {\n    /** Adds energy at a fraction of the width, spread over a few bins. */\n    feed(fx: number, amount: number) {\n      if (fx < 0 || fx > 1 || amount <= 0) return;\n      const centre = fx * (BINS - 1);\n      const from = Math.max(0, Math.floor(centre - SPREAD * 2.5));\n      const to = Math.min(BINS - 1, Math.ceil(centre + SPREAD * 2.5));\n      for (let i = from; i <= to; i += 1) {\n        const d = (i - centre) / SPREAD;\n        charge[i] = Math.min(MAX_CHARGE, charge[i] + amount * Math.exp(-0.5 * d * d));\n      }\n    },\n    /** The light across the width, brought up to now. Sample it with stormAt. */\n    advance(): Float32Array {\n      const now = performance.now();\n      const dt = Math.max(0, now - stamp) / 1000;\n      const fade = Math.exp(-dt / FADE_S);\n      const rise = 1 - Math.exp(-dt / RISE_S);\n      for (let i = 0; i < BINS; i += 1) {\n        charge[i] *= fade;\n        energy[i] += (charge[i] - energy[i]) * rise;\n      }\n      stamp = now;\n      return energy;\n    },\n  };\n}\n\ntype StormSink = (fx: number, amount: number) => void;\nconst sinks = new WeakMap<Element, StormSink>();\n\n/** Routes the feeds for a hero canvas to its curtain renderer; null removes the route. */\nexport function setStormSink(canvas: Element, sink: StormSink | null) {\n  if (sink) sinks.set(canvas, sink);\n  else sinks.delete(canvas);\n}\n\n/** Adds energy at a fraction of a hero canvas's width. */\nexport function feedStorm(canvas: Element, fx: number, amount: number) {\n  if (fx < 0 || fx > 1 || amount <= 0) return;\n  sinks.get(canvas)?.(fx, amount);\n}\n\n/** Energy at a fraction of the width, interpolated between bins. */\nexport function stormAt(energy: Float32Array, fx: number) {\n  const at = Math.max(0, Math.min(1, fx)) * (BINS - 1);\n  const i = Math.floor(at);\n  const next = energy[Math.min(BINS - 1, i + 1)];\n  return energy[i] + (next - energy[i]) * (at - i);\n}\n",
      "type": "registry:component",
      "target": "components/aurora/curtainStorm.ts"
    },
    {
      "path": "components/aurora/curtainWorkerConnection.ts",
      "content": "import type { CurtainCommand, CurtainMessage, CurtainReply } from './curtainProtocol';\n\ntype Receiver = (reply: CurtainReply) => void;\n\nlet worker: Worker | null = null;\nlet ready = false;\nlet nextId = 0;\nconst receivers = new Map<number, Receiver>();\n\n/** One worker serves every curtain; each canvas keeps its own visibility and field time. */\nexport function connectCurtainWorker(receive: Receiver) {\n  if (!worker) {\n    worker = new Worker(new URL('./curtain.worker.ts', import.meta.url), { type: 'module' });\n    worker.onmessage = ({ data }: MessageEvent<CurtainReply>) => {\n      if (data.type === 'ready') {\n        ready = true;\n        for (const receiver of receivers.values()) receiver(data);\n      } else receivers.get(data.id)?.(data);\n    };\n    const fail = () => {\n      for (const [id, receiver] of [...receivers]) receiver({ type: 'error', id });\n    };\n    worker.onerror = (event) => {\n      event.preventDefault();\n      fail();\n    };\n    worker.onmessageerror = fail;\n  }\n  const shared = worker;\n  const id = ++nextId;\n  receivers.set(id, receive);\n  if (ready) queueMicrotask(() => receivers.get(id)?.({ type: 'ready' }));\n\n  return {\n    post(message: CurtainCommand, transfer: Transferable[]) {\n      shared.postMessage({ ...message, id } satisfies CurtainMessage, transfer);\n    },\n    dispose() {\n      shared.postMessage({ type: 'dispose', id } satisfies CurtainMessage);\n      receivers.delete(id);\n      if (receivers.size === 0) {\n        shared.terminate();\n        worker = null;\n        ready = false;\n      }\n    },\n  };\n}\n",
      "type": "registry:component",
      "target": "components/aurora/curtainWorkerConnection.ts"
    },
    {
      "path": "lib/aurora/fixedClock.ts",
      "content": "/**\n * A fixed-rate clock on timers, for a worker that keeps its own time. It calls\n * `tick` with the seconds since the last call, `fps` times a second, without\n * drifting late. The aurora workers paint on it, so the main thread does no\n * work per frame.\n */\nexport function createFixedClock(fps: number, tick: (dt: number) => void) {\n  const interval = 1000 / fps;\n  let timer: ReturnType<typeof setTimeout> | undefined;\n  let next = 0;\n  let last = 0;\n\n  const run = () => {\n    const now = performance.now();\n    const dt = Math.max(0, now - last) / 1000;\n    last = now;\n    tick(dt);\n    if (timer === undefined) return;\n    next += interval;\n    // Behind by a whole frame: start the cadence again instead of catching up.\n    if (next < now) next = now + interval;\n    timer = setTimeout(run, next - now);\n  };\n\n  return {\n    start() {\n      if (timer !== undefined) return;\n      last = performance.now();\n      next = last;\n      timer = setTimeout(run, 0);\n    },\n    stop() {\n      if (timer !== undefined) clearTimeout(timer);\n      timer = undefined;\n    },\n  };\n}\n",
      "type": "registry:lib",
      "target": "lib/aurora/fixedClock.ts"
    },
    {
      "path": "lib/aurora/frameGate.ts",
      "content": "/** The paint rate of the curtain and the wind (lib/aurora/frameLoop.ts). */\nexport const AURORA_FPS = 24;\nconst PAUSE_MS = 1000;\n\n/** Limits work by elapsed time, independent of the display's refresh rate. */\nexport function createFrameGate(fps: number) {\n  const interval = 1000 / fps;\n  let previous = Number.NaN;\n  let elapsed = 0;\n\n  return {\n    due(now: number) {\n      if (!Number.isFinite(previous)) {\n        previous = now;\n        return true;\n      }\n      const delta = now - previous;\n      previous = now;\n      if (delta > PAUSE_MS) {\n        elapsed = 0;\n        return true;\n      }\n      elapsed += Math.max(0, delta);\n      if (elapsed + 0.5 < interval) return false;\n      elapsed = Math.max(0, elapsed - interval);\n      return true;\n    },\n    /** Milliseconds from `now` until `due` returns true. */\n    wait(now: number) {\n      if (!Number.isFinite(previous)) return 0;\n      return Math.max(0, interval - 0.5 - elapsed - Math.max(0, now - previous));\n    },\n    reset() {\n      previous = Number.NaN;\n      elapsed = 0;\n    },\n  };\n}\n",
      "type": "registry:lib",
      "target": "lib/aurora/frameGate.ts"
    },
    {
      "path": "lib/aurora/frameLoop.ts",
      "content": "import { AURORA_FPS, createFrameGate } from './frameGate';\n\ntype FrameCallback = (now: number) => void;\n/** Milliseconds until the subscriber needs its next frame; 0 for the next one. */\ntype FrameWait = (now: number) => number;\n\n/**\n * Every requested frame runs the whole rendering pipeline, so the loop asks\n * only for the frames a subscriber uses, and sleeps on a timer in between.\n * A wait under this is due: a timer that short fires late, not sooner.\n */\nconst MIN_TIMER_MS = 1;\n\nconst everyFrame = () => 0;\nconst frameCallbacks = new Map<FrameCallback, FrameWait>();\nconst paintCallbacks = new Set<FrameCallback>();\n/** The curtain and the wind paint on one shared cadence, so they share frames. */\nconst paintGate = createFrameGate(AURORA_FPS);\nlet frame = 0;\nlet timer = 0;\nlet ticking = false;\n\nfunction subscribers() {\n  return frameCallbacks.size + paintCallbacks.size;\n}\n\n/** Milliseconds until a frame subscriber needs a frame. */\nfunction frameWaitMs(now: number) {\n  let wait = Number.POSITIVE_INFINITY;\n  for (const frameWait of frameCallbacks.values()) wait = Math.min(wait, frameWait(now));\n  return wait;\n}\n\nfunction paintWaitMs(now: number) {\n  return paintCallbacks.size > 0 ? paintGate.wait(now) : Number.POSITIVE_INFINITY;\n}\n\n/*\n * A paint alone runs from the timer, without an animation frame: the curtain\n * and the wind only read geometry and post to their workers, which draw on\n * their own canvases. So an aurora page renders no frames while nothing on it\n * changes. A frame subscriber (the quality probe) still gets real frames, and\n * the paints then share them.\n */\nfunction onTimer() {\n  timer = 0;\n  const now = performance.now();\n  if (frameWaitMs(now) < MIN_TIMER_MS) {\n    frame = requestAnimationFrame(tick);\n    return;\n  }\n  if (paintCallbacks.size > 0 && paintGate.due(now)) {\n    ticking = true;\n    run(paintCallbacks, (callback) => paintCallbacks.has(callback), now);\n    ticking = false;\n  }\n  requestNext();\n}\n\nfunction requestNext() {\n  if (frame || timer || ticking || document.hidden || subscribers() === 0) return;\n  const now = performance.now();\n  const forFrame = frameWaitMs(now);\n  if (forFrame < MIN_TIMER_MS) frame = requestAnimationFrame(tick);\n  else timer = window.setTimeout(onTimer, Math.min(forFrame, paintWaitMs(now)));\n}\n\nfunction run(\n  callbacks: Iterable<FrameCallback>,\n  isSubscribed: (callback: FrameCallback) => boolean,\n  now: number,\n) {\n  for (const callback of [...callbacks]) {\n    if (!isSubscribed(callback)) continue;\n    try {\n      callback(now);\n    } catch (error) {\n      window.reportError(error);\n    }\n  }\n}\n\nfunction tick(now: number) {\n  frame = 0;\n  ticking = true;\n  /*\n   * The gate reads the clock, not the frame's timestamp: a frame can carry the\n   * time it began, before the timer that asked for it fired.\n   */\n  const paint = paintCallbacks.size > 0 && paintGate.due(performance.now());\n  run(frameCallbacks.keys(), (callback) => frameCallbacks.has(callback), now);\n  if (paint) run(paintCallbacks, (callback) => paintCallbacks.has(callback), now);\n  ticking = false;\n  requestNext();\n}\n\nfunction cancel() {\n  cancelAnimationFrame(frame);\n  window.clearTimeout(timer);\n  frame = 0;\n  timer = 0;\n}\n\nfunction onVisibility() {\n  cancel();\n  requestNext();\n}\n\nfunction subscribe(add: () => void, remove: () => void) {\n  if (subscribers() === 0) document.addEventListener('visibilitychange', onVisibility);\n  add();\n  // A new subscriber may need a frame sooner than the scheduled one.\n  cancel();\n  requestNext();\n  return () => {\n    remove();\n    if (subscribers() === 0) {\n      cancel();\n      document.removeEventListener('visibilitychange', onVisibility);\n    }\n  };\n}\n\n/**\n * Calls back on every frame the loop runs. `wait` says when the subscriber\n * needs a frame of its own; without it, the loop runs every display frame.\n */\nexport function onAuroraFrame(callback: FrameCallback, wait: FrameWait = everyFrame) {\n  return subscribe(\n    () => frameCallbacks.set(callback, wait),\n    () => frameCallbacks.delete(callback),\n  );\n}\n\n/** Calls back at the aurora's paint rate (AURORA_FPS), in the same frames for every subscriber. */\nexport function onAuroraPaint(callback: FrameCallback) {\n  return subscribe(\n    () => {\n      if (paintCallbacks.size === 0 && paintGate.wait(performance.now()) === 0) paintGate.reset();\n      paintCallbacks.add(callback);\n    },\n    () => paintCallbacks.delete(callback),\n  );\n}\n",
      "type": "registry:lib",
      "target": "lib/aurora/frameLoop.ts"
    },
    {
      "path": "lib/aurora/frameTask.ts",
      "content": "/**\n * Runs `task` once in the next animation frame, however often it is asked for\n * before then. An observer of the theme or quality attributes reads styles\n * here, not in its callback: there, each read forces the page's style again\n * while the change is still being made.\n */\nexport function createFrameTask(task: () => void) {\n  let frame = 0;\n  return {\n    schedule() {\n      if (frame) return;\n      frame = requestAnimationFrame(() => {\n        frame = 0;\n        task();\n      });\n    },\n    cancel() {\n      cancelAnimationFrame(frame);\n      frame = 0;\n    },\n  };\n}\n",
      "type": "registry:lib",
      "target": "lib/aurora/frameTask.ts"
    },
    {
      "path": "lib/aurora/idle.ts",
      "content": "/**\n * The longest wait for an idle moment before the aurora starts. Until then\n * the curtain shows its CSS still, and the wind is not drawn yet.\n */\nexport const AURORA_START_TIMEOUT_MS = 1500;\n\n/**\n * Calls back at the first idle moment, or after `timeout` ms at most.\n * Without requestIdleCallback (Safari), it calls back in a task of its own.\n * Returns the cancel.\n */\nexport function whenIdle(callback: () => void, timeout: number) {\n  if (typeof window.requestIdleCallback === 'function') {\n    const id = window.requestIdleCallback(callback, { timeout });\n    return () => window.cancelIdleCallback(id);\n  }\n  const id = window.setTimeout(callback, 0);\n  return () => window.clearTimeout(id);\n}\n",
      "type": "registry:lib",
      "target": "lib/aurora/idle.ts"
    },
    {
      "path": "lib/aurora/quality.ts",
      "content": "/**\n * Motion quality of the animated aurora layers, on <html data-au-quality>:\n * - full: effect-specific canvas resolution, and all motion;\n * - lite: fewer wind lines, and no\n *   frosted glass (a blur over a moving layer is drawn again on every frame);\n * - still: no motion, as for prefers-reduced-motion.\n * Without the attribute the level is full. The governor\n * (components/aurora/qualityGovernor.ts) steps the level down while frames\n * stay slow, and the tab keeps that level for its session.\n */\nexport const QUALITY_LEVELS = ['full', 'lite', 'still'] as const;\nexport type Quality = (typeof QUALITY_LEVELS)[number];\n\nexport const QUALITY_ATTR = 'data-au-quality';\n/** sessionStorage: the level the governor reached in this tab. */\nexport const QUALITY_SESSION_KEY = 'dwellir-au-quality';\n/** A level pinned with ?quality= for the browser session. It stops the governor. */\nexport const QUALITY_PIN_COOKIE = 'dwellir-au-quality-pin';\nexport const QUALITY_QUERY_PARAM = 'quality';\n\nfunction isQuality(value: unknown): value is Quality {\n  return typeof value === 'string' && (QUALITY_LEVELS as readonly string[]).includes(value);\n}\n\nexport function readQuality(): Quality {\n  const value = document.documentElement.getAttribute(QUALITY_ATTR);\n  return isQuality(value) ? value : 'full';\n}\n\n/** Canvas pixels per CSS px at this level. */\nexport function canvasDpr(quality: Quality, max = 2) {\n  return quality === 'full' ? Math.min(window.devicePixelRatio || 1, max) : 1;\n}\n\nexport function isQualityPinned() {\n  return new RegExp(`(?:^|; )${QUALITY_PIN_COOKIE}=`).test(document.cookie);\n}\n\n/**\n * Inline script for <head>: applies a pinned level (?quality= or its cookie)\n * or the level this tab reached, before the first paint.\n */\nexport const QUALITY_BOOTSTRAP_SCRIPT = `(() => {\n  try {\n    var allowed = ${JSON.stringify(QUALITY_LEVELS)};\n    var query = new URLSearchParams(window.location.search).get('${QUALITY_QUERY_PARAM}');\n    if (allowed.indexOf(query) !== -1) {\n      document.cookie = '${QUALITY_PIN_COOKIE}=' + query + '; path=/; samesite=lax';\n    }\n    var pin = document.cookie.match(/(?:^|; )${QUALITY_PIN_COOKIE}=([^;]+)/);\n    var level = pin ? pin[1] : window.sessionStorage.getItem('${QUALITY_SESSION_KEY}');\n    if (allowed.indexOf(level) !== -1) document.documentElement.setAttribute('${QUALITY_ATTR}', level);\n  } catch (error) {}\n})();`;\n",
      "type": "registry:lib",
      "target": "lib/aurora/quality.ts"
    }
  ],
  "css": {
    ".au-field": {
      "position": "absolute",
      "inset": "0",
      "pointer-events": "none"
    },
    ".au-field canvas": {
      "display": "block",
      "width": "100%",
      "height": "100%"
    },
    ".au-field[data-au-still]": {
      "--au-still-box": "70%",
      "--au-still-edge": "var(--au-dot)",
      "--au-still-light": "linear-gradient(\n    to bottom,\n    transparent 19%,\n    color-mix(in srgb, var(--au-dot-4) 3%, transparent) 24%,\n    color-mix(in srgb, var(--au-dot-3) 10%, transparent) 37%,\n    color-mix(in srgb, var(--au-dot-2) 20%, transparent) 54%,\n    color-mix(in srgb, var(--au-dot) 34%, transparent) 71%,\n    color-mix(in srgb, var(--au-dot) 52%, transparent) 81%,\n    color-mix(in srgb, var(--au-dot) 70%, transparent) 86%,\n    var(--au-still-edge) 90%\n  )",
      "--au-still-rays": "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='600' height='200' viewBox='0 0 600 200' preserveAspectRatio='none'%3E%3Cfilter id='s' x='0' y='0' width='100%25' height='100%25'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.11 0.004' numOctaves='2' seed='7' stitchTiles='stitch'/%3E%3CfeGaussianBlur stdDeviation='0.6 0'/%3E%3CfeColorMatrix values='0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 1.5 0 0 0 -0.12'/%3E%3C/filter%3E%3Crect width='600' height='200' filter='url(%23s)'/%3E%3C/svg%3E\")",
      "--au-still-wave": "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 600 200' preserveAspectRatio='none'%3E%3Cfilter id='b' x='0' y='-10%25' width='100%25' height='120%25'%3E%3CfeGaussianBlur stdDeviation='0 3'/%3E%3C/filter%3E%3Cpath d='M0 186 C80 178 150 170 240 175 S390 190 470 184 S560 172 600 180 L600 0 L0 0 Z' filter='url(%23b)'/%3E%3C/svg%3E\")",
      "--au-still-fold": "linear-gradient(\n    to right,\n    rgb(0 0 0 / 0.3),\n    rgb(0 0 0 / 0.6) 30%,\n    #000 60%,\n    rgb(0 0 0 / 0.75) 85%,\n    rgb(0 0 0 / 0.55)\n  )",
      "--au-still-quiet": "linear-gradient(#000, #000)",
      "container-type": "size"
    },
    ".au-field[data-au-still][data-au-field='footer']": {
      "--au-still-box": "100%",
      "--au-still-light": "linear-gradient(\n    to bottom,\n    transparent 6%,\n    color-mix(in srgb, var(--au-dot-4) 5%, transparent) 9%,\n    color-mix(in srgb, var(--au-dot-3) 16%, transparent) 28%,\n    color-mix(in srgb, var(--au-dot-2) 30%, transparent) 49%,\n    color-mix(in srgb, var(--au-dot) 46%, transparent) 70%,\n    color-mix(in srgb, var(--au-dot) 66%, transparent) 87%,\n    var(--au-still-edge) 92%\n  )",
      "--au-still-fold": "linear-gradient(\n    to right,\n    rgb(0 0 0 / 0.45),\n    #000 40%,\n    rgb(0 0 0 / 0.3) 85%,\n    rgb(0 0 0 / 0.45)\n  )"
    },
    ":is(.dark, .next-dark-theme) .au-field[data-au-still]": {
      "--au-still-edge": "color-mix(in srgb, var(--au-dot) 70%, #fff)"
    },
    ".au-field[data-au-still][data-au-quiet='top-left']": {
      "--au-still-quiet": "radial-gradient(\n    70% 62% at 0 0,\n    rgb(0 0 0 / 0.1) 66%,\n    #000 100%\n  )"
    },
    ".au-field[data-au-still][data-au-quiet='left']": {
      "--au-still-quiet": "linear-gradient(to right, rgb(0 0 0 / 0.04) 42%, #000 72%)"
    },
    ".au-field[data-au-still][data-au-quiet='center']": {
      "--au-still-quiet": "linear-gradient(\n    to right,\n    #000 8%,\n    rgb(0 0 0 / 0.25) 32%,\n    rgb(0 0 0 / 0.25) 68%,\n    #000 92%\n  )"
    },
    ".au-field[data-au-still]::before": {
      "content": "''",
      "position": "absolute",
      "inset": "0",
      "background": "var(--au-still-light) 0 0 / 100% var(--au-still-box) no-repeat",
      "mask-image": "var(--au-still-rays), var(--au-still-wave), var(--au-still-fold), var(--au-still-quiet)",
      "mask-size": "600px var(--au-still-box),\n    100% var(--au-still-box),\n    100% 100%,\n    100% 100%",
      "mask-repeat": "repeat-x, no-repeat, no-repeat, no-repeat",
      "mask-mode": "luminance, alpha, alpha, alpha",
      "mask-composite": "intersect",
      "transition": "opacity 0.6s ease",
      "pointer-events": "none"
    },
    ":is(.dark, .next-dark-theme) .au-field[data-au-still]::before": {
      "opacity": "0.8"
    },
    ".au-field[data-au-still][data-au-live]::before": {
      "opacity": "0"
    },
    "@container (aspect-ratio < 0.45)": {
      ".au-field[data-au-still]:not([data-au-quiet='center'], [data-au-quiet='none'])::before": {
        "mask-image": "var(--au-still-rays), var(--au-still-wave), var(--au-still-fold),\n      linear-gradient(to bottom, rgb(0 0 0 / 0.05) 62%, #000 86%)"
      }
    },
    ":root, .dark": {
      "--au-dot": "var(--aurora-1)",
      "--au-dot-2": "var(--aurora-2)",
      "--au-dot-3": "var(--aurora-3)",
      "--au-dot-4": "var(--aurora-4)"
    }
  },
  "type": "registry:component"
}