feat(chat-bg): redesign 19 chat backgrounds as modular per-pattern files

Same treatment as the seasonal themes: split the 502-line chatBackground.ts
Record into one premium module per background under lotus/backgrounds/ (each
exposes a tuned dark + light ChatBgVariants), one Opus agent per background
against a shared brief. chatBackground.ts now assembles DARK/LIGHT from the
modules; getChatBg is unchanged. Carbon + Aurora are kept inline as-is (user
favorites); none stays the empty layer.

Every redesign: layered oklch palettes, seamless tiling with worked-out tile
math (integer-multiple periods; edge-wrapping inline-SVG data-URIs for
circuit/hexgrid/waves/herringbone/chevron/tactical), independently-tuned
dark+light (not a recolor), and low "felt-not-read" opacity so chat text stays
WCAG-AA legible. The 5 animated backgrounds (rain, star drift, grid pulse,
aurora flow, fireflies) each colocate a vanilla-extract keyframe .css.ts,
animate only background-position for a jump-free loop, and — since getChatBg
strips animation for reduced-motion — render a finished static frame too.

Redesigned: blueprint, stars, topographic, herringbone, crosshatch, chevron,
polka, triangles, plaid, tactical, circuit, hexgrid, waves, neon, anim-rain,
anim-stars, anim-pulse, anim-aurora, anim-fireflies.

Gates: tsc clean, ESLint clean, Prettier clean, build OK, 551 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-30 20:23:54 -04:00
parent 26f998d243
commit 02b2ce8109
26 changed files with 2003 additions and 409 deletions
@@ -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%',
},
});
@@ -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.050.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 };
@@ -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',
},
});
@@ -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`,
},
};
@@ -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',
},
});
@@ -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`,
},
};
@@ -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' },
});
@@ -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 = `
<linearGradient id='${id}' x1='0' y1='${topY}' x2='0' y2='${headY}' gradientUnits='userSpaceOnUse'>
<stop offset='0' stop-color='${tail}' stop-opacity='0'/>
<stop offset='0.55' stop-color='${tail}'/>
<stop offset='1' stop-color='${head}'/>
</linearGradient>`;
// The streak body is a soft, slightly-blurred vertical bar.
const bar = `<rect x='${x - 3}' y='${topY}' width='6' height='${len}' rx='3' fill='url(#${id})'/>`;
// 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 `<rect x='${x - gw / 2}' y='${gy}' width='${gw}' height='1.4' rx='0.7' fill='${glyph}' fill-opacity='${op}'/>`;
})
.join('');
// The leading glyph: a brighter small square cap at the head.
const cap = `<rect x='${x - 2.5}' y='${headY - 3}' width='5' height='5' rx='1' fill='${head}'/>`;
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 = `<svg xmlns='http://www.w3.org/2000/svg' width='260' height='200' viewBox='0 0 260 200'><defs></defs>${cols}</svg>`;
return `url("data:image/svg+xml,${encodeURIComponent(svg)}")`;
};
export const animRain: ChatBgVariants = {
// Dark: phosphor-green streaks on deep near-black with a faint green cast.
dark: {
backgroundColor: 'oklch(0.16 0.02 150)',
backgroundImage: [
// 1) the falling streak columns (this is the panned layer)
tile(
'oklch(0.75 0.14 150 / 0.5)', // head — bright phosphor glyph
'oklch(0.68 0.12 150 / 0.28)', // tail — dim phosphor
'oklch(0.82 0.1 150 / 0.5)', // glyph ticks — brightest
),
// 2) soft top-down phosphor haze so the rain has atmosphere
'linear-gradient(180deg, oklch(0.24 0.04 150 / 0.55) 0%, transparent 40%)',
// 3) subtle green cast pooling toward the bottom
'radial-gradient(120% 90% at 50% 100%, oklch(0.28 0.05 150 / 0.45) 0%, transparent 60%)',
// 4) vignette — quiet the corners so the reading column stays clean
'radial-gradient(140% 140% at 50% 45%, transparent 60%, oklch(0.1 0.02 150 / 0.6) 100%)',
].join(','),
backgroundSize: ['260px 200px', '100% 100%', '100% 100%', '100% 100%'].join(','),
backgroundPosition: ['0 0', '0 0', '0 0', '0 0'].join(','),
animation: `${rainFall} 12s linear infinite`,
},
// Light: soft teal-grey streaks on a pale cool base — elegant, never neon.
light: {
backgroundColor: 'oklch(0.97 0.008 165)',
backgroundImage: [
tile(
'oklch(0.55 0.07 165 / 0.4)', // head — soft teal-grey drop
'oklch(0.62 0.05 165 / 0.22)', // tail — faint teal-grey
'oklch(0.5 0.06 165 / 0.42)', // glyph ticks
),
// gentle cool wash from the top
'linear-gradient(180deg, oklch(0.94 0.015 175 / 0.6) 0%, transparent 42%)',
// faint teal pooling at the bottom edge
'radial-gradient(120% 90% at 50% 100%, oklch(0.9 0.02 170 / 0.5) 0%, transparent 60%)',
// soft vignette in cool grey
'radial-gradient(140% 140% at 50% 45%, transparent 62%, oklch(0.88 0.02 165 / 0.5) 100%)',
].join(','),
backgroundSize: ['260px 200px', '100% 100%', '100% 100%', '100% 100%'].join(','),
backgroundPosition: ['0 0', '0 0', '0 0', '0 0'].join(','),
animation: `${rainFall} 12s linear infinite`,
},
};
@@ -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',
},
});
@@ -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`,
},
};
@@ -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.030.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.
},
};
@@ -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.030.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 };
@@ -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.050.5 on the accents, traces ~0.10.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 = `<svg xmlns='http://www.w3.org/2000/svg' width='120' height='120' viewBox='0 0 120 120'>
<g fill='none' stroke='${trace}' stroke-width='${traceW}' stroke-linecap='round' stroke-linejoin='round'>
<path d='M0 30 H26 V58 H60'/>
<path d='M60 58 V90 H120'/>
<path d='M0 90 H40 V120'/>
<path d='M40 0 V22 H88'/>
<path d='M88 0 V44 H104'/>
<path d='M104 44 V90 H120'/>
<path d='M88 120 V90'/>
<path d='M60 30 H120'/>
<path d='M60 30 V58'/>
<path d='M26 58 V90'/>
</g>
<g fill='none' stroke='${pad}' stroke-width='${traceW}'>
<circle cx='26' cy='58' r='3.4'/>
<circle cx='40' cy='90' r='3.4'/>
<circle cx='88' cy='22' r='3.4'/>
<circle cx='104' cy='44' r='3.4'/>
</g>
<g fill='${padFill}'>
<circle cx='26' cy='58' r='1.3'/>
<circle cx='40' cy='90' r='1.3'/>
<circle cx='88' cy='22' r='1.3'/>
<circle cx='104' cy='44' r='1.3'/>
</g>
<g fill='${junction}'>
<circle cx='60' cy='58' r='2'/>
<circle cx='60' cy='30' r='2'/>
<circle cx='104' cy='90' r='2'/>
</g>
<g fill='${via}'>
<circle cx='26' cy='58' r='7'/>
<circle cx='88' cy='22' r='7'/>
<circle cx='104' cy='44' r='7'/>
</g>
</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(','),
},
};
@@ -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.020.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 };
@@ -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',
},
};
@@ -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.140.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 };
+121
View File
@@ -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(','),
},
};
+135
View File
@@ -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 };
@@ -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.030.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 };
@@ -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(','),
},
};
@@ -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.030.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.
},
};
@@ -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 };
@@ -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 };
@@ -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;
};
@@ -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 };
+62 -409
View File
@@ -1,12 +1,24 @@
import { CSSProperties } from 'react'; import { CSSProperties } from 'react';
import { ChatBackground } from '../../state/settings'; import { ChatBackground } from '../../state/settings';
import { import { blueprint } from './backgrounds/blueprint';
animRainKeyframe, import { stars } from './backgrounds/stars';
animStarsDriftKeyframe, import { topographic } from './backgrounds/topographic';
animGridPulseKeyframe, import { herringbone } from './backgrounds/herringbone';
animAuroraKeyframe, import { crosshatch } from './backgrounds/crosshatch';
animFirefliesKeyframe, import { chevron } from './backgrounds/chevron';
} from '../../styles/Animations.css'; 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 }[] = [ export const BG_OPTIONS: { value: ChatBackground; label: string }[] = [
{ value: 'none', label: 'None' }, { value: 'none', label: 'None' },
@@ -33,20 +45,14 @@ export const BG_OPTIONS: { value: ChatBackground; label: string }[] = [
{ value: 'anim-fireflies', label: 'Fireflies' }, { 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<ChatBackground, ...> exhaustiveness check.
const DARK: Record<ChatBackground, CSSProperties> = { const DARK: Record<ChatBackground, CSSProperties> = {
none: {}, 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: { carbon: {
backgroundColor: '#0e0e0e', backgroundColor: '#0e0e0e',
backgroundImage: [ backgroundImage: [
@@ -55,138 +61,6 @@ const DARK: Record<ChatBackground, CSSProperties> = {
].join(','), ].join(','),
backgroundSize: '8px 8px', 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: { aurora: {
backgroundColor: '#030810', backgroundColor: '#030810',
backgroundImage: [ backgroundImage: [
@@ -197,86 +71,30 @@ const DARK: Record<ChatBackground, CSSProperties> = {
].join(','), ].join(','),
}, },
// Animated: Matrix digital rain — scrolling stripe columns + phosphor glow flicker blueprint: blueprint.dark,
'anim-rain': { stars: stars.dark,
backgroundColor: '#010804', topographic: topographic.dark,
backgroundImage: [ herringbone: herringbone.dark,
'repeating-linear-gradient(180deg, rgba(0,255,136,0.16) 0px, rgba(0,255,136,0.16) 1px, transparent 1px, transparent 20px)', crosshatch: crosshatch.dark,
'repeating-linear-gradient(180deg, rgba(0,255,136,0.07) 0px, rgba(0,255,136,0.07) 1px, transparent 1px, transparent 8px)', chevron: chevron.dark,
].join(','), polka: polka.dark,
backgroundSize: '40px 200px, 12px 200px', triangles: triangles.dark,
backgroundPosition: '0 0, 0 0', plaid: plaid.dark,
animation: `${animRainKeyframe} 8s linear infinite`, tactical: tactical.dark,
}, circuit: circuit.dark,
hexgrid: hexgrid.dark,
// Animated: drifting star field — three seamlessly-tiling layers at different speeds waves: waves.dark,
'anim-stars': { neon: neon.dark,
backgroundColor: '#050510', 'anim-rain': animRain.dark,
backgroundImage: [ 'anim-stars': animStars.dark,
'radial-gradient(circle, rgba(255,255,255,0.85) 1px, transparent 1px)', 'anim-pulse': animPulse.dark,
'radial-gradient(circle, rgba(200,220,255,0.55) 1px, transparent 1px)', 'anim-aurora': animAurora.dark,
'radial-gradient(circle, rgba(180,200,255,0.3) 1px, transparent 1px)', 'anim-fireflies': animFireflies.dark,
].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`,
},
}; };
const LIGHT: Record<ChatBackground, CSSProperties> = { const LIGHT: Record<ChatBackground, CSSProperties> = {
none: {}, 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: { carbon: {
backgroundColor: '#efefef', backgroundColor: '#efefef',
backgroundImage: [ backgroundImage: [
@@ -285,129 +103,6 @@ const LIGHT: Record<ChatBackground, CSSProperties> = {
].join(','), ].join(','),
backgroundSize: '8px 8px', 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: { aurora: {
backgroundColor: '#f4faf8', backgroundColor: '#f4faf8',
backgroundImage: [ backgroundImage: [
@@ -418,67 +113,25 @@ const LIGHT: Record<ChatBackground, CSSProperties> = {
].join(','), ].join(','),
}, },
// Animated light variants blueprint: blueprint.light,
stars: stars.light,
'anim-rain': { topographic: topographic.light,
backgroundColor: '#f0fff4', herringbone: herringbone.light,
backgroundImage: [ crosshatch: crosshatch.light,
'repeating-linear-gradient(180deg, rgba(0,160,80,0.16) 0px, rgba(0,160,80,0.16) 1px, transparent 1px, transparent 20px)', chevron: chevron.light,
'repeating-linear-gradient(180deg, rgba(0,160,80,0.07) 0px, rgba(0,160,80,0.07) 1px, transparent 1px, transparent 8px)', polka: polka.light,
].join(','), triangles: triangles.light,
backgroundSize: '40px 200px, 12px 200px', plaid: plaid.light,
backgroundPosition: '0 0, 0 0', tactical: tactical.light,
animation: `${animRainKeyframe} 8s linear infinite`, circuit: circuit.light,
}, hexgrid: hexgrid.light,
waves: waves.light,
'anim-stars': { neon: neon.light,
backgroundColor: '#f5f5ff', 'anim-rain': animRain.light,
backgroundImage: [ 'anim-stars': animStars.light,
'radial-gradient(circle, rgba(60,60,160,0.50) 1px, transparent 1px)', 'anim-pulse': animPulse.light,
'radial-gradient(circle, rgba(80,80,180,0.35) 1px, transparent 1px)', 'anim-aurora': animAurora.light,
'radial-gradient(circle, rgba(100,100,200,0.20) 1px, transparent 1px)', 'anim-fireflies': animFireflies.light,
].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`,
},
}; };
export const getChatBg = ( export const getChatBg = (