1 Commits

Author SHA1 Message Date
olly 4919bc26e5 visuals, fixes & storybook upd 2026-06-01 01:09:55 +03:00
19 changed files with 1704 additions and 342 deletions
+1
View File
@@ -238,6 +238,7 @@ const App = () => {
<div className="cap">Slider &amp; stepper</div> <div className="cap">Slider &amp; stepper</div>
<div className="cluster"> <div className="cluster">
<Slider defaultValue={[62]} max={100} step={1} /> <Slider defaultValue={[62]} max={100} step={1} />
<Slider defaultValue={[40]} max={100} step={20} marks />
<Stepper <Stepper
onDecrement={() => setCount((n) => Math.max(0, n - 1))} onDecrement={() => setCount((n) => Math.max(0, n - 1))}
onIncrement={() => setCount((n) => n + 1)} onIncrement={() => setCount((n) => n + 1)}
+109 -43
View File
@@ -1,59 +1,125 @@
import { type ComponentPropsWithoutRef } from 'react'; import { type ComponentPropsWithoutRef, type CSSProperties } from 'react';
import { Slider as RSlider } from 'radix-ui'; import { Slider as RSlider } from 'radix-ui';
type Step = { value: number; label?: string }; type Mark = { value: number; label?: string };
type MarksProp = boolean | Array<number | Mark>;
type NotchPlacement = 'top' | 'bottom' | 'both' | 'none';
type SliderProps = ComponentPropsWithoutRef<typeof RSlider.Root> & { type KnobStyle = 'square' | 'round';
steps?: number | Step[];
type SliderProps = Omit<ComponentPropsWithoutRef<typeof RSlider.Root>, 'className'> & {
/**
* Step marks.
* - `true` — auto-generate one mark per `step` between `min` and `max`.
* - array — explicit marks; numbers or `{ value, label }` for tick labels.
*/
marks?: MarksProp;
/**
* Where to draw the notch ticks relative to the track.
* `'bottom'` (default), `'top'`, `'both'`, or `'none'` to hide ticks
* (labels still render when provided). No effect without `marks`.
*/
notches?: NotchPlacement;
/** Thumb shape. `'square'` (default) has a small border-radius; `'round'` is a full circle. */
knobStyle?: KnobStyle;
className?: string;
}; };
function resolveSteps(steps: number | Step[], min: number, max: number): Step[] { function resolveMarks(
if (Array.isArray(steps)) return steps; marks: MarksProp,
if (steps < 2) return []; min: number,
return Array.from({ length: steps }, (_, i) => ({ max: number,
value: min + (i / (steps - 1)) * (max - min), step: number,
})); ): Mark[] {
if (Array.isArray(marks)) {
return marks
.map((m) => (typeof m === 'number' ? { value: m } : m))
.filter((m) => m.value >= min && m.value <= max);
}
if (marks !== true) return [];
if (!(step > 0) || max <= min) return [];
const count = Math.floor((max - min) / step);
// Guard against absurd notch counts (e.g. step=1 over a 01000 range).
if (count < 1 || count > 100) return [];
return Array.from({ length: count + 1 }, (_, i) => ({ value: min + i * step }));
} }
export const Slider = ({ steps, min = 0, max = 100, ...props }: SliderProps) => { const percent = (value: number, min: number, max: number) =>
const resolved = steps != null ? resolveSteps(steps, min, max) : []; max === min ? 0 : (value - min) / (max - min);
const hasSteps = resolved.length > 0;
const NotchLayer = ({
marks,
min,
max,
side,
}: {
marks: Mark[];
min: number;
max: number;
side: 'top' | 'bottom';
}) => (
<div className={`modern-sk-slider__notches modern-sk-slider__notches--${side}`} aria-hidden>
{marks.map((mark) => (
<span
key={mark.value}
className="modern-sk-slider__notch"
style={{ '--p': percent(mark.value, min, max) } as CSSProperties}
/>
))}
</div>
);
export const Slider = ({
marks,
notches = 'bottom',
knobStyle = 'square',
min = 0,
max = 100,
step = 1,
className,
...props
}: SliderProps) => {
const resolved = marks != null ? resolveMarks(marks, min, max, step) : [];
const hasMarks = resolved.length > 0;
const hasLabels = resolved.some((m) => m.label != null);
const showTop = hasMarks && (notches === 'top' || notches === 'both');
const showBottom = hasMarks && (notches === 'bottom' || notches === 'both');
const cls = [
'modern-sk-slider',
`modern-sk-slider--knob-${knobStyle}`,
hasMarks && 'modern-sk-slider--has-marks',
hasLabels && 'modern-sk-slider--has-labels',
showTop && 'modern-sk-slider--notch-top',
showBottom && 'modern-sk-slider--notch-bottom',
className,
]
.filter(Boolean)
.join(' ');
return ( return (
<RSlider.Root <RSlider.Root className={cls} min={min} max={max} step={step} {...props}>
className={`modern-sk-slider${hasSteps ? ' modern-sk-slider--has-steps' : ''}`}
min={min}
max={max}
{...props}
>
<RSlider.Track className="modern-sk-slider__track"> <RSlider.Track className="modern-sk-slider__track">
<RSlider.Range className="modern-sk-slider__range" /> <RSlider.Range className="modern-sk-slider__range" />
{hasSteps && resolved.map((step) => ( {showTop && <NotchLayer marks={resolved} min={min} max={max} side="top" />}
<div {showBottom && <NotchLayer marks={resolved} min={min} max={max} side="bottom" />}
key={step.value} {hasLabels && (
className="modern-sk-slider__step-dot" <div className="modern-sk-slider__labels" aria-hidden>
aria-hidden {resolved.map((mark) =>
style={{ left: `${((step.value - min) / (max - min)) * 100}%` }} mark.label != null ? (
/> <span
))} key={mark.value}
className="modern-sk-slider__label"
style={{ '--p': percent(mark.value, min, max) } as CSSProperties}
>
{mark.label}
</span>
) : null,
)}
</div>
)}
</RSlider.Track> </RSlider.Track>
<RSlider.Thumb className="modern-sk-slider__thumb" aria-label="Value" /> <RSlider.Thumb className="modern-sk-slider__thumb" aria-label="Value" />
{hasSteps && (
<div className="modern-sk-slider__steps" aria-hidden>
{resolved.map((step) => (
<div
key={step.value}
className="modern-sk-slider__step"
style={{ left: `${((step.value - min) / (max - min)) * 100}%` }}
>
<div className="modern-sk-slider__step-tick" />
{step.label != null && (
<span className="modern-sk-slider__step-label">{step.label}</span>
)}
</div>
))}
</div>
)}
</RSlider.Root> </RSlider.Root>
); );
}; };
+47 -18
View File
@@ -6,7 +6,9 @@ export const Spinner = ({
className, className,
...props ...props
}: ComponentPropsWithoutRef<'span'> & { size?: 'sm' | 'lg' }) => { }: ComponentPropsWithoutRef<'span'> & { size?: 'sm' | 'lg' }) => {
const gid = `modern-sk-groove-${useId()}`; const uid = useId();
const grooveId = `modern-sk-groove-${uid}`;
const glowId = `modern-sk-glow-${uid}`;
return ( return (
<span <span
role="status" role="status"
@@ -16,29 +18,56 @@ export const Spinner = ({
> >
<svg viewBox="0 0 36 36" fill="none"> <svg viewBox="0 0 36 36" fill="none">
<defs> <defs>
<linearGradient {/* Carved channel: flat ring sunk by a top inner shadow — like the switch well. */}
id={gid} <filter id={grooveId} x="-30%" y="-30%" width="160%" height="160%">
x1="18" <feComponentTransfer in="SourceAlpha" result="inv">
y1="4" <feFuncA type="table" tableValues="1 0" />
x2="18" </feComponentTransfer>
y2="32" <feGaussianBlur in="inv" stdDeviation="1" result="blur" />
gradientUnits="userSpaceOnUse" <feOffset in="blur" dy="1" result="off" />
> <feFlood floodColor="#000" floodOpacity="0.7" />
<stop offset="0" stopColor="var(--spin-groove-1)" /> <feComposite in2="off" operator="in" />
<stop offset="1" stopColor="var(--spin-groove-2)" /> <feComposite in2="SourceAlpha" operator="in" result="shadow" />
</linearGradient> <feMerge>
<feMergeNode in="SourceGraphic" />
<feMergeNode in="shadow" />
</feMerge>
</filter>
{/* Soft round glow — generous region so it never clips to a square. */}
<filter id={glowId} x="-100%" y="-100%" width="300%" height="300%">
<feGaussianBlur stdDeviation="1.6" />
</filter>
</defs> </defs>
<circle cx="18" cy="18" r="14" stroke={`url(#${gid})`} strokeWidth="5" />
<circle <circle
className="modern-sk-spinner__arc"
cx="18" cx="18"
cy="18" cy="18"
r="14" r="14"
stroke="var(--lime)" stroke="var(--spin-track)"
strokeWidth="3" strokeWidth="5"
strokeLinecap="round" filter={`url(#${grooveId})`}
strokeDasharray="22 88"
/> />
<g className="modern-sk-spinner__arc">
<circle
cx="18"
cy="18"
r="14"
stroke="var(--lime)"
strokeWidth="4"
strokeLinecap="round"
strokeDasharray="22 88"
opacity="0.8"
filter={`url(#${glowId})`}
/>
<circle
cx="18"
cy="18"
r="14"
stroke="var(--lime)"
strokeWidth="3"
strokeLinecap="round"
strokeDasharray="22 88"
/>
</g>
</svg> </svg>
</span> </span>
); );
+1
View File
@@ -26,6 +26,7 @@ const meta = {
iconOnly: { control: 'boolean', description: 'Square padding for a single glyph.' }, iconOnly: { control: 'boolean', description: 'Square padding for a single glyph.' },
disabled: { control: 'boolean' }, disabled: { control: 'boolean' },
children: { control: 'text' }, children: { control: 'text' },
className: { control: 'text' },
}, },
args: { children: 'Button', variant: 'key' }, args: { children: 'Button', variant: 'key' },
} satisfies Meta<typeof Button>; } satisfies Meta<typeof Button>;
+31 -4
View File
@@ -10,6 +10,7 @@ import {
Th, Th,
Td, Td,
Badge, Badge,
Chip,
} from '../components/ui'; } from '../components/ui';
const meta = { const meta = {
@@ -18,18 +19,23 @@ const meta = {
parameters: { parameters: {
docs: { docs: {
description: { description: {
component: 'Cards, selectable lists/rows, and the bordered table.', component: 'Cards, selectable lists/rows, badges, chips, and the bordered table.',
}, },
}, },
}, },
argTypes: {
className: { control: 'text' },
},
} satisfies Meta<typeof Card>; } satisfies Meta<typeof Card>;
export default meta; export default meta;
type Story = StoryObj<typeof meta>; type Story = StoryObj<typeof meta>;
export const CardSurface: Story = { export const CardPlayground: Story = {
render: () => ( name: 'Card',
<Card style={{ maxWidth: 320, padding: 20 }}> args: { children: 'Card content' },
render: (args) => (
<Card {...args} style={{ maxWidth: 320, padding: 20 }}>
<h3 className="modern-sk-h3">Storage</h3> <h3 className="modern-sk-h3">Storage</h3>
<p className="modern-sk-body">128 GB of 256 GB used.</p> <p className="modern-sk-body">128 GB of 256 GB used.</p>
</Card> </Card>
@@ -47,6 +53,27 @@ export const ListRows: Story = {
), ),
}; };
export const BadgeShowcase: Story = {
render: () => (
<div style={{ display: 'flex', gap: 10, alignItems: 'center', flexWrap: 'wrap' }}>
<Badge variant="lime">Lime</Badge>
<Badge variant="ember">Ember</Badge>
<Badge variant="neutral">Neutral</Badge>
<Badge variant="outline">Outline</Badge>
<Badge variant="lime" dot>Online</Badge>
</div>
),
};
export const Chips: Story = {
render: () => (
<div style={{ display: 'flex', gap: 10, flexWrap: 'wrap' }}>
<Chip>Design</Chip>
<Chip onRemove={() => {}}>Removable</Chip>
</div>
),
};
export const DataTable: Story = { export const DataTable: Story = {
render: () => ( render: () => (
<Table> <Table>
+13
View File
@@ -14,11 +14,24 @@ const meta = {
}, },
}, },
}, },
argTypes: {
value: { control: { type: 'range', min: 0, max: 100, step: 1 } },
className: { control: 'text' },
},
args: { value: 40 },
} satisfies Meta<typeof Progress>; } satisfies Meta<typeof Progress>;
export default meta; export default meta;
type Story = StoryObj<typeof meta>; type Story = StoryObj<typeof meta>;
export const Playground: Story = {
render: (args) => (
<div style={{ width: 320 }}>
<Progress {...args} />
</div>
),
};
function ProgressDemo() { function ProgressDemo() {
const [v, setV] = useState(40); const [v, setV] = useState(40);
return ( return (
+11 -2
View File
@@ -14,8 +14,17 @@ const meta = {
}, },
}, },
argTypes: { argTypes: {
variant: { control: 'inline-radio', options: ['key', 'primary', 'ember', 'ghost'] }, variant: {
size: { control: 'inline-radio', options: ['sm', undefined, 'lg'] }, control: 'inline-radio',
options: ['key', 'primary', 'ember', 'ghost'],
description: 'Visual emphasis. `key` is the default neutral button.',
},
size: {
control: 'inline-radio',
options: ['sm', undefined, 'lg'],
description: 'Button size: `sm` compact, default regular, `lg` large.',
},
disabled: { control: 'boolean' },
}, },
} satisfies Meta<typeof IconButton>; } satisfies Meta<typeof IconButton>;
+17 -2
View File
@@ -25,17 +25,32 @@ const meta = {
}, },
argTypes: { argTypes: {
children: { control: false }, children: { control: false },
content: { control: false }, content: { control: 'text' },
delayDuration: { control: 'number' },
open: { control: 'boolean' },
defaultOpen: { control: 'boolean' },
onOpenChange: { action: 'open changed' },
sideOffset: { control: 'number' },
}, },
args: { args: {
content: '', content: 'Tooltip text',
children: null, children: null,
delayDuration: 0,
}, },
} satisfies Meta<typeof Tooltip>; } satisfies Meta<typeof Tooltip>;
export default meta; export default meta;
type Story = StoryObj<typeof meta>; type Story = StoryObj<typeof meta>;
export const Playground: Story = {
name: 'Tooltip Playground',
render: (args) => (
<Tooltip {...args}>
<Button variant="ghost">Hover me</Button>
</Tooltip>
),
};
export const TooltipStory: Story = { export const TooltipStory: Story = {
name: 'Tooltip', name: 'Tooltip',
render: () => ( render: () => (
+73
View File
@@ -0,0 +1,73 @@
import type { Meta, StoryObj } from 'storybook-react-rsbuild';
import { ScrollArea } from '../components/ui';
const meta = {
title: 'Layout/ScrollArea',
component: ScrollArea,
parameters: {
docs: {
description: {
component:
'Radix ScrollArea with custom styled scrollbars. Wraps content in a viewport with vertical and horizontal scrollbars.',
},
},
},
argTypes: {
children: { control: false },
className: { control: 'text' },
},
} satisfies Meta<typeof ScrollArea>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Playground: Story = {
render: () => (
<ScrollArea style={{ width: 300, height: 200, border: '1px solid var(--neutral-border)' }}>
<div style={{ padding: 16 }}>
<h4 className="modern-sk-h4" style={{ marginBottom: 8 }}>Scrollable content</h4>
{Array.from({ length: 20 }).map((_, i) => (
<p key={i} className="modern-sk-body" style={{ marginBottom: 12 }}>
Item {i + 1}: Lorem ipsum dolor sit amet, consectetur adipiscing elit.
</p>
))}
</div>
</ScrollArea>
),
};
export const Vertical: Story = {
render: () => (
<ScrollArea style={{ width: 280, height: 150, border: '1px solid var(--neutral-border)' }}>
<div style={{ padding: 12 }}>
{Array.from({ length: 30 }).map((_, i) => (
<div key={i} style={{ padding: 8, borderBottom: '1px solid var(--neutral-border)' }}>
Row {i + 1}
</div>
))}
</div>
</ScrollArea>
),
};
export const Horizontal: Story = {
render: () => (
<ScrollArea style={{ width: 320, height: 80, border: '1px solid var(--neutral-border)' }}>
<div style={{ display: 'flex', gap: 8, padding: 12, width: 'fit-content' }}>
{Array.from({ length: 20 }).map((_, i) => (
<div
key={i}
style={{
padding: 8,
minWidth: 100,
background: 'var(--neutral-surface)',
borderRadius: 4,
}}
>
Item {i + 1}
</div>
))}
</div>
</ScrollArea>
),
};
+68
View File
@@ -0,0 +1,68 @@
import { useState } from 'react';
import type { Meta, StoryObj } from 'storybook-react-rsbuild';
import { SegmentedControl } from '../components/ui';
const meta = {
title: 'Selection/SegmentedControl',
component: SegmentedControl,
parameters: {
docs: {
description: {
component:
'Single-select button group with animated thumb. Pass `items` array of `{ value, label }` objects, plus `value` and `onValueChange` for control.',
},
},
},
argTypes: {
value: { control: 'text' },
onValueChange: { action: 'value changed' },
items: { control: false },
className: { control: 'text' },
disabled: { control: 'boolean' },
},
} satisfies Meta<typeof SegmentedControl>;
export default meta;
type Story = StoryObj<typeof meta>;
function SegmentedDemo() {
const [v, setV] = useState('day');
return (
<SegmentedControl
value={v}
onValueChange={setV}
items={[
{ value: 'day', label: 'Day' },
{ value: 'week', label: 'Week' },
{ value: 'month', label: 'Month' },
]}
/>
);
}
export const Playground: Story = {
render: () => <SegmentedDemo />,
};
export const TimeRange: Story = {
render: () => <SegmentedDemo />,
};
function OptionsDemo() {
const [v, setV] = useState('draft');
return (
<SegmentedControl
value={v}
onValueChange={setV}
items={[
{ value: 'draft', label: 'Draft' },
{ value: 'published', label: 'Published' },
{ value: 'archived', label: 'Archived' },
]}
/>
);
}
export const Options: Story = {
render: () => <OptionsDemo />,
};
+9
View File
@@ -19,6 +19,15 @@ const meta = {
}, },
}, },
}, },
argTypes: {
items: { control: false },
placeholder: { control: 'text' },
disabled: { control: 'boolean' },
defaultValue: { control: 'text' },
value: { control: 'text' },
onValueChange: { action: 'value changed' },
'aria-label': { control: 'text' },
},
args: { items, placeholder: 'Pick a release…', 'aria-label': 'macOS release' }, args: { items, placeholder: 'Pick a release…', 'aria-label': 'macOS release' },
} satisfies Meta<typeof Select>; } satisfies Meta<typeof Select>;
+10
View File
@@ -22,11 +22,21 @@ const meta = {
}, },
}, },
}, },
argTypes: {
defaultChecked: { control: 'boolean' },
checked: { control: 'boolean' },
disabled: { control: 'boolean' },
onCheckedChange: { action: 'checked changed' },
},
} satisfies Meta<typeof Switch>; } satisfies Meta<typeof Switch>;
export default meta; export default meta;
type Story = StoryObj<typeof meta>; type Story = StoryObj<typeof meta>;
export const Playground: Story = {
args: { defaultChecked: false },
};
export const Switches: Story = { export const Switches: Story = {
render: () => ( render: () => (
<div style={{ display: 'flex', gap: 20, alignItems: 'center' }}> <div style={{ display: 'flex', gap: 20, alignItems: 'center' }}>
+54 -3
View File
@@ -8,16 +8,67 @@ const meta = {
docs: { docs: {
description: { description: {
component: component:
'Radix Slider in the carved-track skin. All Radix Slider props pass through (`defaultValue`, `min`, `max`, `step`, `onValueChange`).', 'Radix Slider in the carved-track skin. All Radix Slider props pass through (`defaultValue`, `min`, `max`, `step`, `onValueChange`). Set `marks` to carve step notches into the track — `marks` snaps to the Radix `step`.',
}, },
}, },
}, },
args: { defaultValue: [60], min: 0, max: 100, step: 1 },
argTypes: {
defaultValue: { control: 'object', description: 'Uncontrolled starting value(s).' },
min: { control: 'number' },
max: { control: 'number' },
step: { control: 'number', description: 'Snap increment (also drives auto `marks`).' },
disabled: { control: 'boolean' },
marks: {
control: 'boolean',
description: 'Step marks. `true` derives one per `step`; or pass an array for custom/labelled marks.',
},
notches: {
control: 'inline-radio',
options: ['top', 'bottom', 'both', 'none'],
description: 'Notch tick placement relative to the track (labels still render when `none`).',
},
knobStyle: {
control: 'inline-radio',
options: ['square', 'round'],
description: 'Knob shape: `square` (default) or `round`.',
},
className: { control: 'text' },
},
decorators: [(Story) => <div style={{ width: 280 }}><Story /></div>], decorators: [(Story) => <div style={{ width: 280 }}><Story /></div>],
} satisfies Meta<typeof Slider>; } satisfies Meta<typeof Slider>;
export default meta; export default meta;
type Story = StoryObj<typeof meta>; type Story = StoryObj<typeof meta>;
export const Playground: Story = { args: { defaultValue: [60], max: 100, step: 1 } }; export const Playground: Story = {};
export const Stepped: Story = { args: { defaultValue: [40], max: 100, step: 10 } }; /** `marks` auto-derives one notch per `step`. */
export const Stepped: Story = {
args: { defaultValue: [40], step: 10, marks: true },
};
/** `notches='both'` carves ticks above and below the bar. */
export const NotchesBoth: Story = {
args: { defaultValue: [60], step: 20, marks: true, notches: 'both' },
};
/** Pass an array of `{ value, label }` for labelled ticks. */
export const LabelledMarks: Story = {
args: {
defaultValue: [50],
step: 25,
notches: 'bottom',
marks: [
{ value: 0, label: 'Off' },
{ value: 25, label: 'Low' },
{ value: 50, label: 'Mid' },
{ value: 75, label: 'High' },
{ value: 100, label: 'Max' },
],
},
};
export const Disabled: Story = {
args: { defaultValue: [30], step: 10, marks: true, disabled: true },
};
+6
View File
@@ -12,6 +12,12 @@ const meta = {
}, },
}, },
}, },
argTypes: {
defaultValue: { control: 'text' },
value: { control: 'text' },
onValueChange: { action: 'value changed' },
disabled: { control: 'boolean' },
},
} satisfies Meta<typeof Tabs>; } satisfies Meta<typeof Tabs>;
export default meta; export default meta;
+10
View File
@@ -13,6 +13,16 @@ const meta = {
}, },
}, },
}, },
argTypes: {
placeholder: { control: 'text' },
value: { control: 'text' },
defaultValue: { control: 'text' },
disabled: { control: 'boolean' },
readOnly: { control: 'boolean' },
required: { control: 'boolean' },
type: { control: 'text' },
onChange: { action: 'changed' },
},
args: { placeholder: 'Type here…' }, args: { placeholder: 'Type here…' },
} satisfies Meta<typeof TextField>; } satisfies Meta<typeof TextField>;
+5
View File
@@ -12,6 +12,11 @@ const meta = {
}, },
}, },
}, },
argTypes: {
title: { control: 'text' },
badge: { control: false },
children: { control: false },
},
args: { title: 'Finder' }, args: { title: 'Finder' },
} satisfies Meta<typeof Window>; } satisfies Meta<typeof Window>;
+1227 -262
View File
File diff suppressed because it is too large Load Diff
+7 -1
View File
@@ -7,7 +7,13 @@
@import './tokens.css'; @import './tokens.css';
@import './components.css'; @import './components.css';
/* Non-invasive box-sizing for our own components only (zero specificity). */ /* Non-invasive box-sizing + branded font for our own components only
(zero specificity, so consumer elements are never touched and the
--font-mono / --font-display classes still win by class specificity).
This is what carries the typeface onto portalled content (tooltips,
menus, dialogs) and bare text nodes (control labels) that never set
their own font-family. */
:where([class^='modern-sk-'], [class*=' modern-sk-']) { :where([class^='modern-sk-'], [class*=' modern-sk-']) {
box-sizing: border-box; box-sizing: border-box;
font-family: var(--font-sans);
} }
+5 -7
View File
@@ -178,10 +178,9 @@
--grain-opacity: 0.45; --grain-opacity: 0.45;
/* ---------- SPINNER GROOVE ---------- /* ---------- SPINNER GROOVE ----------
Carved donut channel — dark at the top rim, catching light at the Solid carved channel — flat base felt, sunk by an SVG inner shadow,
bottom, exactly like the sunk wells (switch / field). */ exactly like the switch well (no gradient). */
--spin-groove-1: #090a07; /* top — in shadow */ --spin-track: #23241c;
--spin-groove-2: #34352b; /* bottom — catches light */
} }
/* ============================================================ /* ============================================================
@@ -264,9 +263,8 @@
radial-gradient(90% 70% at 85% 110%, rgba(233,87,43,0.08), transparent 60%), radial-gradient(90% 70% at 85% 110%, rgba(233,87,43,0.08), transparent 60%),
radial-gradient(100% 100% at 50% 50%, #f2f2ea 0%, #ecece3 60%, #e2e2d6 100%); radial-gradient(100% 100% at 50% 50%, #f2f2ea 0%, #ecece3 60%, #e2e2d6 100%);
/* carved groove on warm paper — top grey shadow, bottom near-white */ /* carved groove on warm paper — flat base, sunk by inner shadow */
--spin-groove-1: #c2c3b6; --spin-track: #dcdcd2;
--spin-groove-2: #ffffff;
} }
/* ============================================================ /* ============================================================