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 = `${cols}`; + 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 = (