diff --git a/src/app/features/lotus/backgrounds/animAurora.css.ts b/src/app/features/lotus/backgrounds/animAurora.css.ts
new file mode 100644
index 000000000..b86427059
--- /dev/null
+++ b/src/app/features/lotus/backgrounds/animAurora.css.ts
@@ -0,0 +1,49 @@
+import { keyframes } from '@vanilla-extract/css';
+
+// Aurora Flow — a SLOW, gentle pan of layered soft aurora ribbons.
+//
+// The living-aurora illusion is a pure `background-position` drift: each
+// comma-separated gradient layer is authored larger than the viewport
+// (backgroundSize 200%–300%, see animAurora.ts) so there is slack to slide it
+// around. Panning several broad blurred bands by DIFFERENT
+// amounts and along DIFFERENT paths makes the ribbons appear to curl and cross
+// like real northern lights — no single layer ever moves in lockstep.
+//
+// LAYER ORDER (must match animAurora.ts exactly — one position value per layer):
+// 1. green ribbon (drifts a wide, lazy horizontal arc)
+// 2. teal ribbon (drifts on a slower, offset diagonal)
+// 3. violet ribbon (drifts vertically, the "curtain" fold)
+// 4. sky/aqua highlight (small counter-drift for shimmer)
+// 5. calm reading core (STATIC — kept at 50% 50% so the center never moves)
+// 6. vignette (STATIC — kept at 50% 50% so edges never move)
+//
+// SEAMLESS LOOP: every animated layer starts and ends on the SAME position
+// ('0%'/'100%' being identical sample points of the repeating gradient tile),
+// so one period returns each band to its origin with no visible jump. The two
+// static layers list their fixed position at every stop so they never pan.
+//
+// SLOW & GENTLE: paired with a long duration + ease-in-out in animAurora.ts, the
+// motion reads as a barely-perceptible breathing drift, keeping the reading
+// center calm and text crisp.
+//
+// getChatBg adds `willChange: 'background-position'` here and STRIPS the whole
+// `animation` for prefers-reduced-motion / pause-animations, at which point the
+// static `backgroundPosition` authored in animAurora.ts is what shows — already
+// a finished, gorgeous aurora.
+export const auroraFlow = keyframes({
+ '0%': {
+ backgroundPosition: '0% 30%, 100% 70%, 50% 0%, 20% 80%, 50% 50%, 50% 50%',
+ },
+ '25%': {
+ backgroundPosition: '35% 45%, 70% 55%, 55% 35%, 45% 60%, 50% 50%, 50% 50%',
+ },
+ '50%': {
+ backgroundPosition: '65% 60%, 40% 40%, 45% 70%, 70% 35%, 50% 50%, 50% 50%',
+ },
+ '75%': {
+ backgroundPosition: '35% 45%, 70% 55%, 55% 35%, 45% 60%, 50% 50%, 50% 50%',
+ },
+ '100%': {
+ backgroundPosition: '0% 30%, 100% 70%, 50% 0%, 20% 80%, 50% 50%, 50% 50%',
+ },
+});
diff --git a/src/app/features/lotus/backgrounds/animAurora.ts b/src/app/features/lotus/backgrounds/animAurora.ts
new file mode 100644
index 000000000..06385cdbd
--- /dev/null
+++ b/src/app/features/lotus/backgrounds/animAurora.ts
@@ -0,0 +1,81 @@
+import { CSSProperties } from 'react';
+import { ChatBgVariants } from './types';
+import { auroraFlow } from './animAurora.css';
+
+// Aurora Flow — a premium ANIMATED aurora: soft ribbons of northern-lights color
+// slowly drifting and curling over a deep, calm base.
+//
+// CONCEPT
+// Broad, heavily-feathered gradient bands stacked over a deep midnight base, with
+// a gentle vignette that darkens the edges and keeps the reading center calm.
+// The distinct STATIC 'aurora' is a favorite still; this one earns its own slot
+// by MOVING — see animAurora.css.ts, which slowly pans each ribbon along its own
+// path via `background-position` so the curtains appear to fold and cross.
+//
+// LAYER ORDER (must stay in lockstep with auroraFlow's per-layer position list):
+// 1. green ribbon 2. teal ribbon 3. violet ribbon 4. sky highlight
+// 5. calm reading core (static) 6. vignette (static)
+//
+// READABILITY
+// Every ribbon is a wide ellipse fading fully to transparent well before its
+// edge, at low alpha (~0.05–0.13), so no band ever concentrates enough contrast
+// under the message column to threaten WCAG-AA. Layer 5 lifts a soft, even wash
+// through the vertical center — the reading zone — so text always sits on a calm,
+// low-variance field. oklch() keeps every hue perceptually smooth and low-chroma.
+//
+// MOTION / SEAMLESS LOOP
+// backgroundSize is >100% per animated layer, giving room to drift; the keyframe
+// returns every band to its start over one long, ease-in-out period, so the loop
+// is seamless and the motion barely-perceptible. willChange/animation are added
+// (and stripped for reduced-motion) by getChatBg; the static positions below are
+// the finished still that shows when motion is off.
+
+const dark: CSSProperties = {
+ // Deep midnight blue — the polar night sky the aurora glows over.
+ backgroundColor: 'oklch(0.17 0.045 255)',
+ backgroundImage: [
+ // 1. Green ribbon — the signature aurora band.
+ 'radial-gradient(ellipse 70% 45% at 50% 50%, oklch(0.7 0.14 160 / 0.13) 0%, oklch(0.7 0.14 160 / 0.05) 45%, transparent 72%)',
+ // 2. Teal ribbon — cool counterpart, offset.
+ 'radial-gradient(ellipse 80% 40% at 50% 50%, oklch(0.65 0.12 200 / 0.12) 0%, oklch(0.65 0.12 200 / 0.04) 48%, transparent 74%)',
+ // 3. Violet ribbon — the high curtain fold.
+ 'radial-gradient(ellipse 65% 55% at 50% 50%, oklch(0.55 0.13 300 / 0.11) 0%, oklch(0.55 0.13 300 / 0.04) 46%, transparent 70%)',
+ // 4. Sky/aqua highlight — subtle shimmer that counter-drifts.
+ 'radial-gradient(ellipse 55% 35% at 50% 50%, oklch(0.72 0.1 220 / 0.09) 0%, transparent 65%)',
+ // 5. Calm reading core (static) — a soft even wash down the center column so
+ // message text always rests on a low-variance field.
+ 'radial-gradient(ellipse 120% 60% at 50% 50%, oklch(0.2 0.04 255 / 0.5) 0%, transparent 70%)',
+ // 6. Vignette (static) — gently darkens the edges for luminous depth.
+ 'radial-gradient(ellipse 130% 120% at 50% 50%, transparent 55%, oklch(0.12 0.04 260 / 0.55) 100%)',
+ ].join(','),
+ backgroundSize: '260% 240%, 300% 260%, 240% 280%, 220% 200%, 100% 100%, 100% 100%',
+ backgroundPosition: '0% 30%, 100% 70%, 50% 0%, 20% 80%, 50% 50%, 50% 50%',
+ backgroundRepeat: 'no-repeat',
+ animation: `${auroraFlow} 60s ease-in-out infinite`,
+};
+
+const light: CSSProperties = {
+ // Pale cool base — a soft pre-dawn sky the pastel aurora dreams over.
+ backgroundColor: 'oklch(0.97 0.012 240)',
+ backgroundImage: [
+ // 1. Mint ribbon.
+ 'radial-gradient(ellipse 70% 45% at 50% 50%, oklch(0.85 0.08 160 / 0.5) 0%, oklch(0.85 0.08 160 / 0.16) 45%, transparent 72%)',
+ // 2. Sky ribbon.
+ 'radial-gradient(ellipse 80% 40% at 50% 50%, oklch(0.83 0.07 220 / 0.48) 0%, oklch(0.83 0.07 220 / 0.14) 48%, transparent 74%)',
+ // 3. Lilac ribbon — the high curtain fold.
+ 'radial-gradient(ellipse 65% 55% at 50% 50%, oklch(0.82 0.07 300 / 0.42) 0%, oklch(0.82 0.07 300 / 0.12) 46%, transparent 70%)',
+ // 4. Aqua highlight — subtle shimmer that counter-drifts.
+ 'radial-gradient(ellipse 55% 35% at 50% 50%, oklch(0.88 0.06 200 / 0.34) 0%, transparent 65%)',
+ // 5. Calm reading core (static) — a bright even wash down the center column
+ // so dark message text always rests on a light, low-variance field.
+ 'radial-gradient(ellipse 120% 60% at 50% 50%, oklch(0.99 0.005 240 / 0.6) 0%, transparent 70%)',
+ // 6. Vignette (static) — a whisper of cool shade at the edges for depth.
+ 'radial-gradient(ellipse 130% 120% at 50% 50%, transparent 55%, oklch(0.9 0.02 250 / 0.45) 100%)',
+ ].join(','),
+ backgroundSize: '260% 240%, 300% 260%, 240% 280%, 220% 200%, 100% 100%, 100% 100%',
+ backgroundPosition: '0% 30%, 100% 70%, 50% 0%, 20% 80%, 50% 50%, 50% 50%',
+ backgroundRepeat: 'no-repeat',
+ animation: `${auroraFlow} 60s ease-in-out infinite`,
+};
+
+export const animAurora: ChatBgVariants = { dark, light };
diff --git a/src/app/features/lotus/backgrounds/animFireflies.css.ts b/src/app/features/lotus/backgrounds/animFireflies.css.ts
new file mode 100644
index 000000000..7b4c034d7
--- /dev/null
+++ b/src/app/features/lotus/backgrounds/animFireflies.css.ts
@@ -0,0 +1,45 @@
+import { keyframes } from '@vanilla-extract/css';
+
+// Fireflies — a slow, gentle PAN of sparse glowing motes across a warm summer
+// dusk. The scene in animFireflies.ts stacks these background layers:
+// 1. large bright motes — tile 227x227, brightest core+halo, drifts FASTEST
+// 2. medium motes — tile 293x293, dimmer, medium drift
+// 3. tiny far sparks — tile 179x179, faintest, drifts SLOWEST (small step)
+// 4. center vignette (100% 100%) — STATIC
+// 5. warm dusk wash A (100% 100%) — STATIC
+// 6. warm dusk wash B (100% 100%) — STATIC
+//
+// Seamless drift: the single `animation` shorthand shares ONE duration across all
+// layers, so the differing apparent speeds come purely from how FAR each layer
+// travels. For a jump-free loop every mote layer must translate by an EXACT
+// integer multiple of its own tile period in BOTH axes, so the mote re-entering
+// at the wrap is identical to the one that left. Each layer moves exactly one
+// full tile:
+// large : -227 / -227 (1 x 227)
+// medium: -293 / -293 (1 x 293) — bigger tile, same 1-tile move => SLOWER look
+// far : -179 / -179 (1 x 179) — smallest tile, damped by low opacity so it
+// reads as the calm distant layer
+// Because tile sizes differ, one shared 1-tile translation yields three distinct
+// apparent speeds — the wandering-firefly parallax — while every layer lands back
+// on an identical phase at 100% for a perfectly seamless repeat.
+//
+// The diagonal component (both x and y shift) makes motes feel like they wander
+// through the meadow rather than slide flatly. The three static layers (vignette
+// and the two dusk washes) are pinned at '0 0' every frame so the warm ambient
+// glow and the calm reading center never move under the text.
+//
+// The '0%' frame MUST match the static backgroundPosition authored in
+// animFireflies.ts, so when getChatBg STRIPS this animation for
+// prefers-reduced-motion the finished scene of glowing motes shows without a jump.
+export const firefliesDrift = keyframes({
+ '0%': {
+ // large, medium, far, vignette, wash A, wash B
+ backgroundPosition: '0 0, 83px 47px, 131px 101px, 0 0, 0 0, 0 0',
+ },
+ '100%': {
+ // large: 0-227 / 0-227
+ // medium: 83-293 / 47-293
+ // far: 131-179 / 101-179
+ backgroundPosition: '-227px -227px, -210px -246px, -48px -78px, 0 0, 0 0, 0 0',
+ },
+});
diff --git a/src/app/features/lotus/backgrounds/animFireflies.ts b/src/app/features/lotus/backgrounds/animFireflies.ts
new file mode 100644
index 000000000..87d05b171
--- /dev/null
+++ b/src/app/features/lotus/backgrounds/animFireflies.ts
@@ -0,0 +1,98 @@
+import { ChatBgVariants } from './types';
+import { firefliesDrift } from './animFireflies.css';
+
+// Fireflies — a warm summer-dusk meadow. A few soft golden-green motes drift over
+// a deep base, each mote a bright core melting into a warm halo. Sparse by design
+// so the reading column stays clear; the motion is a slow, gentle background-
+// position PAN (see animFireflies.css.ts) that reads as fireflies wandering.
+//
+// Layer stacking order (topmost first — CSS paints image #1 on top):
+// 1. large bright motes — crisp warm core -> warm halo, sparse, largest step
+// 2. medium motes — dimmer, smaller, more of them
+// 3. tiny far sparks — faintest, smallest tile, calm distant layer
+// 4. center vignette — keeps the reading center the calmest area
+// 5. warm dusk wash A — ambient glow, upper
+// 6. warm dusk wash B — ambient glow, lower
+// Mote tiles use coprime-ish sizes (227/293/179) so their repeats never line up
+// and the field reads as scattered, not gridded.
+//
+// getChatBg STRIPS the `animation` for prefers-reduced-motion / pause, so the
+// authored backgroundPosition already composes a finished, gorgeous still scene
+// of glowing motes on its own — the animation only sets them gently adrift.
+export const animFireflies: ChatBgVariants = {
+ // Dark: warm gold-green glows on a deep forest-navy base with a soft vignette.
+ // Cores sit near oklch(0.85 0.13 110); halos fall to a warm amber-green. All
+ // opacities are kept low so message text stays crisp (WCAG-AA) over the field.
+ dark: {
+ backgroundColor: 'oklch(0.17 0.035 175)',
+ backgroundImage: [
+ // 1. large bright motes — golden-green core fading through a warm halo
+ 'radial-gradient(circle at center, oklch(0.85 0.13 110 / 0.55) 1.4px, oklch(0.72 0.14 95 / 0.16) 3px, transparent 6px)',
+ // 2. medium motes — a touch cooler-green, dimmer, more numerous
+ 'radial-gradient(circle at center, oklch(0.82 0.13 128 / 0.40) 1.1px, oklch(0.70 0.12 110 / 0.12) 2.4px, transparent 5px)',
+ // 3. tiny far sparks — faint warm pinpoints, the calm distant layer
+ 'radial-gradient(circle at center, oklch(0.88 0.11 100 / 0.28) 0.8px, transparent 2.4px)',
+ // 4. center vignette — darkens the edges, keeps reading center calmest
+ 'radial-gradient(ellipse 125% 95% at 50% 44%, transparent 40%, oklch(0.10 0.03 175 / 0.55) 100%)',
+ // 5. warm dusk wash A — a low amber-green glow drifting in from upper-right
+ 'radial-gradient(ellipse 140% 120% at 80% 10%, oklch(0.30 0.07 120 / 0.45) 0%, transparent 58%)',
+ // 6. warm dusk wash B — deep teal-navy pooling into the lower-left
+ 'radial-gradient(ellipse 135% 115% at 16% 94%, oklch(0.22 0.05 190 / 0.50) 0%, transparent 60%)',
+ ].join(','),
+ backgroundSize: [
+ '227px 227px', // large motes
+ '293px 293px', // medium motes
+ '179px 179px', // far sparks
+ '100% 100%', // vignette
+ '100% 100%', // wash A
+ '100% 100%', // wash B
+ ].join(','),
+ backgroundPosition: [
+ '0 0', // large (matches firefliesDrift 0%)
+ '83px 47px', // medium (offset breaks alignment)
+ '131px 101px', // far (offset again)
+ '0 0', // vignette (static)
+ '0 0', // wash A (static)
+ '0 0', // wash B (static)
+ ].join(','),
+ animation: `${firefliesDrift} 44s linear infinite`,
+ },
+
+ // Light: a cozy warm dim-dusk. No harsh dots on white — soft amber motes with
+ // gentle halos float on a warm blush->honey gradient. Contrast stays low so the
+ // reading area is comfortable and text remains crisp (WCAG-AA).
+ light: {
+ backgroundColor: 'oklch(0.955 0.02 85)',
+ backgroundImage: [
+ // 1. large amber motes — warm honey core into a soft amber halo
+ 'radial-gradient(circle at center, oklch(0.80 0.11 80 / 0.30) 1.4px, oklch(0.85 0.09 70 / 0.12) 3px, transparent 6px)',
+ // 2. medium motes — slightly greener-gold, softer
+ 'radial-gradient(circle at center, oklch(0.78 0.10 95 / 0.22) 1.1px, oklch(0.86 0.08 85 / 0.10) 2.4px, transparent 5px)',
+ // 3. tiny far sparks — faint warm pinpoints for texture, never noise
+ 'radial-gradient(circle at center, oklch(0.75 0.10 75 / 0.16) 0.8px, transparent 2.4px)',
+ // 4. center vignette — brightens the calm reading center a touch
+ 'radial-gradient(ellipse 125% 95% at 50% 44%, oklch(1 0 0 / 0.40) 30%, transparent 100%)',
+ // 5. warm dusk wash A — honey glow from the upper-right
+ 'radial-gradient(ellipse 140% 120% at 80% 8%, oklch(0.92 0.06 85 / 0.55) 0%, transparent 60%)',
+ // 6. warm dusk wash B — soft rose blush pooling lower-left
+ 'radial-gradient(ellipse 135% 115% at 15% 95%, oklch(0.93 0.05 40 / 0.45) 0%, transparent 62%)',
+ ].join(','),
+ backgroundSize: [
+ '227px 227px', // large motes
+ '293px 293px', // medium motes
+ '179px 179px', // far sparks
+ '100% 100%', // vignette
+ '100% 100%', // wash A
+ '100% 100%', // wash B
+ ].join(','),
+ backgroundPosition: [
+ '0 0', // large (matches firefliesDrift 0%)
+ '83px 47px', // medium
+ '131px 101px', // far
+ '0 0', // vignette (static)
+ '0 0', // wash A (static)
+ '0 0', // wash B (static)
+ ].join(','),
+ animation: `${firefliesDrift} 44s linear infinite`,
+ },
+};
diff --git a/src/app/features/lotus/backgrounds/animPulse.css.ts b/src/app/features/lotus/backgrounds/animPulse.css.ts
new file mode 100644
index 000000000..a2fa1442d
--- /dev/null
+++ b/src/app/features/lotus/backgrounds/animPulse.css.ts
@@ -0,0 +1,39 @@
+import { keyframes } from '@vanilla-extract/css';
+
+// Grid Pulse — a slow "energy" glow that sweeps across a static tech grid.
+//
+// The motif is a crisp thin grid that pulses. Rather than scaling the grid
+// (which shifts every line and reads as a jitter behind text), we keep the grid
+// perfectly still and PAN a single soft radial "bloom" layer diagonally across
+// it. As the bloom drifts, the grid lines it passes over appear to brighten and
+// then settle — a calm travelling pulse, never a flash.
+//
+// Layer mapping (see animPulse.ts — one background-position value per layer):
+// 0. grid core lines (vertical) — STATIC ('0 0')
+// 1. grid core lines (horizontal) — STATIC ('0 0')
+// 2. grid fine sub-lines (V) — STATIC ('0 0')
+// 3. grid fine sub-lines (H) — STATIC ('0 0')
+// 4. TRAVELLING BLOOM — panned here (the only moving layer)
+// 5. base wash / centre glow — STATIC ('0 0')
+// 6. vignette — STATIC ('0 0')
+//
+// Seamless loop: the bloom layer is authored to tile (its backgroundSize in
+// animPulse.ts is 480px — an exact 4x multiple of the 120px grid module, and
+// 8x of the 60px sub-grid). Panning it by EXACTLY one bloom-tile (480px on both
+// axes) returns every pixel to an identical neighbouring tile, so the wrap at
+// 100% is invisible. Diagonal travel (both axes move together) makes the sweep
+// feel organic while still landing on a whole-tile offset.
+//
+// getChatBg adds `willChange: 'background-position'` for the animated case, so a
+// background-position pulse is exactly what the compositor is hinted for. It
+// STRIPS this whole `animation` for prefers-reduced-motion / pause-animations,
+// at which point the static bloom position authored in animPulse.ts is what
+// shows — a finished, gently glowing grid.
+export const gridPulse = keyframes({
+ '0%': {
+ backgroundPosition: '0 0, 0 0, 0 0, 0 0, 0px 0px, 0 0, 0 0',
+ },
+ '100%': {
+ backgroundPosition: '0 0, 0 0, 0 0, 0 0, 480px 480px, 0 0, 0 0',
+ },
+});
diff --git a/src/app/features/lotus/backgrounds/animPulse.ts b/src/app/features/lotus/backgrounds/animPulse.ts
new file mode 100644
index 000000000..fb7d1adf6
--- /dev/null
+++ b/src/app/features/lotus/backgrounds/animPulse.ts
@@ -0,0 +1,121 @@
+import { ChatBgVariants } from './types';
+import { gridPulse } from './animPulse.css';
+
+// Grid Pulse (anim-pulse) — a refined sci-fi grid with a slow energy pulse.
+//
+// Concept: a crisp thin tech grid over which a single soft radial glow drifts
+// diagonally, so the lines it crosses seem to charge and settle — a hypnotic
+// travelling pulse rather than a strobing brightness flash. Three ingredients,
+// exactly per the quality bar:
+// 1. a crisp thin grid — two hairline linear layers (V + H) at a 120px module
+// plus a fainter 60px sub-grid, so the mesh reads as fine machined lattice;
+// 2. a soft bloom layer — one wide, very-low-opacity radial that TRAVELS across
+// the grid (the pulse), authored to tile so the loop is seamless;
+// 3. a radial vignette — keeps the reading centre calm (dark theme darkens it,
+// light theme brightens it) so text always sits on the quietest region.
+//
+// Animation approach & why it's subtle: only ONE layer moves — the bloom — and
+// it moves by pure background-position (the property getChatBg hints via
+// willChange). No line ever shifts, no global brightness flicker, so text never
+// wobbles. The glow itself is barely-there (opacity well under the neon bloom),
+// so the "pulse" is felt as a slow wash of light passing behind the words. 22s
+// per cycle makes it meditative, not busy.
+//
+// Seamless loop: the bloom's backgroundSize is 480px — an exact 4x multiple of
+// the 120px grid module (and 8x of the 60px sub-grid). The keyframe pans it by
+// exactly one 480px tile on both axes, so it wraps onto an identical tile with
+// no visible seam (see animPulse.css.ts).
+//
+// Reduced-motion fallback: getChatBg strips `animation`, leaving the bloom at
+// its authored static position — parked slightly above-centre so the finished
+// frame reads as a deliberately-lit, gently glowing grid rather than a frozen
+// mid-sweep. The grid, wash and vignette are all static regardless, so the
+// still image is already a complete, premium background.
+//
+// Dark vs light: dark is a cool cyan lattice glowing on deep blue-black with a
+// dim bloom and a centre-darkening vignette. Light is a soft slate-blue lattice
+// on pale cool-white with a whisper-faint bloom and a centre-BRIGHTENING
+// vignette, so the reading column lifts toward white. Both keep line + glow
+// opacity low for WCAG-AA legibility in either app theme.
+
+export const animPulse: ChatBgVariants = {
+ // Dark: cyan grid on deep blue-black, a dim energy bloom sweeping through.
+ dark: {
+ backgroundColor: 'oklch(0.16 0.03 240)',
+ backgroundImage: [
+ // 0. grid core — vertical hairlines (cool cyan)
+ 'linear-gradient(90deg, oklch(0.75 0.11 200 / 0.14) 0 1px, transparent 1px)',
+ // 1. grid core — horizontal hairlines
+ 'linear-gradient(0deg, oklch(0.75 0.11 200 / 0.14) 0 1px, transparent 1px)',
+ // 2. fine sub-grid — vertical (fainter, half module)
+ 'linear-gradient(90deg, oklch(0.75 0.11 200 / 0.05) 0 1px, transparent 1px)',
+ // 3. fine sub-grid — horizontal
+ 'linear-gradient(0deg, oklch(0.75 0.11 200 / 0.05) 0 1px, transparent 1px)',
+ // 4. TRAVELLING BLOOM — the pulse: a wide soft cyan glow that drifts
+ 'radial-gradient(circle at 50% 50%, oklch(0.8 0.12 200 / 0.16) 0%, oklch(0.75 0.11 205 / 0.06) 26%, transparent 55%)',
+ // 5. base wash — a faint steady centre glow so the grid never looks flat
+ 'radial-gradient(ellipse 120% 100% at 50% 42%, oklch(0.42 0.07 235 / 0.28) 0%, transparent 62%)',
+ // 6. vignette — darken the edges, keep the reading centre calm & dark
+ 'radial-gradient(ellipse 130% 100% at 50% 46%, transparent 34%, oklch(0.11 0.02 245 / 0.72) 100%)',
+ ].join(','),
+ backgroundSize: [
+ '120px 120px', // grid core V
+ '120px 120px', // grid core H
+ '60px 60px', // sub-grid V (exact 1/2 divisor — re-registers)
+ '60px 60px', // sub-grid H
+ '480px 480px', // bloom (4x module — pans one whole tile, seamless)
+ '100% 100%', // base wash
+ '100% 100%', // vignette
+ ].join(','),
+ backgroundPosition: [
+ '0 0', // grid core V
+ '0 0', // grid core H
+ '0 0', // sub-grid V
+ '0 0', // sub-grid H
+ '120px 40px', // bloom static (reduced-motion) — parked above-centre
+ '0 0', // base wash
+ '0 0', // vignette
+ ].join(','),
+ animation: `${gridPulse} 22s ease-in-out infinite`,
+ },
+
+ // Light: soft slate-blue grid on pale cool-white, a gentle luminance breathe.
+ light: {
+ backgroundColor: 'oklch(0.975 0.006 235)',
+ backgroundImage: [
+ // 0. grid core — vertical hairlines (soft slate-blue)
+ 'linear-gradient(90deg, oklch(0.55 0.08 245 / 0.15) 0 1px, transparent 1px)',
+ // 1. grid core — horizontal hairlines
+ 'linear-gradient(0deg, oklch(0.55 0.08 245 / 0.15) 0 1px, transparent 1px)',
+ // 2. fine sub-grid — vertical (fainter, half module)
+ 'linear-gradient(90deg, oklch(0.55 0.08 245 / 0.055) 0 1px, transparent 1px)',
+ // 3. fine sub-grid — horizontal
+ 'linear-gradient(0deg, oklch(0.55 0.08 245 / 0.055) 0 1px, transparent 1px)',
+ // 4. TRAVELLING BLOOM — a whisper of slate-blue light drifting through
+ 'radial-gradient(circle at 50% 50%, oklch(0.6 0.09 240 / 0.09) 0%, oklch(0.62 0.08 245 / 0.035) 26%, transparent 55%)',
+ // 5. base wash — the faintest cool tint so the grid sits on soft light
+ 'radial-gradient(ellipse 120% 100% at 50% 42%, oklch(0.86 0.03 235 / 0.30) 0%, transparent 62%)',
+ // 6. vignette — brighten the calm reading centre toward white for legibility
+ 'radial-gradient(ellipse 130% 100% at 50% 46%, oklch(1 0 0 / 0.5) 30%, transparent 100%)',
+ ].join(','),
+ backgroundSize: [
+ '120px 120px', // grid core V
+ '120px 120px', // grid core H
+ '60px 60px', // sub-grid V
+ '60px 60px', // sub-grid H
+ '480px 480px', // bloom (4x module — seamless one-tile pan)
+ '100% 100%', // base wash
+ '100% 100%', // vignette
+ ].join(','),
+ backgroundPosition: [
+ '0 0', // grid core V
+ '0 0', // grid core H
+ '0 0', // sub-grid V
+ '0 0', // sub-grid H
+ '120px 40px', // bloom static (reduced-motion) — parked above-centre
+ '0 0', // base wash
+ '0 0', // vignette
+ ].join(','),
+ animation: `${gridPulse} 22s ease-in-out infinite`,
+ },
+};
diff --git a/src/app/features/lotus/backgrounds/animRain.css.ts b/src/app/features/lotus/backgrounds/animRain.css.ts
new file mode 100644
index 000000000..4233eb6aa
--- /dev/null
+++ b/src/app/features/lotus/backgrounds/animRain.css.ts
@@ -0,0 +1,22 @@
+import { keyframes } from '@vanilla-extract/css';
+
+// Digital Rain — a slow vertical PAN of the streak columns.
+//
+// The streak SVG tile is authored 200px tall (see animRain.ts, backgroundSize
+// height = 200px). The falling illusion is a pure background-position translate
+// downward by EXACTLY one tile height (200px) over the cycle, so the loop is
+// perfectly seamless — the pixel at y re-enters where the pixel at y-200 was,
+// which is identical because the tile repeats.
+//
+// Only the first background layer (the streak SVG) is panned; every subsequent
+// comma-separated layer is kept at its authored position ('0 0') so the base
+// gradients / vignette stay put while the rain falls over them. Listing a value
+// per layer is required — a single value would pan ALL layers.
+//
+// getChatBg adds `willChange: 'background-position'` for the animated case, and
+// STRIPS this whole `animation` for reduced-motion, at which point the static
+// backgroundPosition authored in animRain.ts is what shows.
+export const rainFall = keyframes({
+ '0%': { backgroundPosition: '0 0, 0 0, 0 0, 0 0' },
+ '100%': { backgroundPosition: '0 200px, 0 0, 0 0, 0 0' },
+});
diff --git a/src/app/features/lotus/backgrounds/animRain.ts b/src/app/features/lotus/backgrounds/animRain.ts
new file mode 100644
index 000000000..d4b742b1f
--- /dev/null
+++ b/src/app/features/lotus/backgrounds/animRain.ts
@@ -0,0 +1,123 @@
+import { ChatBgVariants } from './types';
+import { rainFall } from './animRain.css';
+
+// anim-rain — "Digital Rain" — a premium take on the Matrix code-rain motif.
+//
+// Concept: sparse vertical columns of falling glyph-streaks. Each streak is a
+// soft vertical gradient that fades from a brighter LEADING glyph (the drop's
+// head) up into a dim trailing tail, punctuated by a scatter of faint monospace
+// glyph marks so it reads as CODE rather than plain stripes. It floats over a
+// near-black base carrying a subtle green phosphor cast and a gentle vignette.
+// Columns are deliberately sparse (only a handful across the 260px-wide tile)
+// so the reading area breathes and text always wins the contrast fight.
+//
+// SEAMLESS TILING + PAN — the streak SVG tile is 260×200. Its content is
+// authored to wrap top↔bottom: each streak's gradient and glyphs are placed so
+// the tile is vertically continuous, and the animation (see animRain.css.ts)
+// pans this first layer down by EXACTLY one tile height (200px) per cycle, so
+// the "fall" loops with no seam. The base / vignette layers are 100% 100% and
+// stay fixed (the keyframe holds them at '0 0').
+//
+// ANIMATION-STRIP SAFETY — getChatBg removes `animation` for reduced-motion /
+// pause-animations users, so the non-animation properties below already read as
+// a finished, gorgeous STATIC rain: a frozen frame of streaks over the base.
+//
+// CSP / Tauri-safe: inline SVG via encodeURIComponent (NOT base64). oklch used
+// throughout; alphas kept low so both themes stay WCAG-AA-friendly for text.
+
+// One vertical streak-column, colour-parameterised. Placed at x within a
+// 260-wide tile. `head` is the bright leading-glyph colour, `tail` the dim
+// trailing colour, `glyph` the colour of the riding monospace glyph ticks.
+const streak = (
+ x: number,
+ headY: number, // y of the leading glyph (drop head)
+ len: number, // trailing tail length upward
+ head: string,
+ tail: string,
+ glyph: string,
+): string => {
+ const topY = headY - len;
+ const id = `g${x}_${headY}`; // unique even when two columns share an x
+ // Vertical fade: transparent at the tail top → tail colour → bright head.
+ const grad = `
+
+
+
+
+`;
+ // The streak body is a soft, slightly-blurred vertical bar.
+ const bar = ``;
+ // A few monospace glyph ticks riding the column (short horizontal dashes).
+ const ticks = [0.22, 0.45, 0.68, 0.86]
+ .map((f, i) => {
+ const gy = Math.round(topY + len * f);
+ const gw = i % 2 === 0 ? 5 : 3;
+ const op = i === 3 ? '0.9' : '0.5';
+ return ``;
+ })
+ .join('');
+ // The leading glyph: a brighter small square cap at the head.
+ const cap = ``;
+ return grad + bar + ticks + cap;
+};
+
+// Full 260×200 tile. Columns are wrapped vertically: a column whose head sits
+// low in the tile has its tail running off the top, and a companion column
+// re-enters that space, so panning by one tile height reads as continuous fall.
+const tile = (head: string, tail: string, glyph: string): string => {
+ const cols = [
+ streak(24, 150, 140, head, tail, glyph),
+ streak(78, 60, 120, head, tail, glyph),
+ streak(122, 196, 160, head, tail, glyph), // head near bottom → tail wraps up
+ streak(122, 40, 160, head, tail, glyph), // partner near top completes the wrap
+ streak(178, 110, 100, head, tail, glyph),
+ streak(232, 176, 130, head, tail, glyph),
+ ].join('');
+ const svg = ``;
+ return `url("data:image/svg+xml,${encodeURIComponent(svg)}")`;
+};
+
+export const animRain: ChatBgVariants = {
+ // Dark: phosphor-green streaks on deep near-black with a faint green cast.
+ dark: {
+ backgroundColor: 'oklch(0.16 0.02 150)',
+ backgroundImage: [
+ // 1) the falling streak columns (this is the panned layer)
+ tile(
+ 'oklch(0.75 0.14 150 / 0.5)', // head — bright phosphor glyph
+ 'oklch(0.68 0.12 150 / 0.28)', // tail — dim phosphor
+ 'oklch(0.82 0.1 150 / 0.5)', // glyph ticks — brightest
+ ),
+ // 2) soft top-down phosphor haze so the rain has atmosphere
+ 'linear-gradient(180deg, oklch(0.24 0.04 150 / 0.55) 0%, transparent 40%)',
+ // 3) subtle green cast pooling toward the bottom
+ 'radial-gradient(120% 90% at 50% 100%, oklch(0.28 0.05 150 / 0.45) 0%, transparent 60%)',
+ // 4) vignette — quiet the corners so the reading column stays clean
+ 'radial-gradient(140% 140% at 50% 45%, transparent 60%, oklch(0.1 0.02 150 / 0.6) 100%)',
+ ].join(','),
+ backgroundSize: ['260px 200px', '100% 100%', '100% 100%', '100% 100%'].join(','),
+ backgroundPosition: ['0 0', '0 0', '0 0', '0 0'].join(','),
+ animation: `${rainFall} 12s linear infinite`,
+ },
+
+ // Light: soft teal-grey streaks on a pale cool base — elegant, never neon.
+ light: {
+ backgroundColor: 'oklch(0.97 0.008 165)',
+ backgroundImage: [
+ tile(
+ 'oklch(0.55 0.07 165 / 0.4)', // head — soft teal-grey drop
+ 'oklch(0.62 0.05 165 / 0.22)', // tail — faint teal-grey
+ 'oklch(0.5 0.06 165 / 0.42)', // glyph ticks
+ ),
+ // gentle cool wash from the top
+ 'linear-gradient(180deg, oklch(0.94 0.015 175 / 0.6) 0%, transparent 42%)',
+ // faint teal pooling at the bottom edge
+ 'radial-gradient(120% 90% at 50% 100%, oklch(0.9 0.02 170 / 0.5) 0%, transparent 60%)',
+ // soft vignette in cool grey
+ 'radial-gradient(140% 140% at 50% 45%, transparent 62%, oklch(0.88 0.02 165 / 0.5) 100%)',
+ ].join(','),
+ backgroundSize: ['260px 200px', '100% 100%', '100% 100%', '100% 100%'].join(','),
+ backgroundPosition: ['0 0', '0 0', '0 0', '0 0'].join(','),
+ animation: `${rainFall} 12s linear infinite`,
+ },
+};
diff --git a/src/app/features/lotus/backgrounds/animStars.css.ts b/src/app/features/lotus/backgrounds/animStars.css.ts
new file mode 100644
index 000000000..cae134c7b
--- /dev/null
+++ b/src/app/features/lotus/backgrounds/animStars.css.ts
@@ -0,0 +1,39 @@
+import { keyframes } from '@vanilla-extract/css';
+
+// Star Drift — a slow, serene PAN of a deep-space starfield with real parallax.
+//
+// The starfield in animStars.ts stacks six background layers:
+// 1. near stars — tile 137x137, brighter, drifts FASTEST
+// 2. mid stars — tile 191x191, medium
+// 3. far dust — tile 233x233, dimmest, drifts SLOWEST
+// 4. center vignette (100% 100%) — STATIC
+// 5. nebula wash A (100% 100%) — STATIC
+// 6. nebula wash B (100% 100%) — STATIC
+//
+// Seamless parallax: the single `animation` shorthand shares ONE duration across
+// all layers, so speed differences are produced purely by how FAR each layer
+// travels in the keyframe. For a perfectly seamless loop each star layer must
+// translate by an EXACT integer multiple of its own tile period, so the pixel
+// re-entering at the wrap is identical to the one that left. We move:
+// near : -274px = 2 x 137 (two tiles -> fastest apparent drift)
+// mid : -191px = 1 x 191 (one tile -> medium)
+// far : -233px = 1 x 233 (one tile, but larger tile => slowest apparent)
+// so near/mid/far read as three depths sliding past each other, yet every layer
+// lands back on an identical phase at 100% for a jump-free repeat.
+//
+// A diagonal component (both x and y shift) makes the drift feel like gentle
+// motion through space rather than a flat slide. The static layers are pinned at
+// '0 0' every frame so the vignette and nebula never move under the text.
+//
+// The start frame ('0%') MUST match the static backgroundPosition authored in
+// animStars.ts, so that when getChatBg STRIPS this animation for
+// prefers-reduced-motion the finished starfield shows without a jump.
+export const starDrift = keyframes({
+ '0%': {
+ backgroundPosition: '0 0, 61px 43px, 113px 97px, 0 0, 0 0, 0 0',
+ },
+ '100%': {
+ // near: -274/-274 (2 tiles), mid: 61-191/43-191, far: 113-233/97-233
+ backgroundPosition: '-274px -274px, -130px -148px, -120px -136px, 0 0, 0 0, 0 0',
+ },
+});
diff --git a/src/app/features/lotus/backgrounds/animStars.ts b/src/app/features/lotus/backgrounds/animStars.ts
new file mode 100644
index 000000000..cc3085d05
--- /dev/null
+++ b/src/app/features/lotus/backgrounds/animStars.ts
@@ -0,0 +1,117 @@
+import { ChatBgVariants } from './types';
+import { starDrift } from './animStars.css';
+
+// animStars ("Star Drift") — a serene deep-space field slowly drifting, with
+// genuine parallax between a near (brighter, faster) and a far (dim, slower)
+// star layer, floated on a faint nebula wash and calmed by a center vignette.
+//
+// Concept: three tiling star layers at coprime-ish tile sizes (137/191/233 dark,
+// 149/199/251 light) so their combined repeat is astronomically large and no
+// seam is ever perceivable. The near layer is crisp and sparse; the far "dust"
+// layer is dim and dense — the layer that gives depth. Beneath the stars sit a
+// deep-blue -> violet nebula (two soft ellipses) and a center vignette that keeps
+// the reading column the calmest, lowest-contrast area of the whole canvas.
+//
+// Layer stacking order (CSS paints image #1 on TOP):
+// 1. near stars — brighter, largest visible drift (tile 137 / 149)
+// 2. mid stars — softer, medium (tile 191 / 199)
+// 3. far dust — dimmest, slowest, most-repeated (tile 233 / 251)
+// 4. center vignette (100% 100%, static)
+// 5. nebula wash A (100% 100%, static)
+// 6. nebula wash B (100% 100%, static)
+//
+// Animation: `starDrift` (see animStars.css.ts) is a SLOW background-position PAN
+// that translates each star layer by an exact integer number of its own tiles,
+// so the loop is seamless AND the three layers drift at different apparent
+// speeds (parallax). getChatBg adds willChange/contain for the animated case and
+// STRIPS the `animation` for prefers-reduced-motion — at which point the static
+// backgroundPosition below (identical to the keyframe's 0% frame) shows as a
+// fully finished starfield on its own.
+//
+// Density is kept modest toward the center by the vignette + conservative dot
+// sizes, and every star opacity stays low so text over the field always clears
+// WCAG-AA in both themes.
+
+export const animStars: ChatBgVariants = {
+ // Dark: cool white + faint blue stars on a near-black cosmos, lifted onto a
+ // deep-blue -> violet nebula with a soft vignette darkening the calm center.
+ dark: {
+ backgroundColor: 'oklch(0.15 0.03 275)',
+ backgroundImage: [
+ // 1. near stars — crisp cool-white, sparse, the "fast" parallax layer
+ 'radial-gradient(circle at center, oklch(0.98 0.012 255 / 0.85) 0.6px, transparent 1.5px)',
+ // 2. mid stars — softer, a touch blue, more of them
+ 'radial-gradient(circle at center, oklch(0.90 0.03 260 / 0.52) 0.6px, transparent 1.3px)',
+ // 3. far dust — faint blue haze, the slow depth layer (most repeats)
+ 'radial-gradient(circle at center, oklch(0.78 0.06 255 / 0.28) 0.5px, transparent 1.1px)',
+ // 4. center vignette — keeps the reading column calmest / lowest-contrast
+ 'radial-gradient(ellipse 120% 90% at 50% 42%, transparent 40%, oklch(0.09 0.03 270 / 0.58) 100%)',
+ // 5. nebula wash A — deep violet high-right
+ 'radial-gradient(ellipse 140% 120% at 78% 10%, oklch(0.26 0.09 285 / 0.55) 0%, transparent 55%)',
+ // 6. nebula wash B — deep blue low-left
+ 'radial-gradient(ellipse 130% 110% at 16% 94%, oklch(0.21 0.07 250 / 0.50) 0%, transparent 58%)',
+ ].join(','),
+ backgroundSize: [
+ '137px 137px', // near stars
+ '191px 191px', // mid stars
+ '233px 233px', // far dust
+ '100% 100%', // vignette
+ '100% 100%', // nebula A
+ '100% 100%', // nebula B
+ ].join(','),
+ // Must equal starDrift's 0% frame so reduced-motion shows this exact field.
+ backgroundPosition: [
+ '0 0', // near
+ '61px 43px', // mid (offset breaks tile alignment)
+ '113px 97px', // far (offset again)
+ '0 0', // vignette
+ '0 0', // nebula A
+ '0 0', // nebula B
+ ].join(','),
+ animation: `${starDrift} 90s linear infinite`,
+ },
+
+ // Light: an airy pre-dawn sky. No literal white-on-white stars — instead very
+ // soft pale sparkles plus the merest cool speckles, floated on a gentle cool
+ // gradient. Reads as elegant atmosphere, never as noise over text.
+ light: {
+ backgroundColor: 'oklch(0.965 0.008 255)',
+ backgroundImage: [
+ // 1. near sparkles — a hair brighter/warmer than the sky
+ 'radial-gradient(circle at center, oklch(0.995 0.015 90 / 0.50) 0.6px, transparent 1.5px)',
+ // 2. mid cool speckles — faintest hint of darkness for texture/contrast
+ 'radial-gradient(circle at center, oklch(0.60 0.05 260 / 0.15) 0.5px, transparent 1.2px)',
+ // 3. far dust — very soft cool haze, the slow depth layer
+ 'radial-gradient(circle at center, oklch(0.70 0.04 255 / 0.11) 0.5px, transparent 1.1px)',
+ // 4. center vignette — subtly brightens the calm reading center
+ 'radial-gradient(ellipse 120% 90% at 50% 44%, oklch(1 0 0 / 0.45) 30%, transparent 100%)',
+ // 5. pre-dawn wash A — cool blue high-right
+ 'radial-gradient(ellipse 150% 120% at 80% 6%, oklch(0.90 0.05 255 / 0.60) 0%, transparent 60%)',
+ // 6. pre-dawn wash B — warm blush low-left
+ 'radial-gradient(ellipse 140% 120% at 14% 96%, oklch(0.93 0.04 40 / 0.42) 0%, transparent 62%)',
+ ].join(','),
+ // Same tile sizes as dark (137/191/233). The shared starDrift keyframe pans
+ // each layer by an exact integer multiple of ITS tile (near 2x137, mid 1x191,
+ // far 1x233); reusing these tiles here guarantees the loop wraps seamlessly in
+ // light mode too, since one keyframe drives both themes. Coprime-ish sizes keep
+ // the combined repeat astronomically large so no seam is ever perceivable.
+ backgroundSize: [
+ '137px 137px', // near sparkles
+ '191px 191px', // mid speckles
+ '233px 233px', // far dust
+ '100% 100%', // vignette
+ '100% 100%', // wash A
+ '100% 100%', // wash B
+ ].join(','),
+ // Positions mirror the keyframe 0% frame (== reduced-motion static field).
+ backgroundPosition: [
+ '0 0', // near
+ '61px 43px', // mid
+ '113px 97px', // far
+ '0 0', // vignette
+ '0 0', // wash A
+ '0 0', // wash B
+ ].join(','),
+ animation: `${starDrift} 100s linear infinite`,
+ },
+};
diff --git a/src/app/features/lotus/backgrounds/blueprint.ts b/src/app/features/lotus/backgrounds/blueprint.ts
new file mode 100644
index 000000000..b55f9b969
--- /dev/null
+++ b/src/app/features/lotus/backgrounds/blueprint.ts
@@ -0,0 +1,85 @@
+import { ChatBgVariants } from './types';
+
+// blueprint — an engineering / architectural drafting sheet.
+//
+// Layers (painted top-to-bottom):
+// 1. SVG draftsman tick-marks + a centred crosshair accent (96px tile — lands
+// exactly on the major grid; corner quarter-arms tile into a full "+" on
+// every major intersection).
+// 2. Major grid lines (heavier) — 96px.
+// 3. Minor grid lines (fine, fainter) — 16px (96 = 6 × 16, so it nests
+// seamlessly inside the major grid with no beat/moiré).
+// 4. A soft radial vignette + a gentle sheet-glow so the surface reads like a
+// real drafting sheet with subtle dimension rather than a flat tile.
+//
+// Everything is kept at low alpha (~0.03–0.16) so the motif is felt, not read:
+// crisp message text sits comfortably above it in both themes (WCAG-AA safe).
+
+const DARK_TICKS =
+ 'url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2296%22%20height%3D%2296%22%3E%3Cg%20stroke%3D%22oklch%280.72%200.11%20230%20%2F%200.32%29%22%20stroke-width%3D%221%22%3E%3Cpath%20d%3D%22M0%200%20H7%20M0%200%20V7%22%2F%3E%3Cpath%20d%3D%22M96%200%20H89%20M96%200%20V7%22%2F%3E%3Cpath%20d%3D%22M0%2096%20H7%20M0%2096%20V89%22%2F%3E%3Cpath%20d%3D%22M96%2096%20H89%20M96%2096%20V89%22%2F%3E%3C%2Fg%3E%3Cg%20stroke%3D%22oklch%280.72%200.11%20230%20%2F%200.18%29%22%20stroke-width%3D%221%22%3E%3Cpath%20d%3D%22M48%2044%20V52%20M44%2048%20H52%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E")';
+
+const LIGHT_TICKS =
+ 'url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2296%22%20height%3D%2296%22%3E%3Cg%20stroke%3D%22oklch%280.48%200.13%20250%20%2F%200.38%29%22%20stroke-width%3D%221%22%3E%3Cpath%20d%3D%22M0%200%20H7%20M0%200%20V7%22%2F%3E%3Cpath%20d%3D%22M96%200%20H89%20M96%200%20V7%22%2F%3E%3Cpath%20d%3D%22M0%2096%20H7%20M0%2096%20V89%22%2F%3E%3Cpath%20d%3D%22M96%2096%20H89%20M96%2096%20V89%22%2F%3E%3C%2Fg%3E%3Cg%20stroke%3D%22oklch%280.48%200.13%20250%20%2F%200.22%29%22%20stroke-width%3D%221%22%3E%3Cpath%20d%3D%22M48%2044%20V52%20M44%2048%20H52%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E")';
+
+export const blueprint: ChatBgVariants = {
+ // Cyan-blue lines on a deep navy sheet.
+ dark: {
+ backgroundColor: 'oklch(0.22 0.05 250)',
+ backgroundImage: [
+ // 1. draftsman ticks + centre crosshair
+ DARK_TICKS,
+ // 4a. sheet-glow: a faint cooler highlight drifting off the top-left,
+ // giving the flat navy some dimension.
+ 'radial-gradient(120% 120% at 18% 8%, oklch(0.30 0.06 245 / 0.55) 0%, transparent 55%)',
+ // 4b. vignette: gently darkens the corners like a drafting sheet edge.
+ 'radial-gradient(140% 140% at 50% 42%, transparent 58%, oklch(0.14 0.04 255 / 0.5) 100%)',
+ // 2. major grid (heavier)
+ 'linear-gradient(oklch(0.72 0.12 230 / 0.13) 1px, transparent 1px)',
+ 'linear-gradient(90deg, oklch(0.72 0.12 230 / 0.13) 1px, transparent 1px)',
+ // 3. minor grid (fine, fainter)
+ 'linear-gradient(oklch(0.72 0.12 230 / 0.05) 1px, transparent 1px)',
+ 'linear-gradient(90deg, oklch(0.72 0.12 230 / 0.05) 1px, transparent 1px)',
+ ].join(','),
+ backgroundSize: [
+ '96px 96px', // ticks
+ '100% 100%', // sheet-glow
+ '100% 100%', // vignette
+ '96px 96px', // major V
+ '96px 96px', // major H
+ '16px 16px', // minor V
+ '16px 16px', // minor H
+ ].join(','),
+ // All layers share the default top-left (0 0) origin so the tick tile, the
+ // 96px major grid and the 16px minor grid stay phase-locked (96 = 6 × 16) —
+ // no drift, no visible seams. (A per-layer `center` would let the differently
+ // sized tiles center independently and fall out of alignment.)
+ },
+
+ // Blue lines on a cool paper-white sheet.
+ light: {
+ backgroundColor: 'oklch(0.97 0.01 240)',
+ backgroundImage: [
+ LIGHT_TICKS,
+ // sheet-glow: a hint of brighter paper toward the top-left.
+ 'radial-gradient(120% 120% at 18% 8%, oklch(0.99 0.008 240 / 0.7) 0%, transparent 55%)',
+ // vignette: soft cool shading into the corners.
+ 'radial-gradient(140% 140% at 50% 42%, transparent 60%, oklch(0.90 0.02 245 / 0.55) 100%)',
+ // major grid (heavier)
+ 'linear-gradient(oklch(0.48 0.13 250 / 0.15) 1px, transparent 1px)',
+ 'linear-gradient(90deg, oklch(0.48 0.13 250 / 0.15) 1px, transparent 1px)',
+ // minor grid (fine, fainter)
+ 'linear-gradient(oklch(0.48 0.13 250 / 0.06) 1px, transparent 1px)',
+ 'linear-gradient(90deg, oklch(0.48 0.13 250 / 0.06) 1px, transparent 1px)',
+ ].join(','),
+ backgroundSize: [
+ '96px 96px',
+ '100% 100%',
+ '100% 100%',
+ '96px 96px',
+ '96px 96px',
+ '16px 16px',
+ '16px 16px',
+ ].join(','),
+ // Shared top-left origin keeps the tick tile and both grids phase-locked.
+ },
+};
diff --git a/src/app/features/lotus/backgrounds/chevron.ts b/src/app/features/lotus/backgrounds/chevron.ts
new file mode 100644
index 000000000..b7f130e4a
--- /dev/null
+++ b/src/app/features/lotus/backgrounds/chevron.ts
@@ -0,0 +1,76 @@
+import { CSSProperties } from 'react';
+import { ChatBgVariants } from './types';
+
+// chevron — refined woven-upholstery zigzag.
+//
+// The motif is a continuous, crisp chevron built to read as *textured fabric*
+// rather than flat stripes. The zigzag threads themselves are drawn with a
+// tiny inline-SVG tile (guaranteed geometrically seamless — the "V" path exits
+// each tile edge exactly where the next tile's path enters, both horizontally
+// and vertically). Over that, layered CSS gradients add the premium feel:
+// • a soft light→shade sweep across the weave gives each band an embossed,
+// woven cross-section (catches light on one diagonal face, shade on the
+// other);
+// • a faint two-tone wash alternates the tint of successive chevron rows for
+// an interlocked-yarn look;
+// • a gentle centre lift + corner vignette settle the field so text always
+// sits over the calmer middle.
+//
+// SEAMLESS TILING
+// The SVG is a WxH tile whose path is one full zigzag wave: it starts at the
+// left edge, dips to the vertex, rises to the right edge at the SAME y it
+// started — so horizontally each tile's end meets the next tile's start with no
+// step. Two stacked strokes (offset by H) fill the vertical repeat, and the
+// tile height equals the row pitch, so vertical stacking is seamless too. The
+// gradient overlays are non-repeating (100% 100%) or share the SVG's tile
+// width, so none of them introduce a seam.
+//
+// Everything sits at low alpha (~0.03–0.11) so the pattern is felt, not read:
+// crisp message text stays comfortably WCAG-AA in both themes.
+
+// One zigzag wave, 40px wide × 20px tall. Path enters at (0,4), dips to the
+// vertex at (20,16), climbs back to (40,4) — identical entry/exit y => seamless
+// horizontal repeat. A second copy shifted +10 in y keeps a soft double thread.
+const svg = (stroke: string, faint: string) =>
+ 'url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20' +
+ 'width%3D%2240%22%20height%3D%2220%22%3E' +
+ `%3Cpath%20d%3D%22M0%204%20L20%2016%20L40%204%22%20fill%3D%22none%22%20stroke%3D%22${stroke}%22%20stroke-width%3D%223%22%2F%3E` +
+ `%3Cpath%20d%3D%22M0%2014%20L20%2026%20L40%2014%20M0%20-6%20L20%206%20L40%20-6%22%20fill%3D%22none%22%20stroke%3D%22${faint}%22%20stroke-width%3D%222%22%2F%3E` +
+ '%3C%2Fsvg%3E")';
+
+const dark: CSSProperties = {
+ backgroundColor: 'oklch(0.20 0.022 260)',
+ backgroundImage: [
+ // 1. The zigzag threads — muted indigo/slate, main + fainter under-thread.
+ svg('oklch(0.55 0.05 265 %2F 0.16)', 'oklch(0.50 0.045 262 %2F 0.07)'),
+ // 2. Woven emboss — a soft diagonal light→shade sweep across the weave so
+ // the bands catch light on one face and fall to shade on the other.
+ 'linear-gradient(135deg, oklch(0.62 0.05 265 / 0.05) 0%, transparent 45%, transparent 55%, oklch(0.14 0.02 260 / 0.06) 100%)',
+ // 3. Two-tone weft — a whisper shade on alternate chevron rows.
+ 'repeating-linear-gradient(0deg, oklch(0.50 0.04 258 / 0.035) 0px, oklch(0.50 0.04 258 / 0.035) 20px, transparent 20px, transparent 40px)',
+ // 4. Tonal wash — cool centre lift for gentle depth.
+ 'radial-gradient(ellipse 90% 75% at 50% 42%, oklch(0.26 0.03 262 / 0.40) 0%, transparent 60%)',
+ // 5. Vignette — feather corners into deeper charcoal-blue.
+ 'radial-gradient(ellipse 120% 130% at 50% 45%, transparent 60%, oklch(0.15 0.02 260 / 0.55) 100%)',
+ ].join(','),
+ backgroundSize: '40px 20px, 100% 100%, 40px 40px, 100% 100%, 100% 100%',
+};
+
+const light: CSSProperties = {
+ backgroundColor: 'oklch(0.965 0.006 85)',
+ backgroundImage: [
+ // 1. The zigzag threads — soft dusty-blue, main + fainter under-thread.
+ svg('oklch(0.55 0.05 255 %2F 0.14)', 'oklch(0.52 0.045 255 %2F 0.06)'),
+ // 2. Woven emboss — diagonal light→shade sweep for a knit-fabric surface.
+ 'linear-gradient(135deg, oklch(0.99 0.008 85 / 0.06) 0%, transparent 45%, transparent 55%, oklch(0.55 0.05 255 / 0.05) 100%)',
+ // 3. Two-tone weft — faint alternating-row shade.
+ 'repeating-linear-gradient(0deg, oklch(0.52 0.04 255 / 0.03) 0px, oklch(0.52 0.04 255 / 0.03) 20px, transparent 20px, transparent 40px)',
+ // 4. Tonal wash — warm paper highlight through the reading centre.
+ 'radial-gradient(ellipse 90% 75% at 50% 42%, oklch(0.99 0.008 85 / 0.55) 0%, transparent 60%)',
+ // 5. Vignette — settle corners into a slightly deeper dusty tone.
+ 'radial-gradient(ellipse 120% 130% at 50% 45%, transparent 60%, oklch(0.91 0.012 250 / 0.40) 100%)',
+ ].join(','),
+ backgroundSize: '40px 20px, 100% 100%, 40px 40px, 100% 100%, 100% 100%',
+};
+
+export const chevron: ChatBgVariants = { dark, light };
diff --git a/src/app/features/lotus/backgrounds/circuit.ts b/src/app/features/lotus/backgrounds/circuit.ts
new file mode 100644
index 000000000..8cf56d4cc
--- /dev/null
+++ b/src/app/features/lotus/backgrounds/circuit.ts
@@ -0,0 +1,116 @@
+import { ChatBgVariants } from './types';
+
+// circuit — an elegant printed-circuit board.
+//
+// Concept: thin right-angle copper traces route between small pads / vias and
+// the occasional solder-junction dot, over a deep board base. It reads as an
+// authentic PCB rather than a plain grid: the routing turns corners, dead-ends
+// at through-hole pads, and picks up faint via-glows — but stays sparse, with
+// generous negative space so message text always wins the contrast fight.
+//
+// The trace network is a single inline SVG data-URI (encodeURIComponent, NOT
+// base64 — CSP / Tauri-safe) so the geometry can be real right-angle routing
+// instead of gradient fakery. It is layered over a subtle board-base gradient.
+//
+// SEAMLESS TILING — the 120×120 tile is authored so every trace that leaves an
+// edge re-enters at the identical coordinate on the OPPOSITE edge, so the copper
+// runs continuously across tile boundaries with no visible seam:
+// • horizontal runs cross the left/right edges at y = 30 and y = 90
+// • vertical runs cross the top/bottom edges at x = 40 and x = 88
+// backgroundSize is set to the tile size (120px) so those crossings line up
+// exactly on repeat.
+//
+// Two hand-tuned SVGs (dark / light) differ only in stroke/fill colour + alpha.
+// Alphas stay low (≈0.05–0.5 on the accents, traces ~0.1–0.16) so the pattern is
+// felt, not read — crisp text sits comfortably above it in both themes.
+
+// Shared geometry, colour-parameterised so the two themes stay pixel-identical
+// in layout and only diverge in palette.
+const tile = (
+ trace: string, // trace stroke colour
+ traceW: string, // trace stroke-width
+ pad: string, // pad ring colour
+ padFill: string, // pad centre / board-coloured hole
+ via: string, // via glow colour
+ junction: string, // filled junction-dot colour
+): string => {
+ const svg = ``;
+ return `url("data:image/svg+xml,${encodeURIComponent(svg)}")`;
+};
+
+export const circuit: ChatBgVariants = {
+ // Faint teal/green copper with dim cyan via-glows on a near-black board.
+ dark: {
+ backgroundColor: 'oklch(0.17 0.02 165)',
+ backgroundImage: [
+ tile(
+ 'oklch(0.7 0.1 165 / 0.16)', // traces — faint teal-green copper
+ '1',
+ 'oklch(0.72 0.11 175 / 0.32)', // pad rings — slightly brighter
+ 'oklch(0.17 0.02 165)', // pad holes — board colour (drilled look)
+ 'oklch(0.78 0.13 200 / 0.14)', // via glow — dim cyan halo
+ 'oklch(0.74 0.12 170 / 0.4)', // junction dots — solid copper
+ ),
+ // board-base: a gentle diagonal sheen so the flat near-black gains depth.
+ 'radial-gradient(130% 130% at 20% 12%, oklch(0.22 0.03 170 / 0.6) 0%, transparent 58%)',
+ // vignette: barely darkens the corners like a laminated board edge.
+ 'radial-gradient(140% 140% at 50% 45%, transparent 62%, oklch(0.12 0.02 165 / 0.55) 100%)',
+ ].join(','),
+ backgroundSize: ['120px 120px', '100% 100%', '100% 100%'].join(','),
+ },
+
+ // Soft green-grey traces on a pale board.
+ light: {
+ backgroundColor: 'oklch(0.96 0.012 160)',
+ backgroundImage: [
+ tile(
+ 'oklch(0.55 0.07 165 / 0.24)', // traces — soft green-grey copper
+ '1',
+ 'oklch(0.5 0.08 170 / 0.4)', // pad rings
+ 'oklch(0.96 0.012 160)', // pad holes — board colour
+ 'oklch(0.6 0.09 200 / 0.1)', // via glow — faint cool halo
+ 'oklch(0.5 0.08 165 / 0.42)', // junction dots
+ ),
+ // board-base: a hint of brighter laminate toward the top-left.
+ 'radial-gradient(130% 130% at 20% 12%, oklch(0.99 0.008 160 / 0.7) 0%, transparent 58%)',
+ // vignette: soft green-grey shading into the corners.
+ 'radial-gradient(140% 140% at 50% 45%, transparent 62%, oklch(0.9 0.02 160 / 0.5) 100%)',
+ ].join(','),
+ backgroundSize: ['120px 120px', '100% 100%', '100% 100%'].join(','),
+ },
+};
diff --git a/src/app/features/lotus/backgrounds/crosshatch.ts b/src/app/features/lotus/backgrounds/crosshatch.ts
new file mode 100644
index 000000000..a4adcf06b
--- /dev/null
+++ b/src/app/features/lotus/backgrounds/crosshatch.ts
@@ -0,0 +1,56 @@
+import { CSSProperties } from 'react';
+import { ChatBgVariants } from './types';
+
+// crosshatch — fine pen-and-ink engraving, like a banknote guilloché.
+// Three hatch directions (right-leaning, left-leaning, near-horizontal cross)
+// are layered at low opacity so the eye reads a woven ink texture rather than
+// discrete stripes. Each direction uses a slightly different pitch so the
+// combined pattern never lines up into a coarse moire, and a barely-there
+// diagonal tonal gradient lends etched depth.
+//
+// Seamless tiling: each hatch is a `repeating-linear-gradient`, which repeats
+// infinitely by definition, so the layers are left at `backgroundSize: auto`
+// and tile with no visible seam at any element size (constraining a diagonal
+// repeat to a small square would clip it mid-period and create a seam). The
+// tonal wash is a single non-repeating gradient stretched to `cover`.
+//
+// Opacities are kept in the 0.02–0.05 range so the texture is felt, not read —
+// crisp message text sits comfortably above it in both themes (WCAG-AA safe).
+
+const dark: CSSProperties = {
+ // near-black base with a whisper of cool blue so silver ink reads as engraving
+ backgroundColor: 'oklch(0.16 0.01 255)',
+ backgroundImage: [
+ // faint tonal gradient — top-left slightly lifted for etched depth
+ 'linear-gradient(135deg, oklch(0.20 0.012 255 / 0.5) 0%, oklch(0.15 0.01 255 / 0) 55%, oklch(0.14 0.008 260 / 0.45) 100%)',
+ // primary hatch, right-leaning fine lines (cool silver ink), ~9px pitch
+ 'repeating-linear-gradient(45deg, oklch(0.75 0.02 250 / 0.05) 0, oklch(0.75 0.02 250 / 0.05) 0.75px, transparent 0.75px, transparent 9px)',
+ // secondary hatch, left-leaning — the cross of the crosshatch
+ 'repeating-linear-gradient(135deg, oklch(0.75 0.02 250 / 0.045) 0, oklch(0.75 0.02 250 / 0.045) 0.75px, transparent 0.75px, transparent 9px)',
+ // tertiary hatch, right-leaning at a denser pitch for engraved richness
+ 'repeating-linear-gradient(45deg, oklch(0.78 0.018 250 / 0.02) 0, oklch(0.78 0.018 250 / 0.02) 0.5px, transparent 0.5px, transparent 4.5px)',
+ // quaternary near-horizontal fill line, very faint, weaves the mesh together
+ 'repeating-linear-gradient(20deg, oklch(0.72 0.015 255 / 0.018) 0, oklch(0.72 0.015 255 / 0.018) 0.5px, transparent 0.5px, transparent 13px)',
+ ].join(','),
+ backgroundSize: 'cover, auto, auto, auto, auto',
+};
+
+const light: CSSProperties = {
+ // warm paper base — graphite ink on cream stock
+ backgroundColor: 'oklch(0.975 0.006 85)',
+ backgroundImage: [
+ // faint tonal wash — soft warm depth for aged-paper feel
+ 'linear-gradient(135deg, oklch(0.94 0.008 85 / 0.55) 0%, oklch(0.98 0.005 85 / 0) 55%, oklch(0.93 0.01 80 / 0.5) 100%)',
+ // primary hatch, right-leaning graphite lines, ~9px pitch
+ 'repeating-linear-gradient(45deg, oklch(0.42 0.01 265 / 0.055) 0, oklch(0.42 0.01 265 / 0.055) 0.75px, transparent 0.75px, transparent 9px)',
+ // secondary hatch, left-leaning — the cross
+ 'repeating-linear-gradient(135deg, oklch(0.42 0.01 265 / 0.05) 0, oklch(0.42 0.01 265 / 0.05) 0.75px, transparent 0.75px, transparent 9px)',
+ // tertiary denser right-leaning hatch for engraved fineness
+ 'repeating-linear-gradient(45deg, oklch(0.40 0.012 265 / 0.025) 0, oklch(0.40 0.012 265 / 0.025) 0.5px, transparent 0.5px, transparent 4.5px)',
+ // quaternary near-horizontal weave line, barely-there
+ 'repeating-linear-gradient(20deg, oklch(0.45 0.01 260 / 0.022) 0, oklch(0.45 0.01 260 / 0.022) 0.5px, transparent 0.5px, transparent 13px)',
+ ].join(','),
+ backgroundSize: 'cover, auto, auto, auto, auto',
+};
+
+export const crosshatch: ChatBgVariants = { dark, light };
diff --git a/src/app/features/lotus/backgrounds/herringbone.ts b/src/app/features/lotus/backgrounds/herringbone.ts
new file mode 100644
index 000000000..1636ae46f
--- /dev/null
+++ b/src/app/features/lotus/backgrounds/herringbone.ts
@@ -0,0 +1,52 @@
+import { ChatBgVariants } from './types';
+
+// Herringbone — a refined, tactile broken-zigzag weave (the classic parquet / tweed
+// motif) rather than a flat hairline grid. Each plank is drawn twice in a compact SVG
+// data-URI tile: a lit "thread" and a 0.6px-offset shadow companion, so every plank
+// reads as a beveled, three-dimensional strand of fabric instead of a line.
+//
+// SEAMLESS TILING: planks live on a 12px lattice and their orientation follows the true
+// herringbone rule orient(cx, cy) = '/' when (cx - cy) mod 4 in {0, 1}, else '\\'.
+// That rule is exactly periodic every 4 cells in BOTH axes, so the 48x48px tile repeats
+// with no seam at any scroll offset; segment endpoints all land on lattice corners, so
+// the broken V's interlock perfectly across tile edges.
+//
+// DEPTH: beneath the weave sit two very low-contrast oklch layers — a diagonal two-tone
+// wash that gives the fabric a faint lit/shadowed side, plus a soft vignette that lets
+// the centre (where text lives) stay calmest. Everything is kept in the "felt, not read"
+// opacity band so WCAG-AA body text sits comfortably on top in both themes.
+
+const WEAVE_DARK =
+ 'data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2248%22%20height%3D%2248%22%20viewBox%3D%220%200%2048%2048%22%3E%3Cpath%20d%3D%22M-11.4%200.6L0.6%20-11.4M0.6%200.6L12.6%20-11.4M12.6%20-11.4L24.6%200.6M24.6%20-11.4L36.6%200.6M36.6%200.6L48.6%20-11.4M48.6%200.6L60.6%20-11.4M-11.4%200.6L0.6%2012.6M0.6%2012.6L12.6%200.6M12.6%2012.6L24.6%200.6M24.6%200.6L36.6%2012.6M36.6%200.6L48.6%2012.6M48.6%2012.6L60.6%200.6M-11.4%2012.6L0.6%2024.6M0.6%2012.6L12.6%2024.6M12.6%2024.6L24.6%2012.6M24.6%2024.6L36.6%2012.6M36.6%2012.6L48.6%2024.6M48.6%2012.6L60.6%2024.6M-11.4%2036.6L0.6%2024.6M0.6%2024.6L12.6%2036.6M12.6%2024.6L24.6%2036.6M24.6%2036.6L36.6%2024.6M36.6%2036.6L48.6%2024.6M48.6%2024.6L60.6%2036.6M-11.4%2048.6L0.6%2036.6M0.6%2048.6L12.6%2036.6M12.6%2036.6L24.6%2048.6M24.6%2036.6L36.6%2048.6M36.6%2048.6L48.6%2036.6M48.6%2048.6L60.6%2036.6M-11.4%2048.6L0.6%2060.6M0.6%2060.6L12.6%2048.6M12.6%2060.6L24.6%2048.6M24.6%2048.6L36.6%2060.6M36.6%2048.6L48.6%2060.6M48.6%2060.6L60.6%2048.6%22%20fill%3D%22none%22%20stroke%3D%22rgb%2810%2C8%2C6%29%22%20stroke-width%3D%221.1%22%20stroke-opacity%3D%220.085%22%20stroke-linecap%3D%22round%22%2F%3E%3Cpath%20d%3D%22M-12%200L0%20-12M0%200L12%20-12M12%20-12L24%200M24%20-12L36%200M36%200L48%20-12M48%200L60%20-12M-12%200L0%2012M0%2012L12%200M12%2012L24%200M24%200L36%2012M36%200L48%2012M48%2012L60%200M-12%2012L0%2024M0%2012L12%2024M12%2024L24%2012M24%2024L36%2012M36%2012L48%2024M48%2012L60%2024M-12%2036L0%2024M0%2024L12%2036M12%2024L24%2036M24%2036L36%2024M36%2036L48%2024M48%2024L60%2036M-12%2048L0%2036M0%2048L12%2036M12%2036L24%2048M24%2036L36%2048M36%2048L48%2036M48%2048L60%2036M-12%2048L0%2060M0%2060L12%2048M12%2060L24%2048M24%2048L36%2060M36%2048L48%2060M48%2060L60%2048%22%20fill%3D%22none%22%20stroke%3D%22rgb%28210%2C199%2C180%29%22%20stroke-width%3D%221.1%22%20stroke-opacity%3D%220.111%22%20stroke-linecap%3D%22round%22%2F%3E%3C%2Fsvg%3E';
+const WEAVE_LIGHT =
+ 'data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2248%22%20height%3D%2248%22%20viewBox%3D%220%200%2048%2048%22%3E%3Cpath%20d%3D%22M-11.4%200.6L0.6%20-11.4M0.6%200.6L12.6%20-11.4M12.6%20-11.4L24.6%200.6M24.6%20-11.4L36.6%200.6M36.6%200.6L48.6%20-11.4M48.6%200.6L60.6%20-11.4M-11.4%200.6L0.6%2012.6M0.6%2012.6L12.6%200.6M12.6%2012.6L24.6%200.6M24.6%200.6L36.6%2012.6M36.6%200.6L48.6%2012.6M48.6%2012.6L60.6%200.6M-11.4%2012.6L0.6%2024.6M0.6%2012.6L12.6%2024.6M12.6%2024.6L24.6%2012.6M24.6%2024.6L36.6%2012.6M36.6%2012.6L48.6%2024.6M48.6%2012.6L60.6%2024.6M-11.4%2036.6L0.6%2024.6M0.6%2024.6L12.6%2036.6M12.6%2024.6L24.6%2036.6M24.6%2036.6L36.6%2024.6M36.6%2036.6L48.6%2024.6M48.6%2024.6L60.6%2036.6M-11.4%2048.6L0.6%2036.6M0.6%2048.6L12.6%2036.6M12.6%2036.6L24.6%2048.6M24.6%2036.6L36.6%2048.6M36.6%2048.6L48.6%2036.6M48.6%2048.6L60.6%2036.6M-11.4%2048.6L0.6%2060.6M0.6%2060.6L12.6%2048.6M12.6%2060.6L24.6%2048.6M24.6%2048.6L36.6%2060.6M36.6%2048.6L48.6%2060.6M48.6%2060.6L60.6%2048.6%22%20fill%3D%22none%22%20stroke%3D%22rgb%28126%2C116%2C98%29%22%20stroke-width%3D%221.1%22%20stroke-opacity%3D%220.075%22%20stroke-linecap%3D%22round%22%2F%3E%3Cpath%20d%3D%22M-12%200L0%20-12M0%200L12%20-12M12%20-12L24%200M24%20-12L36%200M36%200L48%20-12M48%200L60%20-12M-12%200L0%2012M0%2012L12%200M12%2012L24%200M24%200L36%2012M36%200L48%2012M48%2012L60%200M-12%2012L0%2024M0%2012L12%2024M12%2024L24%2012M24%2024L36%2012M36%2012L48%2024M48%2012L60%2024M-12%2036L0%2024M0%2024L12%2036M12%2024L24%2036M24%2036L36%2024M36%2036L48%2024M48%2024L60%2036M-12%2048L0%2036M0%2048L12%2036M12%2036L24%2048M24%2036L36%2048M36%2048L48%2036M48%2048L60%2036M-12%2048L0%2060M0%2060L12%2048M12%2060L24%2048M24%2048L36%2060M36%2048L48%2060M48%2060L60%2048%22%20fill%3D%22none%22%20stroke%3D%22rgb%28255%2C253%2C247%29%22%20stroke-width%3D%221.1%22%20stroke-opacity%3D%220.098%22%20stroke-linecap%3D%22round%22%2F%3E%3C%2Fsvg%3E';
+
+export const herringbone: ChatBgVariants = {
+ // Warm taupe threads (~oklch(0.79 0.02 75)) over a charcoal base. The two-tone wash
+ // runs cool-charcoal -> slightly warmer charcoal across the diagonal so the weave has
+ // a gentle light side; the vignette darkens the far corners a touch for depth.
+ dark: {
+ backgroundColor: '#14120f',
+ backgroundImage: [
+ `url("${WEAVE_DARK}")`,
+ 'linear-gradient(135deg, oklch(0.26 0.012 70 / 0.5) 0%, oklch(0.2 0.008 60 / 0.5) 100%)',
+ 'radial-gradient(120% 120% at 50% 40%, oklch(0.24 0.01 65 / 0) 55%, oklch(0.12 0.006 55 / 0.45) 100%)',
+ ].join(','),
+ backgroundSize: '48px 48px, 100% 100%, 100% 100%',
+ backgroundRepeat: 'repeat, no-repeat, no-repeat',
+ },
+
+ // Greige threads (shadow ~oklch(0.6 0.015 75)) with a warm-white highlight over a warm
+ // off-white base. The wash tilts warm-white -> faint greige across the diagonal for the
+ // lit/shadow side; a whisper-soft vignette keeps corners from going flat.
+ light: {
+ backgroundColor: '#f6f3ec',
+ backgroundImage: [
+ `url("${WEAVE_LIGHT}")`,
+ 'linear-gradient(135deg, oklch(0.99 0.006 85 / 0.6) 0%, oklch(0.93 0.01 80 / 0.6) 100%)',
+ 'radial-gradient(120% 120% at 50% 40%, oklch(0.98 0.006 85 / 0) 58%, oklch(0.87 0.012 78 / 0.4) 100%)',
+ ].join(','),
+ backgroundSize: '48px 48px, 100% 100%, 100% 100%',
+ backgroundRepeat: 'repeat, no-repeat, no-repeat',
+ },
+};
diff --git a/src/app/features/lotus/backgrounds/hexgrid.ts b/src/app/features/lotus/backgrounds/hexgrid.ts
new file mode 100644
index 000000000..2f00620d4
--- /dev/null
+++ b/src/app/features/lotus/backgrounds/hexgrid.ts
@@ -0,0 +1,66 @@
+import { CSSProperties } from 'react';
+import { ChatBgVariants } from './types';
+
+// hexgrid — a refined sci-fi HUD honeycomb lattice.
+//
+// The motif is a crisp pointy-top hexagon honeycomb, drawn as thin interlocking
+// outlines like the readout of a sci-fi interface. It is layered over a soft
+// depth sheen: a faint central glow lifts the middle of the field and a gentle
+// vignette settles the corners, so the lattice reads as a lit HUD surface with
+// dimension rather than a flat repeating tile. Everything is kept at low alpha
+// (hex lines ~0.14–0.16, washes well under legibility thresholds) so the motif
+// is *felt, not read* — crisp message text stays comfortably WCAG-AA in both
+// themes.
+//
+// SEAMLESS TILING
+// The hex outlines live in a single inline-SVG data-URI tile of exactly
+// √3·s × 3·s = 34.641 × 60 (side length s = 20). That is the natural repeat cell
+// of a pointy-top honeycomb: one full central hexagon plus the six neighbours
+// whose bodies straddle the tile edges. Because each straddling hexagon is drawn
+// in full, the half that spills past one edge is completed pixel-for-pixel by the
+// matching half re-entering from the opposite edge on the next repeat — the six
+// vertical side edges land exactly on x = 0 and x = 34.641, the slanted edges
+// meet across y = 0 / y = 60, so the lattice interlocks with no seam and no
+// moiré. `backgroundSize: 34.641px 60px` locks the tile to that period; the glow
+// and vignette are single non-repeating layers sized to 100%.
+//
+// DARK vs LIGHT
+// Dark: cool cyan hex lines (oklch 0.72 0.1 200) on a deep blue-black base, with
+// a soft cyan-tinted central glow — the classic "cold HUD" look.
+// Light: soft slate-blue hexes (oklch 0.55 0.07 250) on a pale cool-white sheet,
+// with a bright paper highlight at centre. Each alpha/lightness is tuned
+// independently so both feel equally quiet against their own base.
+
+// One seamless honeycomb tile (√3·20 × 3·20). Colour is injected per-theme.
+const hexTile = (stroke: string): string =>
+ `url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2234.641%22%20height%3D%2260%22%3E%3Cpath%20d%3D%22M17.32%2010L0%2020L0%2040L17.32%2050L34.64%2040L34.64%2020Z%20M0%20-20L-17.32%20-10L-17.32%2010L0%2020L17.32%2010L17.32%20-10Z%20M34.64%20-20L17.32%20-10L17.32%2010L34.64%2020L51.96%2010L51.96%20-10Z%20M0%2040L-17.32%2050L-17.32%2070L0%2080L17.32%2070L17.32%2050Z%20M34.64%2040L17.32%2050L17.32%2070L34.64%2080L51.96%2070L51.96%2050Z%20M17.32%20-50L0%20-40L0%20-20L17.32%20-10L34.64%20-20L34.64%20-40Z%20M17.32%2070L0%2080L0%20100L17.32%20110L34.64%20100L34.64%2080Z%22%20fill%3D%22none%22%20stroke%3D%22${encodeURIComponent(
+ stroke,
+ )}%22%20stroke-width%3D%220.9%22%20stroke-linejoin%3D%22round%22%2F%3E%3C%2Fsvg%3E")`;
+
+const dark: CSSProperties = {
+ backgroundColor: 'oklch(0.19 0.03 245)',
+ backgroundImage: [
+ // 1. the honeycomb lattice — cool cyan hex outlines.
+ hexTile('oklch(0.72 0.1 200 / 0.14)'),
+ // 2. central glow — a soft cyan lift so the field looks lit from within.
+ 'radial-gradient(120% 90% at 50% 42%, oklch(0.30 0.05 210 / 0.55) 0%, transparent 60%)',
+ // 3. vignette — settles the corners into the deep base for depth.
+ 'radial-gradient(130% 130% at 50% 45%, transparent 55%, oklch(0.13 0.02 240 / 0.55) 100%)',
+ ].join(','),
+ backgroundSize: '34.641px 60px, 100% 100%, 100% 100%',
+};
+
+const light: CSSProperties = {
+ backgroundColor: 'oklch(0.965 0.008 240)',
+ backgroundImage: [
+ // 1. the honeycomb lattice — soft slate-blue hex outlines.
+ hexTile('oklch(0.55 0.07 250 / 0.16)'),
+ // 2. central highlight — a hint of brighter paper toward the middle.
+ 'radial-gradient(120% 90% at 50% 40%, oklch(0.99 0.005 240 / 0.7) 0%, transparent 60%)',
+ // 3. vignette — feather the edges into a slightly cooler paper.
+ 'radial-gradient(130% 130% at 50% 45%, transparent 58%, oklch(0.90 0.015 245 / 0.5) 100%)',
+ ].join(','),
+ backgroundSize: '34.641px 60px, 100% 100%, 100% 100%',
+};
+
+export const hexgrid: ChatBgVariants = { dark, light };
diff --git a/src/app/features/lotus/backgrounds/neon.ts b/src/app/features/lotus/backgrounds/neon.ts
new file mode 100644
index 000000000..7f6c3fc78
--- /dev/null
+++ b/src/app/features/lotus/backgrounds/neon.ts
@@ -0,0 +1,121 @@
+import { ChatBgVariants } from './types';
+
+// neon — a synthwave neon grid with real bloom, kept restrained for readability.
+//
+// Concept: a retro-futuristic magenta/cyan grid that *glows* rather than shouts.
+// The glow is built the way a real neon tube reads: a crisp hairline of light
+// sitting inside a much wider, softer halo of the same hue. We achieve this per
+// axis by stacking TWO linear-gradient layers that share the identical tile size
+// (so their lines land on exactly the same pixel column/row across every repeat):
+// - a wide "bloom" line: a fat, very-low-opacity band with a soft gradient
+// falloff on both sides (transparent -> colour -> transparent), reading as
+// out-of-focus glow;
+// - a crisp "core" line: a 1px bright hairline centred in that bloom.
+// A dark radial vignette then pulls the whole grid back toward the edges and
+// keeps the reading column — the calm centre — darkest and highest-contrast, so
+// text stays crisp. Pure CSS: only linear + radial gradients, no assets.
+//
+// Seamless tiling: every grid layer uses the SAME backgroundSize per axis
+// (magenta and cyan share one 88px module in dark; the fine cyan sub-grid is an
+// exact 1/2 divisor at 44px so it re-registers). Because the bloom and core for
+// an axis share a size and a 0/0 position, their lines are always co-registered
+// and no seam is possible. Vignette/wash layers are 100% 100% and never tile.
+
+export const neon: ChatBgVariants = {
+ // Dark: magenta + cyan tubes glowing over near-black, bloom kept low so the
+ // lines are felt, not read. Vignette darkens the centre for legibility.
+ dark: {
+ backgroundColor: 'oklch(0.135 0.02 285)',
+ backgroundImage: [
+ // 1. magenta core hairlines — crisp, bright, thin (vertical + horizontal)
+ 'linear-gradient(90deg, oklch(0.68 0.21 350 / 0.34) 0 1px, transparent 1px)',
+ 'linear-gradient(0deg, oklch(0.68 0.21 350 / 0.34) 0 1px, transparent 1px)',
+ // 2. magenta bloom — a wide soft halo hugging the same lines
+ 'linear-gradient(90deg, transparent 0, oklch(0.66 0.2 350 / 0.11) 3px, transparent 7px)',
+ 'linear-gradient(0deg, transparent 0, oklch(0.66 0.2 350 / 0.11) 3px, transparent 7px)',
+ // 3. cyan core hairlines on the offset half-grid — the cross accent
+ 'linear-gradient(90deg, oklch(0.82 0.13 200 / 0.20) 0 1px, transparent 1px)',
+ 'linear-gradient(0deg, oklch(0.82 0.13 200 / 0.20) 0 1px, transparent 1px)',
+ // 4. cyan bloom — soft cool halo on the same half-grid lines
+ 'linear-gradient(90deg, transparent 0, oklch(0.80 0.12 200 / 0.07) 2px, transparent 5px)',
+ 'linear-gradient(0deg, transparent 0, oklch(0.80 0.12 200 / 0.07) 2px, transparent 5px)',
+ // 5. vignette — recede the grid, keep the reading centre calm & dark
+ 'radial-gradient(ellipse 125% 95% at 50% 44%, transparent 34%, oklch(0.10 0.02 285 / 0.72) 100%)',
+ // 6. horizon wash — a faint magenta->cyan synthwave glow low on the canvas
+ 'radial-gradient(ellipse 150% 90% at 50% 108%, oklch(0.4 0.14 340 / 0.30) 0%, transparent 60%)',
+ ].join(','),
+ backgroundSize: [
+ '88px 88px', // magenta core V
+ '88px 88px', // magenta core H
+ '88px 88px', // magenta bloom V
+ '88px 88px', // magenta bloom H
+ '44px 44px', // cyan core V (exact 1/2 divisor — re-registers)
+ '44px 44px', // cyan core H
+ '44px 44px', // cyan bloom V
+ '44px 44px', // cyan bloom H
+ '100% 100%', // vignette
+ '100% 100%', // horizon wash
+ ].join(','),
+ backgroundPosition: [
+ '0 0', // magenta core V
+ '0 0', // magenta core H
+ '-3px 0', // magenta bloom V — centre the 7px halo on the 1px core
+ '0 -3px', // magenta bloom H
+ '22px 22px', // cyan core V — sit the fine grid between magenta lines
+ '22px 22px', // cyan core H
+ '20px 22px', // cyan bloom V — centre the 5px halo on the cyan core
+ '22px 20px', // cyan bloom H
+ '0 0', // vignette
+ '0 0', // horizon wash
+ ].join(','),
+ },
+
+ // Light: "neon" reinterpreted as a soft luminous violet/teal grid on a pale
+ // cool-white base — no glow-on-black, just gentle coloured light. Bloom is even
+ // lighter here; a subtle centre-brightening vignette lifts the reading column.
+ light: {
+ backgroundColor: 'oklch(0.972 0.006 275)',
+ backgroundImage: [
+ // 1. violet core hairlines — soft but defined
+ 'linear-gradient(90deg, oklch(0.55 0.17 330 / 0.16) 0 1px, transparent 1px)',
+ 'linear-gradient(0deg, oklch(0.55 0.17 330 / 0.16) 0 1px, transparent 1px)',
+ // 2. violet bloom — the merest wide halo for luminosity
+ 'linear-gradient(90deg, transparent 0, oklch(0.6 0.16 330 / 0.06) 3px, transparent 7px)',
+ 'linear-gradient(0deg, transparent 0, oklch(0.6 0.16 330 / 0.06) 3px, transparent 7px)',
+ // 3. teal core hairlines on the offset half-grid — cool accent
+ 'linear-gradient(90deg, oklch(0.58 0.11 200 / 0.11) 0 1px, transparent 1px)',
+ 'linear-gradient(0deg, oklch(0.58 0.11 200 / 0.11) 0 1px, transparent 1px)',
+ // 4. teal bloom — faint cool halo on the same half-grid lines
+ 'linear-gradient(90deg, transparent 0, oklch(0.62 0.1 200 / 0.045) 2px, transparent 5px)',
+ 'linear-gradient(0deg, transparent 0, oklch(0.62 0.1 200 / 0.045) 2px, transparent 5px)',
+ // 5. vignette — brighten the calm reading centre for max legibility
+ 'radial-gradient(ellipse 125% 95% at 50% 44%, oklch(1 0 0 / 0.50) 30%, transparent 100%)',
+ // 6. horizon wash — a whisper of violet->teal light low on the canvas
+ 'radial-gradient(ellipse 150% 90% at 50% 108%, oklch(0.8 0.09 320 / 0.28) 0%, transparent 60%)',
+ ].join(','),
+ backgroundSize: [
+ '88px 88px', // violet core V
+ '88px 88px', // violet core H
+ '88px 88px', // violet bloom V
+ '88px 88px', // violet bloom H
+ '44px 44px', // teal core V
+ '44px 44px', // teal core H
+ '44px 44px', // teal bloom V
+ '44px 44px', // teal bloom H
+ '100% 100%', // vignette
+ '100% 100%', // horizon wash
+ ].join(','),
+ backgroundPosition: [
+ '0 0', // violet core V
+ '0 0', // violet core H
+ '-3px 0', // violet bloom V
+ '0 -3px', // violet bloom H
+ '22px 22px', // teal core V
+ '22px 22px', // teal core H
+ '20px 22px', // teal bloom V
+ '22px 20px', // teal bloom H
+ '0 0', // vignette
+ '0 0', // horizon wash
+ ].join(','),
+ },
+};
diff --git a/src/app/features/lotus/backgrounds/plaid.ts b/src/app/features/lotus/backgrounds/plaid.ts
new file mode 100644
index 000000000..22a5600e7
--- /dev/null
+++ b/src/app/features/lotus/backgrounds/plaid.ts
@@ -0,0 +1,135 @@
+import { CSSProperties } from 'react';
+import { ChatBgVariants } from './types';
+
+// plaid — an authentic woven tartan, muted to a heather-wool hush.
+//
+// Real tartan is not a grid of lines: it is a *sett* — a repeating sequence of
+// coloured bands of different widths — thrown in BOTH warp (vertical) and weft
+// (horizontal) directions with the SAME sequence. Where a warp band crosses a
+// weft band of the same colour the yarn density doubles and the colour visibly
+// deepens; that reinforced overlap at every crossing is exactly what makes cloth
+// read as woven rather than printed. We reproduce that physically with
+// semi-transparent bands: a vertical band at alpha a and a horizontal band at
+// alpha a stack to ~2a where they cross (over transparent to 1x elsewhere), so
+// the crossings darken on their own with no extra layer.
+//
+// THE SETT (band widths across one tile)
+// We use a few closely-related widths for a wool-flannel rhythm rather than a
+// clean check: a wide ground band, a medium companion, and a thin accent
+// over-stripe of a warmer hue (the classic single guard line). The identical
+// sequence in warp and weft yields the tartan lattice. A faint diagonal twill
+// hatch sits on top at very low alpha to suggest the 2/2 twill thread angle of
+// woven wool. A soft central wash lifts the reading zone and a gentle vignette
+// settles the corners.
+//
+// SEAMLESS TILING
+// Every band layer is a `repeating-linear-gradient` whose stop sequence is
+// expressed in px and whose period divides the tile exactly (dark tile 96px:
+// wide=48, medium=24, accent=96; light tile 88px similarly). Warp layers repeat
+// at 0deg-across (90deg gradient) and weft at 0deg, sharing one square
+// `backgroundSize`, so the sett closes on itself with no seam in either axis.
+// The twill hatch is a repeating-linear-gradient on a small square tile that
+// divides the main tile. Wash and vignette are single non-repeating gradients
+// at 100% 100%, so they never seam.
+
+const dark: CSSProperties = {
+ // Deep muted forest-charcoal ground.
+ backgroundColor: 'oklch(0.19 0.018 155)',
+ backgroundImage: [
+ // Twill hatch — whisper-faint diagonal thread angle of the weave itself.
+ 'repeating-linear-gradient(45deg,' +
+ ' oklch(0.55 0.03 155 / 0.03) 0px, oklch(0.55 0.03 155 / 0.03) 1px,' +
+ ' transparent 1px, transparent 4px)',
+
+ // WEFT (horizontal bands) --------------------------------------------
+ // Wide muted-forest ground band.
+ 'repeating-linear-gradient(0deg,' +
+ ' oklch(0.45 0.05 150 / 0.14) 0px, oklch(0.45 0.05 150 / 0.14) 22px,' +
+ ' transparent 22px, transparent 48px)',
+ // Medium companion band (cooler, offset into the ground gap).
+ 'repeating-linear-gradient(0deg,' +
+ ' transparent 0px, transparent 60px,' +
+ ' oklch(0.42 0.035 175 / 0.11) 60px, oklch(0.42 0.035 175 / 0.11) 72px,' +
+ ' transparent 72px, transparent 96px)',
+ // Thin warm amber guard line — the single accent over-stripe.
+ 'repeating-linear-gradient(0deg,' +
+ ' transparent 0px, transparent 36px,' +
+ ' oklch(0.60 0.08 40 / 0.13) 36px, oklch(0.60 0.08 40 / 0.13) 38px,' +
+ ' transparent 38px, transparent 96px)',
+
+ // WARP (vertical bands, identical sett) -------------------------------
+ 'repeating-linear-gradient(90deg,' +
+ ' oklch(0.45 0.05 150 / 0.14) 0px, oklch(0.45 0.05 150 / 0.14) 22px,' +
+ ' transparent 22px, transparent 48px)',
+ 'repeating-linear-gradient(90deg,' +
+ ' transparent 0px, transparent 60px,' +
+ ' oklch(0.42 0.035 175 / 0.11) 60px, oklch(0.42 0.035 175 / 0.11) 72px,' +
+ ' transparent 72px, transparent 96px)',
+ 'repeating-linear-gradient(90deg,' +
+ ' transparent 0px, transparent 36px,' +
+ ' oklch(0.60 0.08 40 / 0.13) 36px, oklch(0.60 0.08 40 / 0.13) 38px,' +
+ ' transparent 38px, transparent 96px)',
+
+ // Tonal wash — soft warm-green lift through the reading centre.
+ 'radial-gradient(ellipse 92% 78% at 50% 42%, oklch(0.27 0.03 150 / 0.38) 0%, transparent 62%)',
+ // Vignette — feather the corners into deeper forest-charcoal.
+ 'radial-gradient(ellipse 122% 132% at 50% 45%, transparent 58%, oklch(0.14 0.016 155 / 0.55) 100%)',
+ ].join(','),
+ backgroundSize:
+ '8px 8px,' + // twill (multiple of 4px hatch period → seamless)
+ '96px 96px, 96px 96px, 96px 96px,' + // weft: wide, medium, accent
+ '96px 96px, 96px 96px, 96px 96px,' + // warp: wide, medium, accent
+ '100% 100%, 100% 100%', // wash, vignette
+};
+
+const light: CSSProperties = {
+ // Warm off-white paper ground.
+ backgroundColor: 'oklch(0.965 0.007 85)',
+ backgroundImage: [
+ // Twill hatch — faint diagonal weave angle on paper.
+ 'repeating-linear-gradient(45deg,' +
+ ' oklch(0.45 0.03 250 / 0.025) 0px, oklch(0.45 0.03 250 / 0.025) 1px,' +
+ ' transparent 1px, transparent 4px)',
+
+ // WEFT (horizontal bands) --------------------------------------------
+ // Wide dusty-blue ground band.
+ 'repeating-linear-gradient(0deg,' +
+ ' oklch(0.60 0.045 245 / 0.12) 0px, oklch(0.60 0.045 245 / 0.12) 20px,' +
+ ' transparent 20px, transparent 44px)',
+ // Medium greige companion band.
+ 'repeating-linear-gradient(0deg,' +
+ ' transparent 0px, transparent 55px,' +
+ ' oklch(0.62 0.018 90 / 0.11) 55px, oklch(0.62 0.018 90 / 0.11) 66px,' +
+ ' transparent 66px, transparent 88px)',
+ // Thin warm sand guard line — the single accent over-stripe.
+ 'repeating-linear-gradient(0deg,' +
+ ' transparent 0px, transparent 33px,' +
+ ' oklch(0.68 0.06 55 / 0.12) 33px, oklch(0.68 0.06 55 / 0.12) 35px,' +
+ ' transparent 35px, transparent 88px)',
+
+ // WARP (vertical bands, identical sett) -------------------------------
+ 'repeating-linear-gradient(90deg,' +
+ ' oklch(0.60 0.045 245 / 0.12) 0px, oklch(0.60 0.045 245 / 0.12) 20px,' +
+ ' transparent 20px, transparent 44px)',
+ 'repeating-linear-gradient(90deg,' +
+ ' transparent 0px, transparent 55px,' +
+ ' oklch(0.62 0.018 90 / 0.11) 55px, oklch(0.62 0.018 90 / 0.11) 66px,' +
+ ' transparent 66px, transparent 88px)',
+ 'repeating-linear-gradient(90deg,' +
+ ' transparent 0px, transparent 33px,' +
+ ' oklch(0.68 0.06 55 / 0.12) 33px, oklch(0.68 0.06 55 / 0.12) 35px,' +
+ ' transparent 35px, transparent 88px)',
+
+ // Tonal wash — warm paper highlight through the reading centre.
+ 'radial-gradient(ellipse 92% 78% at 50% 42%, oklch(0.99 0.008 85 / 0.55) 0%, transparent 62%)',
+ // Vignette — settle the corners into a slightly deeper dusty tone.
+ 'radial-gradient(ellipse 122% 132% at 50% 45%, transparent 58%, oklch(0.90 0.014 245 / 0.40) 100%)',
+ ].join(','),
+ backgroundSize:
+ '8px 8px,' + // twill (multiple of 4px hatch period → seamless)
+ '88px 88px, 88px 88px, 88px 88px,' + // weft: wide, medium, accent
+ '88px 88px, 88px 88px, 88px 88px,' + // warp: wide, medium, accent
+ '100% 100%, 100% 100%', // wash, vignette
+};
+
+export const plaid: ChatBgVariants = { dark, light };
diff --git a/src/app/features/lotus/backgrounds/polka.ts b/src/app/features/lotus/backgrounds/polka.ts
new file mode 100644
index 000000000..4fb043318
--- /dev/null
+++ b/src/app/features/lotus/backgrounds/polka.ts
@@ -0,0 +1,59 @@
+import { CSSProperties } from 'react';
+import { ChatBgVariants } from './types';
+
+// polka — a grown-up polka dot: embossed leather / fine letterpress stationery,
+// not childish spots. Each dot is not a flat circle but a soft radial "bump":
+// an off-centre highlight fading into a faint recessed shadow, so it reads as a
+// gently raised (or debossed) node catching a single top-left light. Two subtly
+// different dot sizes are staggered on a half-tile offset for a refined,
+// hand-set rhythm, and a large single vignette gradient adds quiet depth toward
+// the edges.
+//
+// SEAMLESS TILING: both dot layers repeat on the SAME 44px cell (backgroundSize
+// 44px 44px). The larger "primary" dots sit at 0 0; the smaller "secondary"
+// dots are shifted by exactly half a tile (22px 22px) so they fall in the gaps
+// of the primary lattice — a true staggered brick layout that wraps with no
+// seam. Each radial gradient's highlight/shadow rings are fully enclosed well
+// inside its cell, so nothing is clipped at a tile boundary. The vignette is a
+// single non-repeating gradient covering the whole element ('cover').
+//
+// SUBTLETY: dot opacities live in the 0.03–0.10 range and every dot fades to
+// transparent over a soft edge (no hard rim), so the surface is felt as tactile
+// grain rather than read as dots. Crisp message text sits comfortably above it
+// in both themes (WCAG-AA safe).
+
+const dark: CSSProperties = {
+ // deep espresso base — warm, near-black brown
+ backgroundColor: 'oklch(0.19 0.018 65)',
+ backgroundImage: [
+ // vignette — corners settle darker so the field feels like supple leather
+ 'radial-gradient(120% 120% at 50% 40%, oklch(0.22 0.02 65 / 0.5) 0%, oklch(0.19 0.018 65 / 0) 55%, oklch(0.15 0.015 60 / 0.55) 100%)',
+ // PRIMARY dot — larger raised pearl. Top-left warm highlight, then the body,
+ // then a whisper of shadow at the lower-right rim for embossed dimension.
+ 'radial-gradient(circle at 42% 40%, oklch(0.82 0.02 80 / 0.10) 0%, oklch(0.80 0.02 80 / 0.075) 22%, oklch(0.55 0.02 70 / 0.045) 44%, oklch(0.12 0.01 60 / 0.05) 62%, transparent 72%)',
+ // SECONDARY dot — smaller, staggered into the gaps, fainter for depth layering
+ 'radial-gradient(circle at 42% 40%, oklch(0.82 0.02 80 / 0.075) 0%, oklch(0.78 0.02 80 / 0.05) 26%, oklch(0.50 0.02 70 / 0.03) 52%, oklch(0.12 0.01 60 / 0.04) 70%, transparent 82%)',
+ ].join(','),
+ // primary dots ~9px, secondary ~6px, both on the same 44px lattice
+ backgroundSize: 'cover, 44px 44px, 44px 44px',
+ // secondary offset by half a tile => staggered brick lattice
+ backgroundPosition: 'center, 0 0, 22px 22px',
+};
+
+const light: CSSProperties = {
+ // cream stationery base — warm off-white paper stock
+ backgroundColor: 'oklch(0.975 0.008 85)',
+ backgroundImage: [
+ // vignette — a gentle warm settling toward the edges, like heavy cotton paper
+ 'radial-gradient(120% 120% at 50% 40%, oklch(0.99 0.006 85 / 0.5) 0%, oklch(0.975 0.008 85 / 0) 55%, oklch(0.945 0.012 80 / 0.55) 100%)',
+ // PRIMARY dot — soft taupe deboss. Faint paper highlight at top-left, taupe
+ // body, then a soft shadow lower-right so each dot reads pressed into the sheet.
+ 'radial-gradient(circle at 42% 40%, oklch(0.99 0.004 85 / 0.35) 0%, oklch(0.72 0.02 70 / 0.075) 30%, oklch(0.60 0.025 65 / 0.09) 50%, oklch(0.55 0.025 60 / 0.05) 66%, transparent 76%)',
+ // SECONDARY dot — smaller, staggered, lighter for a two-tier hand-set rhythm
+ 'radial-gradient(circle at 42% 40%, oklch(0.99 0.004 85 / 0.28) 0%, oklch(0.74 0.02 70 / 0.05) 34%, oklch(0.62 0.025 65 / 0.06) 56%, oklch(0.56 0.025 60 / 0.035) 72%, transparent 84%)',
+ ].join(','),
+ backgroundSize: 'cover, 44px 44px, 44px 44px',
+ backgroundPosition: 'center, 0 0, 22px 22px',
+};
+
+export const polka: ChatBgVariants = { dark, light };
diff --git a/src/app/features/lotus/backgrounds/stars.ts b/src/app/features/lotus/backgrounds/stars.ts
new file mode 100644
index 000000000..a5838401c
--- /dev/null
+++ b/src/app/features/lotus/backgrounds/stars.ts
@@ -0,0 +1,91 @@
+import { ChatBgVariants } from './types';
+
+// stars — a deep-space starfield with subtle depth.
+//
+// Concept: three parallax layers of stars at different tile sizes and offsets
+// (so the repeat never lines up and reads as a genuine random field), lifted
+// onto a faint deep-blue->violet nebula wash for depth, and finished with a
+// gentle center vignette that keeps the reading column the calmest area of the
+// canvas. Every layer is a stacked radial-gradient — pure CSS, no assets.
+//
+// Layer stacking order (topmost first, as CSS paints image #1 on top):
+// 1. bright near stars (crisp, sparse, largest tile)
+// 2. mid stars (dimmer, medium tile)
+// 3. faint blue far stars (haze, smallest tile — most repeats, least visible)
+// 4. calming center vignette
+// 5. nebula wash (deep blue -> violet)
+// The three star tiles use coprime-ish sizes (137/191/233 dark) so their least
+// common repeat is enormous and no seam is perceivable.
+
+export const stars: ChatBgVariants = {
+ // Dark: bright/dim white + faint blue stars on a near-black cosmos, with a
+ // deep-blue->violet nebula and a soft vignette that darkens the calm center.
+ dark: {
+ backgroundColor: 'oklch(0.16 0.03 275)',
+ backgroundImage: [
+ // 1. bright near stars — crisp cool-white, sparse
+ 'radial-gradient(circle at center, oklch(0.98 0.01 260 / 0.85) 0.6px, transparent 1.4px)',
+ // 2. mid stars — softer, more of them
+ 'radial-gradient(circle at center, oklch(0.92 0.02 265 / 0.55) 0.6px, transparent 1.3px)',
+ // 3. faint blue far dust — the parallax haze
+ 'radial-gradient(circle at center, oklch(0.80 0.06 255 / 0.30) 0.5px, transparent 1.1px)',
+ // 4. center vignette — keeps the reading column calmest
+ 'radial-gradient(ellipse 120% 90% at 50% 42%, transparent 42%, oklch(0.10 0.03 270 / 0.55) 100%)',
+ // 5. nebula wash — deep blue -> violet drift
+ 'radial-gradient(ellipse 140% 120% at 78% 12%, oklch(0.25 0.08 280 / 0.55) 0%, transparent 55%)',
+ 'radial-gradient(ellipse 130% 110% at 18% 92%, oklch(0.20 0.06 250 / 0.50) 0%, transparent 58%)',
+ ].join(','),
+ backgroundSize: [
+ '137px 137px', // near stars
+ '191px 191px', // mid stars
+ '233px 233px', // far dust
+ '100% 100%', // vignette
+ '100% 100%', // nebula A
+ '100% 100%', // nebula B
+ ].join(','),
+ backgroundPosition: [
+ '0 0', // near
+ '61px 43px', // mid (offset breaks alignment)
+ '113px 97px', // far (offset again)
+ '0 0', // vignette
+ '0 0', // nebula A
+ '0 0', // nebula B
+ ].join(','),
+ },
+
+ // Light: an airy pre-dawn sky. No literal white stars on white — instead very
+ // soft pale sparkles paired with the faintest cool-grey speckles, floated on a
+ // gentle cool gradient. Reads as elegant atmosphere, never as noise over text.
+ light: {
+ backgroundColor: 'oklch(0.965 0.008 255)',
+ backgroundImage: [
+ // 1. pale warm pre-dawn sparkles — a hair brighter than the sky
+ 'radial-gradient(circle at center, oklch(0.995 0.015 90 / 0.55) 0.6px, transparent 1.4px)',
+ // 2. tiny cool speckles — the merest hint of darkness for texture/contrast
+ 'radial-gradient(circle at center, oklch(0.62 0.05 260 / 0.16) 0.5px, transparent 1.2px)',
+ // 3. faint far dust — very soft, most-repeated layer
+ 'radial-gradient(circle at center, oklch(0.70 0.04 255 / 0.12) 0.5px, transparent 1.1px)',
+ // 4. center vignette — brightens the calm reading center slightly
+ 'radial-gradient(ellipse 120% 90% at 50% 44%, oklch(1 0 0 / 0.45) 30%, transparent 100%)',
+ // 5. pre-dawn wash — cool blue high, warm blush low
+ 'radial-gradient(ellipse 150% 120% at 80% 8%, oklch(0.90 0.05 255 / 0.60) 0%, transparent 60%)',
+ 'radial-gradient(ellipse 140% 120% at 15% 95%, oklch(0.93 0.04 40 / 0.45) 0%, transparent 62%)',
+ ].join(','),
+ backgroundSize: [
+ '149px 149px', // sparkles
+ '199px 199px', // speckles
+ '251px 251px', // far dust
+ '100% 100%', // vignette
+ '100% 100%', // wash A
+ '100% 100%', // wash B
+ ].join(','),
+ backgroundPosition: [
+ '0 0', // sparkles
+ '71px 53px', // speckles
+ '127px 109px', // far dust
+ '0 0', // vignette
+ '0 0', // wash A
+ '0 0', // wash B
+ ].join(','),
+ },
+};
diff --git a/src/app/features/lotus/backgrounds/tactical.ts b/src/app/features/lotus/backgrounds/tactical.ts
new file mode 100644
index 000000000..2caf280f5
--- /dev/null
+++ b/src/app/features/lotus/backgrounds/tactical.ts
@@ -0,0 +1,93 @@
+import { ChatBgVariants } from './types';
+
+// tactical — a military tactical display / recon coordinate grid (MGRS-style).
+//
+// The motif is a fine grid nested inside bold sector squares, with a reticle
+// crosshair (arms + ring) at every sector intersection, small stencil corner
+// brackets inside each sector, and coordinate tick-marks along the sector edges
+// — a convincing mil-spec map overlay rather than a plain dot grid.
+//
+// Layers (painted top-to-bottom):
+// 1. SVG reticle/stencil tile (128px). Corner arms + quarter-ring arcs radiate
+// from each of the four tile corners, so four neighbouring tiles combine
+// into ONE full crosshair "+" with a full ring at every sector intersection.
+// The tile also carries L-shaped stencil brackets, edge coordinate ticks and
+// a micro centre reticle. Because every mark is anchored to the 128px tile
+// lattice, it stays phase-locked to the grids below — no seams, no drift.
+// 2. Sector lines (heavier) — 128px.
+// 3. Fine recon grid (fainter) — 16px (128 = 8 × 16, so it nests
+// exactly inside every sector with no beat/moiré).
+// 4. A soft scan vignette that keeps the CENTRE calm and clear for text while
+// letting the grid fall away slightly toward the edges — dimension without
+// contrast.
+//
+// All strokes sit at low alpha (~0.03–0.30 on 1px marks) so the display is felt,
+// not read: crisp message text stays comfortably WCAG-AA legible in both themes.
+// A single shared top-left (0 0) origin keeps the reticle tile, the 128px sector
+// grid and the 16px fine grid all in phase.
+
+const DARK_RETICLE =
+ 'url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22128%22%20height%3D%22128%22%3E%3Cg%20stroke%3D%22oklch%280.72%200.13%2085%20%2F%200.30%29%22%20stroke-width%3D%221%22%20fill%3D%22none%22%3E%3Cpath%20d%3D%22M0%200%20H14%20M0%200%20V14%22%2F%3E%3Cpath%20d%3D%22M128%200%20H114%20M128%200%20V14%22%2F%3E%3Cpath%20d%3D%22M0%20128%20H14%20M0%20128%20V114%22%2F%3E%3Cpath%20d%3D%22M128%20128%20H114%20M128%20128%20V114%22%2F%3E%3Ccircle%20cx%3D%220%22%20cy%3D%220%22%20r%3D%226%22%2F%3E%3Ccircle%20cx%3D%22128%22%20cy%3D%220%22%20r%3D%226%22%2F%3E%3Ccircle%20cx%3D%220%22%20cy%3D%22128%22%20r%3D%226%22%2F%3E%3Ccircle%20cx%3D%22128%22%20cy%3D%22128%22%20r%3D%226%22%2F%3E%3C%2Fg%3E%3Cg%20stroke%3D%22oklch%280.72%200.10%2095%20%2F%200.22%29%22%20stroke-width%3D%221%22%20fill%3D%22none%22%3E%3Cpath%20d%3D%22M10%2020%20V10%20H20%22%2F%3E%3Cpath%20d%3D%22M118%2020%20V10%20H108%22%2F%3E%3Cpath%20d%3D%22M10%20108%20V118%20H20%22%2F%3E%3Cpath%20d%3D%22M118%20108%20V118%20H108%22%2F%3E%3C%2Fg%3E%3Cg%20stroke%3D%22oklch%280.72%200.10%2095%20%2F%200.22%29%22%20stroke-width%3D%221%22%20fill%3D%22none%22%3E%3Cpath%20d%3D%22M64%200%20V6%20M64%20128%20V122%20M0%2064%20H6%20M128%2064%20H122%22%2F%3E%3C%2Fg%3E%3Cg%20stroke%3D%22oklch%280.72%200.13%2085%20%2F%200.30%29%22%20stroke-width%3D%221%22%20fill%3D%22none%22%3E%3Cpath%20d%3D%22M64%2058%20V70%20M58%2064%20H70%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E")';
+
+const LIGHT_RETICLE =
+ 'url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22128%22%20height%3D%22128%22%3E%3Cg%20stroke%3D%22oklch%280.45%200.07%20120%20%2F%200.40%29%22%20stroke-width%3D%221%22%20fill%3D%22none%22%3E%3Cpath%20d%3D%22M0%200%20H14%20M0%200%20V14%22%2F%3E%3Cpath%20d%3D%22M128%200%20H114%20M128%200%20V14%22%2F%3E%3Cpath%20d%3D%22M0%20128%20H14%20M0%20128%20V114%22%2F%3E%3Cpath%20d%3D%22M128%20128%20H114%20M128%20128%20V114%22%2F%3E%3Ccircle%20cx%3D%220%22%20cy%3D%220%22%20r%3D%226%22%2F%3E%3Ccircle%20cx%3D%22128%22%20cy%3D%220%22%20r%3D%226%22%2F%3E%3Ccircle%20cx%3D%220%22%20cy%3D%22128%22%20r%3D%226%22%2F%3E%3Ccircle%20cx%3D%22128%22%20cy%3D%22128%22%20r%3D%226%22%2F%3E%3C%2Fg%3E%3Cg%20stroke%3D%22oklch%280.42%200.05%20130%20%2F%200.28%29%22%20stroke-width%3D%221%22%20fill%3D%22none%22%3E%3Cpath%20d%3D%22M10%2020%20V10%20H20%22%2F%3E%3Cpath%20d%3D%22M118%2020%20V10%20H108%22%2F%3E%3Cpath%20d%3D%22M10%20108%20V118%20H20%22%2F%3E%3Cpath%20d%3D%22M118%20108%20V118%20H108%22%2F%3E%3C%2Fg%3E%3Cg%20stroke%3D%22oklch%280.42%200.05%20130%20%2F%200.28%29%22%20stroke-width%3D%221%22%20fill%3D%22none%22%3E%3Cpath%20d%3D%22M64%200%20V6%20M64%20128%20V122%20M0%2064%20H6%20M128%2064%20H122%22%2F%3E%3C%2Fg%3E%3Cg%20stroke%3D%22oklch%280.45%200.07%20120%20%2F%200.40%29%22%20stroke-width%3D%221%22%20fill%3D%22none%22%3E%3Cpath%20d%3D%22M64%2058%20V70%20M58%2064%20H70%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E")';
+
+export const tactical: ChatBgVariants = {
+ // Phosphor amber/olive lines glowing on a near-black recon display.
+ dark: {
+ backgroundColor: 'oklch(0.17 0.012 95)',
+ backgroundImage: [
+ // 1. reticles + stencil brackets + coordinate ticks
+ DARK_RETICLE,
+ // 4. scan vignette: keeps the centre calm, eases grid contrast at edges.
+ 'radial-gradient(135% 120% at 50% 46%, transparent 52%, oklch(0.11 0.01 100 / 0.55) 100%)',
+ // a faint phosphor bloom drifting off the top so the black isn't dead flat.
+ 'radial-gradient(120% 90% at 50% 0%, oklch(0.24 0.03 90 / 0.45) 0%, transparent 60%)',
+ // 2. sector lines (heavier)
+ 'linear-gradient(oklch(0.72 0.13 85 / 0.11) 1px, transparent 1px)',
+ 'linear-gradient(90deg, oklch(0.72 0.13 85 / 0.11) 1px, transparent 1px)',
+ // 3. fine recon grid (fainter)
+ 'linear-gradient(oklch(0.72 0.11 90 / 0.045) 1px, transparent 1px)',
+ 'linear-gradient(90deg, oklch(0.72 0.11 90 / 0.045) 1px, transparent 1px)',
+ ].join(','),
+ backgroundSize: [
+ '128px 128px', // reticle tile
+ '100% 100%', // vignette
+ '100% 100%', // phosphor bloom
+ '128px 128px', // sector V
+ '128px 128px', // sector H
+ '16px 16px', // fine V
+ '16px 16px', // fine H
+ ].join(','),
+ // Shared top-left origin: reticle tile + 128px sector grid + 16px fine grid
+ // (128 = 8 × 16) stay phase-locked, so corner arms land on sector crossings.
+ },
+
+ // Olive-graphite recon grid printed on cool tactical paper.
+ light: {
+ backgroundColor: 'oklch(0.95 0.008 120)',
+ backgroundImage: [
+ LIGHT_RETICLE,
+ // scan vignette: gentle cool shading into the corners, calm centre.
+ 'radial-gradient(135% 120% at 50% 46%, transparent 56%, oklch(0.86 0.02 125 / 0.5) 100%)',
+ // paper sheen toward the top so the surface reads like a printed sheet.
+ 'radial-gradient(120% 90% at 50% 0%, oklch(0.98 0.006 120 / 0.7) 0%, transparent 60%)',
+ // sector lines (heavier)
+ 'linear-gradient(oklch(0.45 0.07 120 / 0.14) 1px, transparent 1px)',
+ 'linear-gradient(90deg, oklch(0.45 0.07 120 / 0.14) 1px, transparent 1px)',
+ // fine recon grid (fainter)
+ 'linear-gradient(oklch(0.45 0.06 125 / 0.055) 1px, transparent 1px)',
+ 'linear-gradient(90deg, oklch(0.45 0.06 125 / 0.055) 1px, transparent 1px)',
+ ].join(','),
+ backgroundSize: [
+ '128px 128px',
+ '100% 100%',
+ '100% 100%',
+ '128px 128px',
+ '128px 128px',
+ '16px 16px',
+ '16px 16px',
+ ].join(','),
+ // Shared top-left origin keeps the reticle tile and both grids phase-locked.
+ },
+};
diff --git a/src/app/features/lotus/backgrounds/topographic.ts b/src/app/features/lotus/backgrounds/topographic.ts
new file mode 100644
index 000000000..06a3b9166
--- /dev/null
+++ b/src/app/features/lotus/backgrounds/topographic.ts
@@ -0,0 +1,73 @@
+import { CSSProperties } from 'react';
+import { ChatBgVariants } from './types';
+
+// topographic — an elegant contour / elevation map.
+//
+// The motif is a delicate cartographic contour survey: nested rings suggest two
+// gentle "peaks" and a shallow "valley", drawn with occasional heavier "index
+// contour" lines for authenticity, all floating over a soft tonal wash. It is
+// tuned to be *felt, not read* — line opacities sit well under legibility
+// thresholds so crisp message text stays comfortably WCAG-AA in both themes.
+//
+// SEAMLESS TILING
+// Each contour system is a `repeating-radial-gradient` whose ring period P is a
+// clean divisor of its `backgroundSize` tile. A repeating-radial-gradient tiles
+// seamlessly only when the tile edge falls on a whole number of ring periods, so
+// every layer below uses tile = N * P. Peak A's fine (32px) and index (128px)
+// layers share one 256px tile (256 = 8*32 = 2*128) AND one center, so the heavy
+// index lines land exactly on every 4th fine ring — a true index contour, never
+// drifting out of register. Peak B tiles 288 = 12*24; the valley tiles 384 =
+// 8*48. The tonal washes/vignette are single non-repeating gradients sized to
+// the same tiles, so nothing shows a visible seam.
+
+const dark: CSSProperties = {
+ backgroundColor: 'oklch(0.205 0.018 235)',
+ backgroundImage: [
+ // Peak A — fine contour lines (soft teal), 32px period.
+ 'repeating-radial-gradient(circle at 27% 34%, transparent 0, transparent 26px,' +
+ ' oklch(0.62 0.055 190 / 0.09) 27px, oklch(0.62 0.055 190 / 0.09) 28px, transparent 29px, transparent 32px)',
+ // Peak A — index (heavier) contour every 4th ring, 128px period.
+ 'repeating-radial-gradient(circle at 27% 34%, transparent 0, transparent 122px,' +
+ ' oklch(0.66 0.06 190 / 0.10) 123px, oklch(0.66 0.06 190 / 0.10) 125px, transparent 126px, transparent 128px)',
+ // Peak B — fine contour lines (cooler sage-teal), 24px period.
+ 'repeating-radial-gradient(circle at 78% 72%, transparent 0, transparent 19px,' +
+ ' oklch(0.60 0.05 200 / 0.07) 20px, oklch(0.60 0.05 200 / 0.07) 21px, transparent 22px, transparent 24px)',
+ // Valley — broad shallow rings (very faint), 48px period.
+ 'repeating-radial-gradient(circle at 52% 8%, transparent 0, transparent 42px,' +
+ ' oklch(0.58 0.045 195 / 0.05) 43px, oklch(0.58 0.045 195 / 0.05) 44px, transparent 45px, transparent 48px)',
+ // Tonal wash — lifts the "peaks", sinks the corners for depth.
+ 'radial-gradient(circle at 27% 34%, oklch(0.26 0.03 200 / 0.55) 0%, transparent 46%)',
+ 'radial-gradient(circle at 78% 72%, oklch(0.24 0.028 205 / 0.45) 0%, transparent 44%)',
+ // Vignette — soft edge darkening keeps the field calm behind text.
+ 'radial-gradient(ellipse 120% 130% at 50% 42%, transparent 58%, oklch(0.15 0.02 235 / 0.55) 100%)',
+ ].join(','),
+ backgroundSize:
+ '256px 256px, 256px 256px, 288px 288px, 384px 384px, 100% 100%, 100% 100%, 100% 100%',
+};
+
+const light: CSSProperties = {
+ backgroundColor: 'oklch(0.965 0.008 85)',
+ backgroundImage: [
+ // Peak A — fine contour lines (warm graphite/sand), 32px period.
+ 'repeating-radial-gradient(circle at 27% 34%, transparent 0, transparent 26px,' +
+ ' oklch(0.45 0.03 70 / 0.08) 27px, oklch(0.45 0.03 70 / 0.08) 28px, transparent 29px, transparent 32px)',
+ // Peak A — index (heavier) contour every 4th ring, 128px period.
+ 'repeating-radial-gradient(circle at 27% 34%, transparent 0, transparent 122px,' +
+ ' oklch(0.40 0.035 68 / 0.10) 123px, oklch(0.40 0.035 68 / 0.10) 125px, transparent 126px, transparent 128px)',
+ // Peak B — fine contour lines (soft sage-graphite), 24px period.
+ 'repeating-radial-gradient(circle at 78% 72%, transparent 0, transparent 19px,' +
+ ' oklch(0.47 0.028 120 / 0.06) 20px, oklch(0.47 0.028 120 / 0.06) 21px, transparent 22px, transparent 24px)',
+ // Valley — broad shallow rings (very faint), 48px period.
+ 'repeating-radial-gradient(circle at 52% 8%, transparent 0, transparent 42px,' +
+ ' oklch(0.46 0.025 75 / 0.045) 43px, oklch(0.46 0.025 75 / 0.045) 44px, transparent 45px, transparent 48px)',
+ // Tonal wash — warm paper highlights over the "peaks".
+ 'radial-gradient(circle at 27% 34%, oklch(0.985 0.012 85 / 0.60) 0%, transparent 46%)',
+ 'radial-gradient(circle at 78% 72%, oklch(0.945 0.014 95 / 0.45) 0%, transparent 44%)',
+ // Vignette — feather the edges to a slightly deeper sand for depth.
+ 'radial-gradient(ellipse 120% 130% at 50% 42%, transparent 58%, oklch(0.90 0.016 80 / 0.45) 100%)',
+ ].join(','),
+ backgroundSize:
+ '256px 256px, 256px 256px, 288px 288px, 384px 384px, 100% 100%, 100% 100%, 100% 100%',
+};
+
+export const topographic: ChatBgVariants = { dark, light };
diff --git a/src/app/features/lotus/backgrounds/triangles.ts b/src/app/features/lotus/backgrounds/triangles.ts
new file mode 100644
index 000000000..c6f5ff1fe
--- /dev/null
+++ b/src/app/features/lotus/backgrounds/triangles.ts
@@ -0,0 +1,103 @@
+import { CSSProperties } from 'react';
+import { ChatBgVariants } from './types';
+
+// triangles — elegant low-poly / faceted-crystal mesh.
+//
+// The motif stays true to its name — a triangular tessellation — but is rebuilt
+// to read as a *faceted crystalline surface* rather than the old flat isometric
+// lines. Neighbouring triangular facets carry barely-there tonal shifts (one
+// face catches a whisper of light, the adjacent one falls into a whisper of
+// shade) so the plane looks gently faceted and dimensional, like brushed slate
+// or cut glass seen at a shallow angle. A hairline "mesh glint" traces the facet
+// edges so the crystalline structure is felt, never read. A soft tonal wash and
+// a feathered vignette give the whole field quiet architectural depth.
+//
+// FACET SHADING
+// An isometric triangle grid is three families of parallel lines at 0deg, 60deg
+// and 120deg. Each `linear-gradient` below is a *hard-edged* two-band ramp along
+// one of those axes: a faint tonal band followed by transparent, repeating
+// across the tile. Overlapping the three axes partitions the plane into small
+// triangular cells; because each axis contributes its shade to a different set
+// of cells, up-pointing and down-pointing facets end up carrying subtly
+// different summed tones — the alternating light/shadow facet look. A separate
+// hairline layer per axis draws the thin edge glint at the facet borders.
+//
+// SEAMLESS TILING
+// Equilateral geometry needs the tile height to be the width times sqrt(3). We
+// use a 48x83px tile (48 * 1.732 = 83.1, rounded to 83) so the 60deg/120deg
+// ramps close exactly on the tile box, and the horizontal edge family repeats on
+// half-height (48x41.5 -> the 0deg hairline is sized to the full tile so its
+// bands land on tile edges). Every facet-shade and edge layer shares this tile
+// (or an exact multiple), and the 60/120 layers meet at the tile's mid columns,
+// so triangles interlock across every seam with no drift. Wash and vignette are
+// single non-repeating gradients at 100% 100%, so they never seam.
+
+const dark: CSSProperties = {
+ // Deep navy base — the crystal sits on cool night stone.
+ backgroundColor: 'oklch(0.19 0.028 258)',
+ backgroundImage: [
+ // --- Facet shading: three cool-slate tonal ramps, one per triangle axis.
+ // Ascending-diagonal facets — a soft light band on one face family.
+ 'linear-gradient(60deg,' +
+ ' oklch(0.46 0.03 250 / 0.07) 0%, oklch(0.46 0.03 250 / 0.07) 50%,' +
+ ' transparent 50%, transparent 100%)',
+ // Descending-diagonal facets — the shade family, closing the triangles.
+ 'linear-gradient(120deg,' +
+ ' oklch(0.34 0.03 255 / 0.06) 0%, oklch(0.34 0.03 255 / 0.06) 50%,' +
+ ' transparent 50%, transparent 100%)',
+ // Horizontal facets — a third, fainter slate band so cells read three-sided.
+ 'linear-gradient(0deg,' +
+ ' oklch(0.42 0.028 248 / 0.045) 0%, oklch(0.42 0.028 248 / 0.045) 50%,' +
+ ' transparent 50%, transparent 100%)',
+ // --- Mesh glint: hairline edges tracing the crystalline facet borders.
+ 'linear-gradient(60deg, transparent 0, transparent calc(50% - 0.5px),' +
+ ' oklch(0.62 0.035 250 / 0.10) calc(50% - 0.5px), oklch(0.62 0.035 250 / 0.10) 50%,' +
+ ' transparent 50%)',
+ 'linear-gradient(120deg, transparent 0, transparent calc(50% - 0.5px),' +
+ ' oklch(0.62 0.035 250 / 0.10) calc(50% - 0.5px), oklch(0.62 0.035 250 / 0.10) 50%,' +
+ ' transparent 50%)',
+ // --- Tonal wash — a gentle cool lift through the reading centre for depth.
+ 'radial-gradient(ellipse 95% 80% at 50% 40%, oklch(0.28 0.03 255 / 0.45) 0%, transparent 62%)',
+ // --- Vignette — feather the corners into deeper navy.
+ 'radial-gradient(ellipse 125% 130% at 50% 45%, transparent 58%, oklch(0.13 0.022 258 / 0.55) 100%)',
+ ].join(','),
+ backgroundSize: '48px 83px, 48px 83px, 48px 83px, 48px 83px, 48px 83px, 100% 100%, 100% 100%',
+ // Offset the 120deg (shade) and its glint by half a tile so up/down facets
+ // interlock — this is what alternates the light/shadow triangles.
+ backgroundPosition: '0 0, 24px 0, 0 0, 0 0, 24px 0, 0 0, 0 0',
+};
+
+const light: CSSProperties = {
+ // Pale ice-white base — cut glass on frosted paper.
+ backgroundColor: 'oklch(0.975 0.004 250)',
+ backgroundImage: [
+ // --- Facet shading: soft cool-grey tonal ramps, one per triangle axis.
+ // Ascending-diagonal facets — a barely-there shade on one face family.
+ 'linear-gradient(60deg,' +
+ ' oklch(0.66 0.022 252 / 0.09) 0%, oklch(0.66 0.022 252 / 0.09) 50%,' +
+ ' transparent 50%, transparent 100%)',
+ // Descending-diagonal facets — a hair darker, closing the triangles.
+ 'linear-gradient(120deg,' +
+ ' oklch(0.58 0.024 255 / 0.08) 0%, oklch(0.58 0.024 255 / 0.08) 50%,' +
+ ' transparent 50%, transparent 100%)',
+ // Horizontal facets — the third, faintest cool-grey band.
+ 'linear-gradient(0deg,' +
+ ' oklch(0.62 0.02 250 / 0.055) 0%, oklch(0.62 0.02 250 / 0.055) 50%,' +
+ ' transparent 50%, transparent 100%)',
+ // --- Mesh glint: crisp hairline facet edges in cool slate.
+ 'linear-gradient(60deg, transparent 0, transparent calc(50% - 0.5px),' +
+ ' oklch(0.50 0.03 255 / 0.11) calc(50% - 0.5px), oklch(0.50 0.03 255 / 0.11) 50%,' +
+ ' transparent 50%)',
+ 'linear-gradient(120deg, transparent 0, transparent calc(50% - 0.5px),' +
+ ' oklch(0.50 0.03 255 / 0.11) calc(50% - 0.5px), oklch(0.50 0.03 255 / 0.11) 50%,' +
+ ' transparent 50%)',
+ // --- Tonal wash — a clean white highlight through the reading centre.
+ 'radial-gradient(ellipse 95% 80% at 50% 40%, oklch(0.995 0.003 250 / 0.60) 0%, transparent 62%)',
+ // --- Vignette — settle the corners into a faint cool grey.
+ 'radial-gradient(ellipse 125% 130% at 50% 45%, transparent 58%, oklch(0.90 0.012 252 / 0.42) 100%)',
+ ].join(','),
+ backgroundSize: '48px 83px, 48px 83px, 48px 83px, 48px 83px, 48px 83px, 100% 100%, 100% 100%',
+ backgroundPosition: '0 0, 24px 0, 0 0, 0 0, 24px 0, 0 0, 0 0',
+};
+
+export const triangles: ChatBgVariants = { dark, light };
diff --git a/src/app/features/lotus/backgrounds/types.ts b/src/app/features/lotus/backgrounds/types.ts
new file mode 100644
index 000000000..655990bfd
--- /dev/null
+++ b/src/app/features/lotus/backgrounds/types.ts
@@ -0,0 +1,13 @@
+import { CSSProperties } from 'react';
+
+// A chat background provides an independently-tuned CSSProperties per app theme:
+// the `dark` variant is a subtle light-ish pattern on a dark base, the `light`
+// variant a subtle dark-ish pattern on a light base. Each sits DIRECTLY behind
+// the chat message list, so both must stay subtle enough to preserve WCAG-AA
+// text legibility. Animated backgrounds include an `animation`; getChatBg strips
+// it for prefers-reduced-motion / pause-animations, so the remaining properties
+// must already read as a finished static background on their own.
+export type ChatBgVariants = {
+ dark: CSSProperties;
+ light: CSSProperties;
+};
diff --git a/src/app/features/lotus/backgrounds/waves.ts b/src/app/features/lotus/backgrounds/waves.ts
new file mode 100644
index 000000000..6a02c04fa
--- /dev/null
+++ b/src/app/features/lotus/backgrounds/waves.ts
@@ -0,0 +1,68 @@
+import { CSSProperties } from 'react';
+import { ChatBgVariants } from './types';
+
+// waves — a serene, rhythmic ocean swell / sound-wave contour.
+//
+// The motif is three stacked sine contours — layered swell at slightly varied
+// amplitude, weight and opacity — floating over a soft vertical depth wash so
+// the field reads like gentle water or sculpted sand. It is tuned to be *felt,
+// not read*: every stroke sits well under legibility thresholds so crisp
+// message text stays comfortably WCAG-AA in both themes.
+//
+// TRUE SINE CURVES VIA INLINE SVG
+// Gradients can't draw a real sine, so each wave is a polyline sampling of
+// y = yc - amp*sin(2*pi*N*x/W), rendered as an inline SVG data-URI (fully
+// URL-encoded, so it is CSP/Tauri-safe and needs no external asset). oklch()
+// stroke colors give perceptually even, low-chroma lines.
+//
+// SEAMLESS TILING
+// The SVG tile is 240x120 with EXACTLY N=2 whole periods across its 240px
+// width, so the first and last sample of every wave share the same y — the
+// horizontal repeat has no seam. All three contours live within y = 24..106,
+// clear of the 0/120 tile edges, so the vertical repeat is seam-free too. To
+// avoid a rigid stacked look, the same tile is layered a second time shifted by
+// half a tile (120px x, 60px y) at lower opacity, weaving the rows into a
+// continuous drifting swell. backgroundSize = 240px 120px keeps the SVG at its
+// authored scale; the depth wash is a single 100% gradient sized to match.
+
+const waveTileDark =
+ 'url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22240%22%20height%3D%22120%22%20viewBox%3D%220%200%20240%20120%22%3E%3Cg%20fill%3D%22none%22%20stroke-linecap%3D%22round%22%3E%3Cpath%20d%3D%22M0%2034%20L5%2031.41%20L10%2029%20L15%2026.93%20L20%2025.34%20L25%2024.34%20L30%2024%20L35%2024.34%20L40%2025.34%20L45%2026.93%20L50%2029%20L55%2031.41%20L60%2034%20L65%2036.59%20L70%2039%20L75%2041.07%20L80%2042.66%20L85%2043.66%20L90%2044%20L95%2043.66%20L100%2042.66%20L105%2041.07%20L110%2039%20L115%2036.59%20L120%2034%20L125%2031.41%20L130%2029%20L135%2026.93%20L140%2025.34%20L145%2024.34%20L150%2024%20L155%2024.34%20L160%2025.34%20L165%2026.93%20L170%2029%20L175%2031.41%20L180%2034%20L185%2036.59%20L190%2039%20L195%2041.07%20L200%2042.66%20L205%2043.66%20L210%2044%20L215%2043.66%20L220%2042.66%20L225%2041.07%20L230%2039%20L235%2036.59%20L240%2034%22%20stroke%3D%22oklch(0.65%200.08%20200%20%2F%200.16)%22%20stroke-width%3D%221.5%22%2F%3E%3Cpath%20d%3D%22M0%2064%20L5%2062.19%20L10%2060.5%20L15%2059.05%20L20%2057.94%20L25%2057.24%20L30%2057%20L35%2057.24%20L40%2057.94%20L45%2059.05%20L50%2060.5%20L55%2062.19%20L60%2064%20L65%2065.81%20L70%2067.5%20L75%2068.95%20L80%2070.06%20L85%2070.76%20L90%2071%20L95%2070.76%20L100%2070.06%20L105%2068.95%20L110%2067.5%20L115%2065.81%20L120%2064%20L125%2062.19%20L130%2060.5%20L135%2059.05%20L140%2057.94%20L145%2057.24%20L150%2057%20L155%2057.24%20L160%2057.94%20L165%2059.05%20L170%2060.5%20L175%2062.19%20L180%2064%20L185%2065.81%20L190%2067.5%20L195%2068.95%20L200%2070.06%20L205%2070.76%20L210%2071%20L215%2070.76%20L220%2070.06%20L225%2068.95%20L230%2067.5%20L235%2065.81%20L240%2064%22%20stroke%3D%22oklch(0.68%200.07%20195%20%2F%200.11)%22%20stroke-width%3D%221.2%22%2F%3E%3Cpath%20d%3D%22M0%2094%20L5%2090.89%20L10%2088%20L15%2085.51%20L20%2083.61%20L25%2082.41%20L30%2082%20L35%2082.41%20L40%2083.61%20L45%2085.51%20L50%2088%20L55%2090.89%20L60%2094%20L65%2097.11%20L70%20100%20L75%20102.49%20L80%20104.39%20L85%20105.59%20L90%20106%20L95%20105.59%20L100%20104.39%20L105%20102.49%20L110%20100%20L115%2097.11%20L120%2094%20L125%2090.89%20L130%2088%20L135%2085.51%20L140%2083.61%20L145%2082.41%20L150%2082%20L155%2082.41%20L160%2083.61%20L165%2085.51%20L170%2088%20L175%2090.89%20L180%2094%20L185%2097.11%20L190%20100%20L195%20102.49%20L200%20104.39%20L205%20105.59%20L210%20106%20L215%20105.59%20L220%20104.39%20L225%20102.49%20L230%20100%20L235%2097.11%20L240%2094%22%20stroke%3D%22oklch(0.62%200.075%20205%20%2F%200.14)%22%20stroke-width%3D%221.6%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E")';
+
+const waveTileLight =
+ 'url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22240%22%20height%3D%22120%22%20viewBox%3D%220%200%20240%20120%22%3E%3Cg%20fill%3D%22none%22%20stroke-linecap%3D%22round%22%3E%3Cpath%20d%3D%22M0%2034%20L5%2031.41%20L10%2029%20L15%2026.93%20L20%2025.34%20L25%2024.34%20L30%2024%20L35%2024.34%20L40%2025.34%20L45%2026.93%20L50%2029%20L55%2031.41%20L60%2034%20L65%2036.59%20L70%2039%20L75%2041.07%20L80%2042.66%20L85%2043.66%20L90%2044%20L95%2043.66%20L100%2042.66%20L105%2041.07%20L110%2039%20L115%2036.59%20L120%2034%20L125%2031.41%20L130%2029%20L135%2026.93%20L140%2025.34%20L145%2024.34%20L150%2024%20L155%2024.34%20L160%2025.34%20L165%2026.93%20L170%2029%20L175%2031.41%20L180%2034%20L185%2036.59%20L190%2039%20L195%2041.07%20L200%2042.66%20L205%2043.66%20L210%2044%20L215%2043.66%20L220%2042.66%20L225%2041.07%20L230%2039%20L235%2036.59%20L240%2034%22%20stroke%3D%22oklch(0.62%200.045%20235%20%2F%200.16)%22%20stroke-width%3D%221.5%22%2F%3E%3Cpath%20d%3D%22M0%2064%20L5%2062.19%20L10%2060.5%20L15%2059.05%20L20%2057.94%20L25%2057.24%20L30%2057%20L35%2057.24%20L40%2057.94%20L45%2059.05%20L50%2060.5%20L55%2062.19%20L60%2064%20L65%2065.81%20L70%2067.5%20L75%2068.95%20L80%2070.06%20L85%2070.76%20L90%2071%20L95%2070.76%20L100%2070.06%20L105%2068.95%20L110%2067.5%20L115%2065.81%20L120%2064%20L125%2062.19%20L130%2060.5%20L135%2059.05%20L140%2057.94%20L145%2057.24%20L150%2057%20L155%2057.24%20L160%2057.94%20L165%2059.05%20L170%2060.5%20L175%2062.19%20L180%2064%20L185%2065.81%20L190%2067.5%20L195%2068.95%20L200%2070.06%20L205%2070.76%20L210%2071%20L215%2070.76%20L220%2070.06%20L225%2068.95%20L230%2067.5%20L235%2065.81%20L240%2064%22%20stroke%3D%22oklch(0.66%200.04%20240%20%2F%200.11)%22%20stroke-width%3D%221.2%22%2F%3E%3Cpath%20d%3D%22M0%2094%20L5%2090.89%20L10%2088%20L15%2085.51%20L20%2083.61%20L25%2082.41%20L30%2082%20L35%2082.41%20L40%2083.61%20L45%2085.51%20L50%2088%20L55%2090.89%20L60%2094%20L65%2097.11%20L70%20100%20L75%20102.49%20L80%20104.39%20L85%20105.59%20L90%20106%20L95%20105.59%20L100%20104.39%20L105%20102.49%20L110%20100%20L115%2097.11%20L120%2094%20L125%2090.89%20L130%2088%20L135%2085.51%20L140%2083.61%20L145%2082.41%20L150%2082%20L155%2082.41%20L160%2083.61%20L165%2085.51%20L170%2088%20L175%2090.89%20L180%2094%20L185%2097.11%20L190%20100%20L195%20102.49%20L200%20104.39%20L205%20105.59%20L210%20106%20L215%20105.59%20L220%20104.39%20L225%20102.49%20L230%20100%20L235%2097.11%20L240%2094%22%20stroke%3D%22oklch(0.60%200.05%20230%20%2F%200.14)%22%20stroke-width%3D%221.6%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E")';
+
+const dark: CSSProperties = {
+ // Deep ink-blue base — the "water" the swell floats on.
+ backgroundColor: 'oklch(0.19 0.03 245)',
+ backgroundImage: [
+ // Primary swell — teal/aqua sine contours.
+ waveTileDark,
+ // Offset echo — same tile shifted half a period, dimmed, to weave rows.
+ waveTileDark,
+ // Depth wash — subtle lift toward the top, sink toward the bottom.
+ 'linear-gradient(180deg, oklch(0.24 0.04 240 / 0.5) 0%, oklch(0.16 0.025 250 / 0.5) 100%)',
+ ].join(','),
+ backgroundSize: '240px 120px, 240px 120px, 100% 100%',
+ backgroundPosition: '0 0, 120px 60px, 0 0',
+ // Dim the offset echo layer relative to the primary swell.
+ backgroundBlendMode: 'normal, soft-light, normal',
+};
+
+const light: CSSProperties = {
+ // Soft warm white base — like sunlit paper or pale sand.
+ backgroundColor: 'oklch(0.975 0.006 240)',
+ backgroundImage: [
+ // Primary swell — pale blue-grey sine contours.
+ waveTileLight,
+ // Offset echo — same tile shifted half a period, dimmed, to weave rows.
+ waveTileLight,
+ // Depth wash — faint cool tint feathering toward the bottom for calm depth.
+ 'linear-gradient(180deg, oklch(0.99 0.004 240 / 0.5) 0%, oklch(0.95 0.01 245 / 0.5) 100%)',
+ ].join(','),
+ backgroundSize: '240px 120px, 240px 120px, 100% 100%',
+ backgroundPosition: '0 0, 120px 60px, 0 0',
+ // Dim the offset echo layer relative to the primary swell.
+ backgroundBlendMode: 'normal, multiply, normal',
+};
+
+export const waves: ChatBgVariants = { dark, light };
diff --git a/src/app/features/lotus/chatBackground.ts b/src/app/features/lotus/chatBackground.ts
index 3b9c42e17..d5b35e7c8 100644
--- a/src/app/features/lotus/chatBackground.ts
+++ b/src/app/features/lotus/chatBackground.ts
@@ -1,12 +1,24 @@
import { CSSProperties } from 'react';
import { ChatBackground } from '../../state/settings';
-import {
- animRainKeyframe,
- animStarsDriftKeyframe,
- animGridPulseKeyframe,
- animAuroraKeyframe,
- animFirefliesKeyframe,
-} from '../../styles/Animations.css';
+import { blueprint } from './backgrounds/blueprint';
+import { stars } from './backgrounds/stars';
+import { topographic } from './backgrounds/topographic';
+import { herringbone } from './backgrounds/herringbone';
+import { crosshatch } from './backgrounds/crosshatch';
+import { chevron } from './backgrounds/chevron';
+import { polka } from './backgrounds/polka';
+import { triangles } from './backgrounds/triangles';
+import { plaid } from './backgrounds/plaid';
+import { tactical } from './backgrounds/tactical';
+import { circuit } from './backgrounds/circuit';
+import { hexgrid } from './backgrounds/hexgrid';
+import { waves } from './backgrounds/waves';
+import { neon } from './backgrounds/neon';
+import { animRain } from './backgrounds/animRain';
+import { animStars } from './backgrounds/animStars';
+import { animPulse } from './backgrounds/animPulse';
+import { animAurora } from './backgrounds/animAurora';
+import { animFireflies } from './backgrounds/animFireflies';
export const BG_OPTIONS: { value: ChatBackground; label: string }[] = [
{ value: 'none', label: 'None' },
@@ -33,20 +45,14 @@ export const BG_OPTIONS: { value: ChatBackground; label: string }[] = [
{ value: 'anim-fireflies', label: 'Fireflies' },
];
+// `none`, `carbon` and `aurora` stay inline: carbon + aurora are the kept user
+// favorites, none is the empty layer. Every other background is a premium
+// per-pattern module under ./backgrounds/ (each exposes a `dark` + `light`
+// variant). Keeping the whole record here lets getChatBg stay the single entry
+// point and preserves the Record exhaustiveness check.
const DARK: Record = {
none: {},
- blueprint: {
- backgroundColor: '#0a1628',
- backgroundImage: [
- 'linear-gradient(rgba(100,149,237,0.14) 1px, transparent 1px)',
- 'linear-gradient(90deg, rgba(100,149,237,0.14) 1px, transparent 1px)',
- 'linear-gradient(rgba(100,149,237,0.05) 1px, transparent 1px)',
- 'linear-gradient(90deg, rgba(100,149,237,0.05) 1px, transparent 1px)',
- ].join(','),
- backgroundSize: '80px 80px, 80px 80px, 16px 16px, 16px 16px',
- },
-
carbon: {
backgroundColor: '#0e0e0e',
backgroundImage: [
@@ -55,138 +61,6 @@ const DARK: Record = {
].join(','),
backgroundSize: '8px 8px',
},
-
- stars: {
- backgroundColor: '#050510',
- backgroundImage: [
- 'radial-gradient(circle, rgba(255,255,255,0.85) 1px, transparent 1px)',
- 'radial-gradient(circle, rgba(255,255,255,0.55) 1px, transparent 1px)',
- 'radial-gradient(circle, rgba(200,200,255,0.3) 1px, transparent 1px)',
- ].join(','),
- backgroundSize: '130px 130px, 190px 190px, 260px 260px',
- backgroundPosition: '0 0, 65px 32px, 32px 97px',
- },
-
- topographic: {
- backgroundColor: '#0f0f17',
- backgroundImage: [
- 'repeating-radial-gradient(circle at 20% 20%, transparent 0, transparent 30px, rgba(152,0,0,0.07) 31px, transparent 32px)',
- 'repeating-radial-gradient(circle at 80% 80%, transparent 0, transparent 25px, rgba(100,100,200,0.06) 26px, transparent 27px)',
- 'repeating-radial-gradient(circle at 50% 10%, transparent 0, transparent 45px, rgba(152,0,0,0.04) 46px, transparent 47px)',
- ].join(','),
- },
-
- herringbone: {
- backgroundColor: '#111118',
- backgroundImage: [
- 'repeating-linear-gradient(60deg, rgba(180,160,210,0.08) 0, rgba(180,160,210,0.08) 1px, transparent 0, transparent 50%)',
- 'repeating-linear-gradient(120deg, rgba(180,160,210,0.08) 0, rgba(180,160,210,0.08) 1px, transparent 0, transparent 50%)',
- ].join(','),
- backgroundSize: '20px 36px',
- },
-
- crosshatch: {
- backgroundColor: '#0f0f0f',
- backgroundImage: [
- 'linear-gradient(rgba(255,255,255,0.06) 1px, transparent 1px)',
- 'linear-gradient(90deg, rgba(255,255,255,0.06) 1px, transparent 1px)',
- 'linear-gradient(rgba(255,255,255,0.022) 1px, transparent 1px)',
- 'linear-gradient(90deg, rgba(255,255,255,0.022) 1px, transparent 1px)',
- ].join(','),
- backgroundSize: '60px 60px, 60px 60px, 12px 12px, 12px 12px',
- },
-
- // Interlocking zigzag stripes
- chevron: {
- backgroundColor: '#0f0f17',
- backgroundImage: [
- 'linear-gradient(135deg, rgba(180,160,210,0.1) 25%, transparent 25%)',
- 'linear-gradient(225deg, rgba(180,160,210,0.1) 25%, transparent 25%)',
- 'linear-gradient(315deg, rgba(180,160,210,0.1) 25%, transparent 25%)',
- 'linear-gradient(45deg, rgba(180,160,210,0.1) 25%, transparent 25%)',
- ].join(','),
- backgroundSize: '20px 20px',
- },
-
- // Even dot grid
- polka: {
- backgroundColor: '#0e0e14',
- backgroundImage: 'radial-gradient(circle, rgba(255,255,255,0.2) 2px, transparent 2px)',
- backgroundSize: '28px 28px',
- },
-
- // Isometric triangle grid
- triangles: {
- backgroundColor: '#111118',
- backgroundImage: [
- 'linear-gradient(60deg, rgba(100,149,237,0.09) 25%, transparent 25%, transparent 75%, rgba(100,149,237,0.09) 75%)',
- 'linear-gradient(120deg, rgba(100,149,237,0.09) 25%, transparent 25%, transparent 75%, rgba(100,149,237,0.09) 75%)',
- ].join(','),
- backgroundSize: '40px 70px',
- backgroundPosition: '0 0, 20px 35px',
- },
-
- // Tartan-inspired crossing lines with accent colour
- plaid: {
- backgroundColor: '#0a1020',
- backgroundImage: [
- 'repeating-linear-gradient(0deg, transparent, transparent 39px, rgba(100,149,237,0.13) 39px, rgba(100,149,237,0.13) 40px)',
- 'repeating-linear-gradient(90deg, transparent, transparent 39px, rgba(100,149,237,0.13) 39px, rgba(100,149,237,0.13) 40px)',
- 'repeating-linear-gradient(0deg, transparent, transparent 7px, rgba(152,0,0,0.08) 7px, rgba(152,0,0,0.08) 8px)',
- 'repeating-linear-gradient(90deg, transparent, transparent 7px, rgba(152,0,0,0.08) 7px, rgba(152,0,0,0.08) 8px)',
- ].join(','),
- },
-
- // LotusGuild TDS exact dot-grid
- tactical: {
- backgroundColor: '#030508',
- backgroundImage: 'radial-gradient(circle, rgba(0,212,255,0.055) 1px, transparent 1px)',
- backgroundSize: '28px 28px',
- },
-
- // Circuit board — green grid with node dots
- circuit: {
- backgroundColor: '#040a04',
- backgroundImage: [
- 'linear-gradient(rgba(0,255,136,0.045) 1px, transparent 1px)',
- 'linear-gradient(90deg, rgba(0,255,136,0.045) 1px, transparent 1px)',
- 'radial-gradient(circle, rgba(0,255,136,0.20) 1.5px, transparent 1.5px)',
- ].join(','),
- backgroundSize: '40px 40px, 40px 40px, 40px 40px',
- backgroundPosition: '0 0, 0 0, 20px 20px',
- },
-
- // True pointy-top hexagonal grid via SVG data URI
- hexgrid: {
- backgroundColor: '#060c14',
- backgroundImage:
- 'url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2229%22%20height%3D%2250%22%3E%3Cpath%20d%3D%22M14.5%200L29%208L29%2025L14.5%2033L0%2025L0%208Z%20M14.5%2033L29%2041V50%20M14.5%2033L0%2041V50%22%20fill%3D%22none%22%20stroke%3D%22rgba%280%2C212%2C255%2C0.13%29%22%20stroke-width%3D%220.8%22/%3E%3C/svg%3E")',
- backgroundSize: '29px 50px',
- },
-
- // Flowing sine-wave lines
- waves: {
- backgroundColor: '#080c18',
- backgroundImage: [
- 'repeating-radial-gradient(ellipse at 0% 50%, transparent 0, transparent 18px, rgba(80,130,255,0.07) 19px, transparent 20px)',
- 'repeating-radial-gradient(ellipse at 100% 50%, transparent 0, transparent 28px, rgba(80,130,255,0.05) 29px, transparent 30px)',
- 'repeating-radial-gradient(ellipse at 50% 0%, transparent 0, transparent 22px, rgba(100,60,200,0.06) 23px, transparent 24px)',
- ].join(','),
- },
-
- // Neon cyberpunk grid — orange/cyan TDS colors
- neon: {
- backgroundColor: '#020408',
- backgroundImage: [
- 'linear-gradient(rgba(255,107,0,0.10) 1px, transparent 1px)',
- 'linear-gradient(90deg, rgba(255,107,0,0.10) 1px, transparent 1px)',
- 'linear-gradient(rgba(0,212,255,0.05) 1px, transparent 1px)',
- 'linear-gradient(90deg, rgba(0,212,255,0.05) 1px, transparent 1px)',
- ].join(','),
- backgroundSize: '60px 60px, 60px 60px, 12px 12px, 12px 12px',
- },
-
- // Aurora borealis — flowing gradient bands
aurora: {
backgroundColor: '#030810',
backgroundImage: [
@@ -197,86 +71,30 @@ const DARK: Record = {
].join(','),
},
- // Animated: Matrix digital rain — scrolling stripe columns + phosphor glow flicker
- 'anim-rain': {
- backgroundColor: '#010804',
- backgroundImage: [
- 'repeating-linear-gradient(180deg, rgba(0,255,136,0.16) 0px, rgba(0,255,136,0.16) 1px, transparent 1px, transparent 20px)',
- 'repeating-linear-gradient(180deg, rgba(0,255,136,0.07) 0px, rgba(0,255,136,0.07) 1px, transparent 1px, transparent 8px)',
- ].join(','),
- backgroundSize: '40px 200px, 12px 200px',
- backgroundPosition: '0 0, 0 0',
- animation: `${animRainKeyframe} 8s linear infinite`,
- },
-
- // Animated: drifting star field — three seamlessly-tiling layers at different speeds
- 'anim-stars': {
- backgroundColor: '#050510',
- backgroundImage: [
- 'radial-gradient(circle, rgba(255,255,255,0.85) 1px, transparent 1px)',
- 'radial-gradient(circle, rgba(200,220,255,0.55) 1px, transparent 1px)',
- 'radial-gradient(circle, rgba(180,200,255,0.3) 1px, transparent 1px)',
- ].join(','),
- backgroundSize: '130px 130px, 190px 190px, 260px 260px',
- backgroundPosition: '0 0, 65px 32px, 32px 97px',
- animation: `${animStarsDriftKeyframe} 30s linear infinite`,
- },
-
- // Animated: neon grid pulse — size breathe + independent brightness oscillation
- 'anim-pulse': {
- backgroundColor: '#030508',
- backgroundImage: [
- 'linear-gradient(rgba(255,107,0,0.12) 1px, transparent 1px)',
- 'linear-gradient(90deg, rgba(255,107,0,0.12) 1px, transparent 1px)',
- 'linear-gradient(rgba(0,212,255,0.06) 1px, transparent 1px)',
- 'linear-gradient(90deg, rgba(0,212,255,0.06) 1px, transparent 1px)',
- ].join(','),
- backgroundSize: '60px 60px, 60px 60px, 12px 12px, 12px 12px',
- animation: `${animGridPulseKeyframe} 4s ease-in-out infinite`,
- },
-
- // Animated: aurora borealis — four bands each travel an independent path
- 'anim-aurora': {
- backgroundColor: '#020a10',
- backgroundImage: [
- 'radial-gradient(ellipse 80% 60% at 20% 30%, rgba(0,255,136,0.12) 0%, transparent 70%)',
- 'radial-gradient(ellipse 70% 50% at 80% 70%, rgba(0,100,255,0.12) 0%, transparent 70%)',
- 'radial-gradient(ellipse 90% 70% at 50% 10%, rgba(191,95,255,0.09) 0%, transparent 65%)',
- 'radial-gradient(ellipse 75% 55% at 60% 90%, rgba(0,212,255,0.09) 0%, transparent 65%)',
- ].join(','),
- backgroundSize: '200% 200%, 250% 250%, 300% 300%, 220% 220%',
- backgroundPosition: '0% 0%, 100% 0%, 50% 100%, 0% 50%',
- animation: `${animAuroraKeyframe} 28s ease-in-out infinite`,
- },
-
- // Animated: fireflies — drift + brightness glow + opacity blink at prime periods
- 'anim-fireflies': {
- backgroundColor: '#030508',
- backgroundImage: [
- 'radial-gradient(circle, rgba(255,220,50,0.7) 1.5px, rgba(255,160,0,0.18) 3px, transparent 4px)',
- 'radial-gradient(circle, rgba(255,200,30,0.55) 1px, rgba(255,140,0,0.14) 2.5px, transparent 3.5px)',
- 'radial-gradient(circle, rgba(255,240,100,0.4) 1px, transparent 2px)',
- ].join(','),
- backgroundSize: '200px 200px, 280px 280px, 160px 160px',
- backgroundPosition: '0 0, 120px 80px, 60px 140px',
- animation: `${animFirefliesKeyframe} 30s linear infinite`,
- },
+ blueprint: blueprint.dark,
+ stars: stars.dark,
+ topographic: topographic.dark,
+ herringbone: herringbone.dark,
+ crosshatch: crosshatch.dark,
+ chevron: chevron.dark,
+ polka: polka.dark,
+ triangles: triangles.dark,
+ plaid: plaid.dark,
+ tactical: tactical.dark,
+ circuit: circuit.dark,
+ hexgrid: hexgrid.dark,
+ waves: waves.dark,
+ neon: neon.dark,
+ 'anim-rain': animRain.dark,
+ 'anim-stars': animStars.dark,
+ 'anim-pulse': animPulse.dark,
+ 'anim-aurora': animAurora.dark,
+ 'anim-fireflies': animFireflies.dark,
};
const LIGHT: Record = {
none: {},
- blueprint: {
- backgroundColor: '#eef3ff',
- backgroundImage: [
- 'linear-gradient(rgba(50,100,220,0.16) 1px, transparent 1px)',
- 'linear-gradient(90deg, rgba(50,100,220,0.16) 1px, transparent 1px)',
- 'linear-gradient(rgba(50,100,220,0.06) 1px, transparent 1px)',
- 'linear-gradient(90deg, rgba(50,100,220,0.06) 1px, transparent 1px)',
- ].join(','),
- backgroundSize: '80px 80px, 80px 80px, 16px 16px, 16px 16px',
- },
-
carbon: {
backgroundColor: '#efefef',
backgroundImage: [
@@ -285,129 +103,6 @@ const LIGHT: Record = {
].join(','),
backgroundSize: '8px 8px',
},
-
- // Stars is intentionally always dark — it's a night-sky theme
- stars: {
- backgroundColor: '#050510',
- backgroundImage: [
- 'radial-gradient(circle, rgba(255,255,255,0.85) 1px, transparent 1px)',
- 'radial-gradient(circle, rgba(255,255,255,0.55) 1px, transparent 1px)',
- 'radial-gradient(circle, rgba(200,200,255,0.3) 1px, transparent 1px)',
- ].join(','),
- backgroundSize: '130px 130px, 190px 190px, 260px 260px',
- backgroundPosition: '0 0, 65px 32px, 32px 97px',
- },
-
- topographic: {
- backgroundColor: '#faf8f5',
- backgroundImage: [
- 'repeating-radial-gradient(circle at 20% 20%, transparent 0, transparent 30px, rgba(100,60,60,0.09) 31px, transparent 32px)',
- 'repeating-radial-gradient(circle at 80% 80%, transparent 0, transparent 25px, rgba(60,60,130,0.07) 26px, transparent 27px)',
- 'repeating-radial-gradient(circle at 50% 10%, transparent 0, transparent 45px, rgba(100,60,60,0.05) 46px, transparent 47px)',
- ].join(','),
- },
-
- herringbone: {
- backgroundColor: '#f9f9f9',
- backgroundImage: [
- 'repeating-linear-gradient(60deg, rgba(80,70,110,0.09) 0, rgba(80,70,110,0.09) 1px, transparent 0, transparent 50%)',
- 'repeating-linear-gradient(120deg, rgba(80,70,110,0.09) 0, rgba(80,70,110,0.09) 1px, transparent 0, transparent 50%)',
- ].join(','),
- backgroundSize: '20px 36px',
- },
-
- crosshatch: {
- backgroundColor: '#ffffff',
- backgroundImage: [
- 'linear-gradient(rgba(0,0,0,0.07) 1px, transparent 1px)',
- 'linear-gradient(90deg, rgba(0,0,0,0.07) 1px, transparent 1px)',
- 'linear-gradient(rgba(0,0,0,0.025) 1px, transparent 1px)',
- 'linear-gradient(90deg, rgba(0,0,0,0.025) 1px, transparent 1px)',
- ].join(','),
- backgroundSize: '60px 60px, 60px 60px, 12px 12px, 12px 12px',
- },
-
- chevron: {
- backgroundColor: '#f9f8ff',
- backgroundImage: [
- 'linear-gradient(135deg, rgba(80,60,130,0.1) 25%, transparent 25%)',
- 'linear-gradient(225deg, rgba(80,60,130,0.1) 25%, transparent 25%)',
- 'linear-gradient(315deg, rgba(80,60,130,0.1) 25%, transparent 25%)',
- 'linear-gradient(45deg, rgba(80,60,130,0.1) 25%, transparent 25%)',
- ].join(','),
- backgroundSize: '20px 20px',
- },
-
- polka: {
- backgroundColor: '#fafafa',
- backgroundImage: 'radial-gradient(circle, rgba(0,0,0,0.18) 2px, transparent 2px)',
- backgroundSize: '28px 28px',
- },
-
- triangles: {
- backgroundColor: '#f4f7ff',
- backgroundImage: [
- 'linear-gradient(60deg, rgba(50,100,220,0.1) 25%, transparent 25%, transparent 75%, rgba(50,100,220,0.1) 75%)',
- 'linear-gradient(120deg, rgba(50,100,220,0.1) 25%, transparent 25%, transparent 75%, rgba(50,100,220,0.1) 75%)',
- ].join(','),
- backgroundSize: '40px 70px',
- backgroundPosition: '0 0, 20px 35px',
- },
-
- plaid: {
- backgroundColor: '#f5f0ff',
- backgroundImage: [
- 'repeating-linear-gradient(0deg, transparent, transparent 39px, rgba(100,50,180,0.15) 39px, rgba(100,50,180,0.15) 40px)',
- 'repeating-linear-gradient(90deg, transparent, transparent 39px, rgba(100,50,180,0.15) 39px, rgba(100,50,180,0.15) 40px)',
- 'repeating-linear-gradient(0deg, transparent, transparent 7px, rgba(200,0,0,0.09) 7px, rgba(200,0,0,0.09) 8px)',
- 'repeating-linear-gradient(90deg, transparent, transparent 7px, rgba(200,0,0,0.09) 7px, rgba(200,0,0,0.09) 8px)',
- ].join(','),
- },
-
- tactical: {
- backgroundColor: '#f0f4fa',
- backgroundImage: 'radial-gradient(circle, rgba(0,100,200,0.08) 1px, transparent 1px)',
- backgroundSize: '28px 28px',
- },
-
- circuit: {
- backgroundColor: '#f0f8f0',
- backgroundImage: [
- 'linear-gradient(rgba(0,160,80,0.06) 1px, transparent 1px)',
- 'linear-gradient(90deg, rgba(0,160,80,0.06) 1px, transparent 1px)',
- 'radial-gradient(circle, rgba(0,160,80,0.22) 1.5px, transparent 1.5px)',
- ].join(','),
- backgroundSize: '40px 40px, 40px 40px, 40px 40px',
- backgroundPosition: '0 0, 0 0, 20px 20px',
- },
-
- hexgrid: {
- backgroundColor: '#f4f8ff',
- backgroundImage:
- 'url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2229%22%20height%3D%2250%22%3E%3Cpath%20d%3D%22M14.5%200L29%208L29%2025L14.5%2033L0%2025L0%208Z%20M14.5%2033L29%2041V50%20M14.5%2033L0%2041V50%22%20fill%3D%22none%22%20stroke%3D%22rgba%2850%2C100%2C220%2C0.11%29%22%20stroke-width%3D%220.8%22/%3E%3C/svg%3E")',
- backgroundSize: '29px 50px',
- },
-
- waves: {
- backgroundColor: '#eef3ff',
- backgroundImage: [
- 'repeating-radial-gradient(ellipse at 0% 50%, transparent 0, transparent 18px, rgba(50,100,220,0.09) 19px, transparent 20px)',
- 'repeating-radial-gradient(ellipse at 100% 50%, transparent 0, transparent 28px, rgba(50,100,220,0.07) 29px, transparent 30px)',
- 'repeating-radial-gradient(ellipse at 50% 0%, transparent 0, transparent 22px, rgba(80,40,180,0.07) 23px, transparent 24px)',
- ].join(','),
- },
-
- neon: {
- backgroundColor: '#fafafa',
- backgroundImage: [
- 'linear-gradient(rgba(196,78,0,0.12) 1px, transparent 1px)',
- 'linear-gradient(90deg, rgba(196,78,0,0.12) 1px, transparent 1px)',
- 'linear-gradient(rgba(0,98,184,0.06) 1px, transparent 1px)',
- 'linear-gradient(90deg, rgba(0,98,184,0.06) 1px, transparent 1px)',
- ].join(','),
- backgroundSize: '60px 60px, 60px 60px, 12px 12px, 12px 12px',
- },
-
aurora: {
backgroundColor: '#f4faf8',
backgroundImage: [
@@ -418,67 +113,25 @@ const LIGHT: Record = {
].join(','),
},
- // Animated light variants
-
- 'anim-rain': {
- backgroundColor: '#f0fff4',
- backgroundImage: [
- 'repeating-linear-gradient(180deg, rgba(0,160,80,0.16) 0px, rgba(0,160,80,0.16) 1px, transparent 1px, transparent 20px)',
- 'repeating-linear-gradient(180deg, rgba(0,160,80,0.07) 0px, rgba(0,160,80,0.07) 1px, transparent 1px, transparent 8px)',
- ].join(','),
- backgroundSize: '40px 200px, 12px 200px',
- backgroundPosition: '0 0, 0 0',
- animation: `${animRainKeyframe} 8s linear infinite`,
- },
-
- 'anim-stars': {
- backgroundColor: '#f5f5ff',
- backgroundImage: [
- 'radial-gradient(circle, rgba(60,60,160,0.50) 1px, transparent 1px)',
- 'radial-gradient(circle, rgba(80,80,180,0.35) 1px, transparent 1px)',
- 'radial-gradient(circle, rgba(100,100,200,0.20) 1px, transparent 1px)',
- ].join(','),
- backgroundSize: '130px 130px, 190px 190px, 260px 260px',
- backgroundPosition: '0 0, 65px 32px, 32px 97px',
- animation: `${animStarsDriftKeyframe} 30s linear infinite`,
- },
-
- 'anim-pulse': {
- backgroundColor: '#ffffff',
- backgroundImage: [
- 'linear-gradient(rgba(0,98,184,0.14) 1px, transparent 1px)',
- 'linear-gradient(90deg, rgba(0,98,184,0.14) 1px, transparent 1px)',
- 'linear-gradient(rgba(0,98,184,0.06) 1px, transparent 1px)',
- 'linear-gradient(90deg, rgba(0,98,184,0.06) 1px, transparent 1px)',
- ].join(','),
- backgroundSize: '60px 60px, 60px 60px, 12px 12px, 12px 12px',
- animation: `${animGridPulseKeyframe} 4s ease-in-out infinite`,
- },
-
- 'anim-aurora': {
- backgroundColor: '#f0f8f4',
- backgroundImage: [
- 'radial-gradient(ellipse 80% 60% at 20% 30%, rgba(0,160,80,0.13) 0%, transparent 70%)',
- 'radial-gradient(ellipse 70% 50% at 80% 70%, rgba(0,80,200,0.13) 0%, transparent 70%)',
- 'radial-gradient(ellipse 90% 70% at 50% 10%, rgba(140,60,220,0.10) 0%, transparent 65%)',
- 'radial-gradient(ellipse 75% 55% at 60% 90%, rgba(0,160,200,0.10) 0%, transparent 65%)',
- ].join(','),
- backgroundSize: '200% 200%, 250% 250%, 300% 300%, 220% 220%',
- backgroundPosition: '0% 0%, 100% 0%, 50% 100%, 0% 50%',
- animation: `${animAuroraKeyframe} 28s ease-in-out infinite`,
- },
-
- 'anim-fireflies': {
- backgroundColor: '#fffdf0',
- backgroundImage: [
- 'radial-gradient(circle, rgba(180,120,0,0.70) 1.5px, rgba(160,90,0,0.18) 3px, transparent 4px)',
- 'radial-gradient(circle, rgba(160,100,0,0.55) 1px, rgba(140,80,0,0.14) 2.5px, transparent 3.5px)',
- 'radial-gradient(circle, rgba(200,140,0,0.40) 1px, transparent 2px)',
- ].join(','),
- backgroundSize: '200px 200px, 280px 280px, 160px 160px',
- backgroundPosition: '0 0, 120px 80px, 60px 140px',
- animation: `${animFirefliesKeyframe} 30s linear infinite`,
- },
+ blueprint: blueprint.light,
+ stars: stars.light,
+ topographic: topographic.light,
+ herringbone: herringbone.light,
+ crosshatch: crosshatch.light,
+ chevron: chevron.light,
+ polka: polka.light,
+ triangles: triangles.light,
+ plaid: plaid.light,
+ tactical: tactical.light,
+ circuit: circuit.light,
+ hexgrid: hexgrid.light,
+ waves: waves.light,
+ neon: neon.light,
+ 'anim-rain': animRain.light,
+ 'anim-stars': animStars.light,
+ 'anim-pulse': animPulse.light,
+ 'anim-aurora': animAurora.light,
+ 'anim-fireflies': animFireflies.light,
};
export const getChatBg = (